links_notation/binary/
format.rs1use super::error::{BinaryError, BinaryResult};
10use super::mapping::LinoDocument;
11use crate::{parse_lino_to_links, LiNo};
12
13pub fn parse_document(text: &str) -> BinaryResult<LinoDocument> {
15 if text
16 .bytes()
17 .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
18 {
19 return Ok(Vec::new());
20 }
21 let links =
22 parse_lino_to_links(text).map_err(|error| BinaryError::InvalidLino(error.to_string()))?;
23 Ok(links.into_iter().map(canonical).collect())
24}
25
26pub fn canonical(link: LiNo<String>) -> LiNo<String> {
31 match link {
32 LiNo::Link {
33 id: None,
34 mut values,
35 } if values.len() == 1 && matches!(values[0], LiNo::Ref(_)) => {
36 values.pop().expect("one value")
37 }
38 LiNo::Link { id, values } => LiNo::Link {
39 id,
40 values: values.into_iter().map(canonical).collect(),
41 },
42 reference => reference,
43 }
44}
45
46pub fn format_document(document: &[LiNo<String>]) -> String {
52 document
53 .iter()
54 .map(format_top_level)
55 .collect::<Vec<_>>()
56 .join("\n")
57}
58
59fn format_top_level(link: &LiNo<String>) -> String {
60 match link {
61 LiNo::Link { id: None, values } if values.len() >= 2 => join_values(values),
62 link => format_link(link),
63 }
64}
65
66pub fn format_link(link: &LiNo<String>) -> String {
68 match link {
69 LiNo::Ref(reference) => format_reference(reference),
70 LiNo::Link { id: None, values } => match values.as_slice() {
71 [LiNo::Ref(reference)] => format!("(({}))", format_reference(reference)),
74 values => format!("({})", join_values(values)),
75 },
76 LiNo::Link {
77 id: Some(id),
78 values,
79 } => {
80 if values.is_empty() {
81 format!("({}:)", format_reference(id))
82 } else {
83 format!("({}: {})", format_reference(id), join_values(values))
84 }
85 }
86 }
87}
88
89fn join_values(values: &[LiNo<String>]) -> String {
90 values.iter().map(format_link).collect::<Vec<_>>().join(" ")
91}
92
93pub fn format_reference(reference: &str) -> String {
102 let needs_quotes = reference.is_empty()
103 || reference.starts_with('#')
104 || reference.chars().any(|character| {
105 character.is_whitespace()
106 || matches!(
107 character,
108 '\u{1c}'..='\u{1f}' | '\u{feff}' | '(' | ')' | ':' | '\'' | '"' | '`'
109 )
110 });
111 if !needs_quotes {
112 return reference.to_string();
113 }
114 let first = reference.chars().next();
115 let (quote, count) = ['\'', '"', '`']
116 .into_iter()
117 .filter(|"e| first != Some(quote))
118 .map(|quote| (quote, (longest_run(reference, quote) + 1) | 1))
119 .min_by_key(|&(_, count)| count)
120 .expect("a reference starts with at most one of three quote characters");
121 let delimiter = quote.to_string().repeat(count);
122 format!("{delimiter}{reference}{delimiter}")
123}
124
125fn longest_run(text: &str, quote: char) -> usize {
126 let (mut longest, mut current) = (0, 0);
127 for character in text.chars() {
128 current = if character == quote { current + 1 } else { 0 };
129 longest = longest.max(current);
130 }
131 longest
132}