link_cli/protocol/
format.rs1use super::error::{ProtocolError, ProtocolResult};
10use super::mapping::LinoDocument;
11use links_notation::{parse_lino_to_links, LiNo};
12
13pub fn parse_document(text: &str) -> ProtocolResult<LinoDocument> {
15 if text.trim().is_empty() {
16 return Ok(Vec::new());
17 }
18 let links =
19 parse_lino_to_links(text).map_err(|error| ProtocolError::InvalidLino(error.to_string()))?;
20 Ok(links.into_iter().map(canonical).collect())
21}
22
23pub fn canonical(link: LiNo<String>) -> LiNo<String> {
28 match link {
29 LiNo::Link {
30 id: None,
31 mut values,
32 } if values.len() == 1 && matches!(values[0], LiNo::Ref(_)) => {
33 values.pop().expect("one value")
34 }
35 LiNo::Link { id, values } => LiNo::Link {
36 id,
37 values: values.into_iter().map(canonical).collect(),
38 },
39 reference => reference,
40 }
41}
42
43pub fn format_document(document: &[LiNo<String>]) -> String {
49 document
50 .iter()
51 .map(format_top_level)
52 .collect::<Vec<_>>()
53 .join("\n")
54}
55
56fn format_top_level(link: &LiNo<String>) -> String {
57 match link {
58 LiNo::Link { id: None, values } if values.len() >= 2 => join_values(values),
59 link => format_link(link),
60 }
61}
62
63pub fn format_link(link: &LiNo<String>) -> String {
65 match link {
66 LiNo::Ref(reference) => format_reference(reference),
67 LiNo::Link { id: None, values } => match values.as_slice() {
68 [LiNo::Ref(reference)] => format!("(({}))", format_reference(reference)),
71 values => format!("({})", join_values(values)),
72 },
73 LiNo::Link {
74 id: Some(id),
75 values,
76 } => {
77 if values.is_empty() {
78 format!("({}:)", format_reference(id))
79 } else {
80 format!("({}: {})", format_reference(id), join_values(values))
81 }
82 }
83 }
84}
85
86fn join_values(values: &[LiNo<String>]) -> String {
87 values.iter().map(format_link).collect::<Vec<_>>().join(" ")
88}
89
90pub fn format_reference(reference: &str) -> String {
99 let needs_quotes = reference.is_empty()
100 || reference.chars().any(|character| {
101 character.is_whitespace() || matches!(character, '(' | ')' | ':' | '\'' | '"' | '`')
102 });
103 if !needs_quotes {
104 return reference.to_string();
105 }
106 let first = reference.chars().next();
107 let (quote, count) = ['\'', '"', '`']
108 .into_iter()
109 .filter(|"e| first != Some(quote))
110 .map(|quote| (quote, (longest_run(reference, quote) + 1) | 1))
111 .min_by_key(|&(_, count)| count)
112 .expect("a reference starts with at most one of three quote characters");
113 let delimiter = quote.to_string().repeat(count);
114 format!("{delimiter}{reference}{delimiter}")
115}
116
117fn longest_run(text: &str, quote: char) -> usize {
118 let (mut longest, mut current) = (0, 0);
119 for character in text.chars() {
120 current = if character == quote { current + 1 } else { 0 };
121 longest = longest.max(current);
122 }
123 longest
124}