links_notation/binary/format.rs
1//! Canonical LiNo text for documents, shared by every protocol.
2//!
3//! Native and binary text formatting share a lossless reference encoder.
4//! This canonical group formatter guarantees that
5//! `parse(format(document)) == document` for every parsed document, which is
6//! what makes the text and binary protocols interchangeable.
7
8use super::error::{BinaryError, BinaryResult};
9use super::mapping::LinoDocument;
10use crate::{parse_lino_to_links, LiNo};
11
12/// Parses LiNo text into a canonical document. Blank input is the empty document.
13pub fn parse_document(text: &str) -> BinaryResult<LinoDocument> {
14 if text
15 .bytes()
16 .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
17 {
18 return Ok(Vec::new());
19 }
20 let links =
21 parse_lino_to_links(text).map_err(|error| BinaryError::InvalidLino(error.to_string()))?;
22 Ok(links.into_iter().map(canonical).collect())
23}
24
25/// Converts a parsed link into the canonical model: an unnamed group holding
26/// exactly one reference is that reference, so `a` and `(a)` both parse as
27/// the reference `a`. links-notation (since 0.17) and its C# port keep that
28/// wrapper; removing it gives both ports the same document model.
29pub fn canonical(link: LiNo<String>) -> LiNo<String> {
30 match link {
31 LiNo::Link {
32 id: None,
33 mut values,
34 } if values.len() == 1 && matches!(values[0], LiNo::Ref(_)) => {
35 values.pop().expect("one value")
36 }
37 LiNo::Link { id, values } => LiNo::Link {
38 id,
39 values: values.into_iter().map(canonical).collect(),
40 },
41 reference => reference,
42 }
43}
44
45/// Formats a document as canonical LiNo text, one top-level link per line.
46///
47/// A top-level link without an id and with at least two values is written
48/// without its outer parentheses, the way queries are usually typed:
49/// `() ((1 1))`.
50pub fn format_document(document: &[LiNo<String>]) -> String {
51 document
52 .iter()
53 .map(format_top_level)
54 .collect::<Vec<_>>()
55 .join("\n")
56}
57
58fn format_top_level(link: &LiNo<String>) -> String {
59 match link {
60 LiNo::Link { id: None, values } if values.len() >= 2 => join_values(values),
61 link => format_link(link),
62 }
63}
64
65/// Formats one link as it appears nested inside another link.
66pub fn format_link(link: &LiNo<String>) -> String {
67 match link {
68 LiNo::Ref(reference) => format_reference(reference),
69 LiNo::Link { id: None, values } => match values.as_slice() {
70 // `(a)` parses back as the reference `a`, so a one-reference
71 // link needs a second pair of parentheses.
72 [LiNo::Ref(reference)] => format!("(({}))", format_reference(reference)),
73 values => format!("({})", join_values(values)),
74 },
75 LiNo::Link {
76 id: Some(id),
77 values,
78 } => {
79 if values.is_empty() {
80 format!("({}:)", format_reference(id))
81 } else {
82 format!("({}: {})", format_reference(id), join_values(values))
83 }
84 }
85 }
86}
87
88fn join_values(values: &[LiNo<String>]) -> String {
89 values.iter().map(format_link).collect::<Vec<_>>().join(" ")
90}
91
92/// Quotes a reference when it would not survive parsing as a bare word.
93///
94/// links-notation opens a quoted reference with a run of `N` equal quote
95/// characters, closes it with the next run of exactly `N`, and reads `2N`
96/// quotes inside as `N` literal ones. The opening run is counted greedily, so
97/// the chosen quote must differ from the first character; `N` is one more
98/// than the longest run of that quote inside, and odd, because an even
99/// delimiter run may be read as an empty reference.
100pub fn format_reference(reference: &str) -> String {
101 crate::reference_literal::format_reference(reference)
102}