links_notation/
reference_literal.rs1pub fn encode_reference_literal(text: &str) -> String {
5 use std::fmt::Write;
6 let mut result = String::with_capacity(text.len() * 2 + 4);
7 result.push_str("~1{");
8 for byte in text.bytes() {
9 write!(result, "{byte:02x}").expect("writing to a string cannot fail");
10 }
11 result.push('}');
12 result
13}
14
15pub fn decode_reference_literal(literal: &str) -> Result<String, &'static str> {
17 let prefix = prefix_end(literal).ok_or("invalid reference literal")?;
18 if &literal[..prefix] != "~1{" || !literal.ends_with('}') {
19 return Err("invalid or unsupported reference literal");
20 }
21 let hex = &literal[prefix..literal.len() - 1];
22 if !hex.len().is_multiple_of(2) || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
23 return Err("reference literal requires an even number of hex digits");
24 }
25 let bytes = (0..hex.len())
26 .step_by(2)
27 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("validated hex"))
28 .collect();
29 String::from_utf8(bytes).map_err(|_| "reference literal must contain valid UTF-8")
30}
31
32pub(crate) fn prefix_end(text: &str) -> Option<usize> {
33 let bytes = text.as_bytes();
34 if bytes.first() != Some(&b'~') {
35 return None;
36 }
37 let mut position = 1;
38 while bytes.get(position).is_some_and(u8::is_ascii_digit) {
39 position += 1;
40 }
41 (position > 1 && bytes.get(position) == Some(&b'{')).then_some(position + 1)
42}
43
44pub(crate) fn format_reference(text: &str) -> String {
45 if text.is_empty() || text.chars().any(|c| c < ' ' || c == '\u{7f}') {
46 return encode_reference_literal(text);
47 }
48 if prefix_end(text).is_none()
49 && !text.starts_with('#')
50 && !text.chars().any(|c| {
51 c.is_whitespace() || matches!(c, '\u{feff}' | '(' | ')' | ':' | '\'' | '"' | '`')
52 })
53 {
54 return text.to_string();
55 }
56 let first = text.chars().next();
57 let (quote, count) = ['\'', '"', '`']
58 .into_iter()
59 .filter(|"e| first != Some(quote))
60 .map(|quote| {
61 let (mut longest, mut current) = (0, 0);
62 for c in text.chars() {
63 current = if c == quote { current + 1 } else { 0 };
64 longest = longest.max(current);
65 }
66 (quote, (longest + 1) | 1)
67 })
68 .min_by_key(|&(_, count)| count)
69 .expect("at least two eligible delimiters");
70 let delimiter = quote.to_string().repeat(count);
71 format!("{delimiter}{text}{delimiter}")
72}