Skip to main content

links_notation/
cst.rs

1//! The concrete syntax tree of a Links Notation document.
2//!
3//! Every byte of a document belongs to exactly one token, so the text of the
4//! tree is the document itself, comments, blank lines, indentation, quotes and
5//! line endings included. A document is a list of `line` nodes; a line holds the
6//! tokens up to and including its line ending, and a parenthesized `group` node
7//! holds its own parentheses and the lines written between them. A group is part
8//! of the line it opens on, however many lines it spans.
9//!
10//! The tree records how a document was written; [`parse_lino_to_links`] reads
11//! what it means. They agree because a document is parsed before its tree is
12//! built, and the tokens are read with the same rules the parser uses: comments
13//! are found by [`strip_comments`], delimited references by the parser's own
14//! reader and versioned literals by the same prefix.
15//!
16//! [`parse_lino_to_links`]: crate::parse_lino_to_links
17//! [`strip_comments`]: crate::comments::strip_comments
18
19use crate::comments::COMMENT;
20use crate::quotes::{DelimitedReferences, QUOTES};
21use crate::reference_literal::{decode_reference_literal, prefix_end};
22use crate::{ParseError, ParserConfig, check_document};
23use std::fmt;
24
25/// The kinds of nodes and tokens of a concrete syntax tree.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum CstKind {
28    /// The whole document: its lines.
29    Document,
30    /// One line: its tokens and groups, up to and including its line ending.
31    Line,
32    /// A parenthesized group: `(`, the lines inside it, and `)`.
33    Group,
34    /// A run of spaces and tabs.
35    Whitespace,
36    /// One line ending: `\n`, `\r\n` or `\r`.
37    Newline,
38    /// A comment, from `#` up to the line ending.
39    Comment,
40    /// `(`
41    OpenParen,
42    /// `)`
43    CloseParen,
44    /// `:`
45    Colon,
46    /// A bare, quoted or versioned literal reference.
47    Reference,
48}
49
50impl CstKind {
51    /// The name every Links Notation implementation gives this kind, as used in
52    /// [`CstNode::to_s_expression`]: `document`, `line`, `group`, `whitespace`,
53    /// `newline`, `comment`, `open_paren`, `close_paren`, `colon` or
54    /// `reference`.
55    pub fn as_str(self) -> &'static str {
56        match self {
57            CstKind::Document => "document",
58            CstKind::Line => "line",
59            CstKind::Group => "group",
60            CstKind::Whitespace => "whitespace",
61            CstKind::Newline => "newline",
62            CstKind::Comment => "comment",
63            CstKind::OpenParen => "open_paren",
64            CstKind::CloseParen => "close_paren",
65            CstKind::Colon => "colon",
66            CstKind::Reference => "reference",
67        }
68    }
69
70    /// Whether this kind is a token, rather than a node with children.
71    pub fn is_token(self) -> bool {
72        !matches!(self, CstKind::Document | CstKind::Line | CstKind::Group)
73    }
74}
75
76impl fmt::Display for CstKind {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(self.as_str())
79    }
80}
81
82/// One node or token of a concrete syntax tree.
83///
84/// Its [`Display`](fmt::Display) writes the exact text it was read from.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CstNode {
87    /// What this node or token is.
88    pub kind: CstKind,
89    /// The children of a node, in document order; empty for a token.
90    pub children: Vec<CstNode>,
91    /// What a reference token means, its delimiters and escapes resolved and a
92    /// versioned literal decoded (the ID a parsed link has); `None` for every
93    /// other node and token.
94    pub value: Option<String>,
95    /// The delimiter a quoted reference token was written between (`'`, `"` or
96    /// `` ` ``); `None` for a bare or literal reference and every other kind.
97    pub quote: Option<char>,
98    /// A token's text exactly as written; empty for a node.
99    token_text: String,
100}
101
102impl CstNode {
103    fn node(kind: CstKind, children: Vec<CstNode>) -> Self {
104        CstNode {
105            kind,
106            children,
107            value: None,
108            quote: None,
109            token_text: String::new(),
110        }
111    }
112
113    fn token(kind: CstKind, text: &str) -> Self {
114        CstNode {
115            kind,
116            children: Vec::new(),
117            value: None,
118            quote: None,
119            token_text: text.to_owned(),
120        }
121    }
122
123    /// Whether this is a token rather than a node with children.
124    pub fn is_token(&self) -> bool {
125        self.kind.is_token()
126    }
127
128    /// The exact text this node or token was read from: a token's own text, and
129    /// the text of a node's children one after the other.
130    pub fn text(&self) -> String {
131        self.to_string()
132    }
133
134    /// Every token of this subtree, in document order.
135    pub fn tokens(&self) -> Vec<&CstNode> {
136        let mut tokens = Vec::new();
137        self.collect_tokens(&mut tokens);
138        tokens
139    }
140
141    fn collect_tokens<'a>(&'a self, tokens: &mut Vec<&'a CstNode>) {
142        if self.is_token() {
143            tokens.push(self);
144        } else {
145            for child in &self.children {
146                child.collect_tokens(tokens);
147            }
148        }
149    }
150
151    /// The tree as an S-expression: `(kind child ...)` for a node and
152    /// `kind:"text"` for a token, the text escaped as a JSON string would escape
153    /// `\`, `"`, line feeds, carriage returns and tabs. Two trees are the same
154    /// exactly when their S-expressions are.
155    ///
156    /// # Examples
157    /// ```
158    /// use links_notation::parse_lino_to_cst;
159    ///
160    /// assert_eq!(
161    ///     parse_lino_to_cst("a '1'").unwrap().to_s_expression(),
162    ///     r#"(document (line reference:"a" whitespace:" " reference:"'1'"))"#
163    /// );
164    /// ```
165    pub fn to_s_expression(&self) -> String {
166        let mut out = String::new();
167        self.write_s_expression(&mut out);
168        out
169    }
170
171    fn write_s_expression(&self, out: &mut String) {
172        if self.is_token() {
173            out.push_str(self.kind.as_str());
174            out.push_str(":\"");
175            for c in self.token_text.chars() {
176                match c {
177                    '\\' => out.push_str("\\\\"),
178                    '"' => out.push_str("\\\""),
179                    '\n' => out.push_str("\\n"),
180                    '\r' => out.push_str("\\r"),
181                    '\t' => out.push_str("\\t"),
182                    _ => out.push(c),
183                }
184            }
185            out.push('"');
186            return;
187        }
188        out.push('(');
189        out.push_str(self.kind.as_str());
190        for child in &self.children {
191            out.push(' ');
192            child.write_s_expression(out);
193        }
194        out.push(')');
195    }
196}
197
198impl fmt::Display for CstNode {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        if self.is_token() {
201            return f.write_str(&self.token_text);
202        }
203        for child in &self.children {
204            fmt::Display::fmt(child, f)?;
205        }
206        Ok(())
207    }
208}
209
210/// Reads a document, with comments, as a lossless concrete syntax tree.
211///
212/// Every character of the document, comments and whitespace included, belongs
213/// to exactly one token, so `parse_lino_to_cst(document)?.to_string()` is the
214/// document. The document is parsed first, so it is refused exactly when
215/// [`parse_lino_to_links`](crate::parse_lino_to_links) refuses it, with the
216/// same error.
217///
218/// # Examples
219/// ```
220/// use links_notation::{CstKind, parse_lino_to_cst};
221///
222/// let document = "# config\nname: 'my app' # quoted\n";
223/// let cst = parse_lino_to_cst(document).unwrap();
224/// assert_eq!(cst.to_string(), document);
225/// assert_eq!(cst.kind, CstKind::Document);
226///
227/// let quoted = cst
228///     .tokens()
229///     .into_iter()
230///     .find(|token| token.quote.is_some())
231///     .unwrap();
232/// assert_eq!(quoted.text(), "'my app'");
233/// assert_eq!(quoted.value.as_deref(), Some("my app"));
234///
235/// assert!(parse_lino_to_cst("(a").is_err());
236/// ```
237pub fn parse_lino_to_cst(document: &str) -> Result<CstNode, ParseError> {
238    parse_lino_to_cst_with_config(document, &ParserConfig::default())
239}
240
241/// Reads a document the way `config` says to as a lossless concrete syntax
242/// tree; see [`parse_lino_to_cst`].
243///
244/// Without comments, a `#` is read as part of a reference, as the parser reads
245/// it.
246///
247/// # Examples
248/// ```
249/// use links_notation::{CstKind, ParserConfig, parse_lino_to_cst_with_config};
250///
251/// let cst = parse_lino_to_cst_with_config("a #b", &ParserConfig::without_comments()).unwrap();
252/// let kinds: Vec<CstKind> = cst.tokens().iter().map(|token| token.kind).collect();
253/// assert_eq!(kinds, [CstKind::Reference, CstKind::Whitespace, CstKind::Reference]);
254/// assert_eq!(cst.to_string(), "a #b");
255/// ```
256pub fn parse_lino_to_cst_with_config(
257    document: &str,
258    config: &ParserConfig,
259) -> Result<CstNode, ParseError> {
260    let prepared = check_document(document, config)?;
261    build_cst(document, &prepared, config.comments)
262}
263
264/// A container being filled: the document or a group, with its line that is
265/// still open.
266struct Container {
267    kind: CstKind,
268    children: Vec<CstNode>,
269    line: Option<Vec<CstNode>>,
270}
271
272impl Container {
273    fn new(kind: CstKind, children: Vec<CstNode>) -> Self {
274        Container {
275            kind,
276            children,
277            line: None,
278        }
279    }
280
281    /// The line being filled, started when there is none.
282    fn line(&mut self) -> &mut Vec<CstNode> {
283        self.line.get_or_insert_with(Vec::new)
284    }
285
286    /// Adds the open line, if any, to the children.
287    fn close_line(&mut self) {
288        if let Some(line) = self.line.take() {
289            self.children.push(CstNode::node(CstKind::Line, line));
290        }
291    }
292
293    fn finish(mut self) -> CstNode {
294        self.close_line();
295        CstNode::node(self.kind, self.children)
296    }
297}
298
299/// The bytes that end a bare reference.
300const BARE_REFERENCE_END: &[u8] = b" \t\r\n(:)";
301
302/// The concrete syntax tree of `document`, which has already been parsed.
303/// `prepared` is the document the parser read: the same bytes, with its
304/// comments blanked when `comments` is on.
305fn build_cst(document: &str, prepared: &str, comments: bool) -> Result<CstNode, ParseError> {
306    // Blanking a comment keeps every byte where it was, so a position in one is
307    // the same position in the other.
308    debug_assert_eq!(document.len(), prepared.len());
309    let input = document.as_bytes();
310    let blanked = prepared.as_bytes();
311    let references = DelimitedReferences::new(prepared);
312    // The open containers, innermost last. A group joins the line it opens on
313    // when it closes; that line stays open until then, since every line break
314    // in between belongs to the group.
315    let mut containers = vec![Container::new(CstKind::Document, Vec::new())];
316    let mut position = 0;
317
318    while position < input.len() {
319        let byte = input[position];
320        let mut end = position + 1;
321        let innermost = containers.len() - 1;
322
323        if comments && byte == COMMENT as u8 && blanked[position] != byte {
324            // A blanked `#` opens a comment, which runs to the line ending.
325            while end < input.len() && input[end] != b'\n' && input[end] != b'\r' {
326                end += 1;
327            }
328            let token = CstNode::token(CstKind::Comment, &document[position..end]);
329            containers[innermost].line().push(token);
330        } else if byte == b'\r' || byte == b'\n' {
331            if byte == b'\r' && input.get(end) == Some(&b'\n') {
332                end += 1;
333            }
334            let token = CstNode::token(CstKind::Newline, &document[position..end]);
335            containers[innermost].line().push(token);
336            containers[innermost].close_line();
337        } else if byte == b' ' || byte == b'\t' {
338            while end < input.len() && (input[end] == b' ' || input[end] == b'\t') {
339                end += 1;
340            }
341            let token = CstNode::token(CstKind::Whitespace, &document[position..end]);
342            containers[innermost].line().push(token);
343        } else if byte == b'(' {
344            // Open the line the group belongs to before the group is filled.
345            containers[innermost].line();
346            let open = CstNode::token(CstKind::OpenParen, &document[position..end]);
347            containers.push(Container::new(CstKind::Group, vec![open]));
348        } else if byte == b')' {
349            if containers.len() == 1 {
350                return Err(ParseError::InternalError(format!(
351                    "Unbalanced \")\" at offset {position}"
352                )));
353            }
354            let mut group = containers.pop().expect("a group is open");
355            group.close_line();
356            group.children.push(CstNode::token(
357                CstKind::CloseParen,
358                &document[position..end],
359            ));
360            let group = CstNode::node(CstKind::Group, group.children);
361            containers
362                .last_mut()
363                .expect("the document is open")
364                .line()
365                .push(group);
366        } else if byte == b':' {
367            let token = CstNode::token(CstKind::Colon, &document[position..end]);
368            containers[innermost].line().push(token);
369        } else {
370            let (reference_end, value, quote) =
371                read_reference(document, prepared, position, &references);
372            end = reference_end;
373            let mut token = CstNode::token(CstKind::Reference, &document[position..end]);
374            token.value = Some(value);
375            token.quote = quote;
376            containers[innermost].line().push(token);
377        }
378        position = end;
379    }
380
381    if containers.len() != 1 {
382        return Err(ParseError::InternalError(
383            "Unbalanced \"(\" at the end of the document".to_string(),
384        ));
385    }
386    Ok(containers.pop().expect("the document is open").finish())
387}
388
389/// The reference that starts at `start`, read as the parser reads it: where it
390/// ends, what it means, and the delimiter it was written between.
391fn read_reference(
392    document: &str,
393    prepared: &str,
394    start: usize,
395    references: &DelimitedReferences,
396) -> (usize, String, Option<char>) {
397    let blanked = prepared.as_bytes();
398    if let Some(end) = literal_end(prepared, start) {
399        let text = &document[start..end];
400        let value = decode_reference_literal(text).unwrap_or_else(|_| text.to_owned());
401        return (end, value, None);
402    }
403    let first = blanked[start];
404    if QUOTES.contains(&first) {
405        if let Some(reading) = references.read(&prepared[start..]) {
406            return (start + reading.length, reading.value, Some(first as char));
407        }
408    }
409    let mut end = start + 1;
410    while end < blanked.len() && !BARE_REFERENCE_END.contains(&blanked[end]) {
411        end += 1;
412    }
413    (end, document[start..end].to_owned(), None)
414}
415
416/// The end of the versioned literal `~<digits>{<anything but } space tab line
417/// ending or parenthesis>}` that starts at `start`, if one does.
418fn literal_end(prepared: &str, start: usize) -> Option<usize> {
419    let rest = &prepared[start..];
420    let body = prefix_end(rest)?;
421    let bytes = rest.as_bytes();
422    let mut end = body;
423    while end < bytes.len() && !b"} \t\r\n()".contains(&bytes[end]) {
424        end += 1;
425    }
426    (bytes.get(end) == Some(&b'}')).then_some(start + end + 1)
427}