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, reference_literal_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 // Bare references end where they end in the document the parser read.
312 // Delimited references and literals are read, as the parser reads them, in
313 // the document as written, so a comment never changes what a reference
314 // holds (issue #377).
315 let references = DelimitedReferences::new(document);
316 // The open containers, innermost last. A group joins the line it opens on
317 // when it closes; that line stays open until then, since every line break
318 // in between belongs to the group.
319 let mut containers = vec![Container::new(CstKind::Document, Vec::new())];
320 let mut position = 0;
321
322 while position < input.len() {
323 let byte = input[position];
324 let mut end = position + 1;
325 let innermost = containers.len() - 1;
326
327 if comments && byte == COMMENT as u8 && blanked[position] != byte {
328 // A blanked `#` opens a comment, which runs to the line ending.
329 while end < input.len() && input[end] != b'\n' && input[end] != b'\r' {
330 end += 1;
331 }
332 let token = CstNode::token(CstKind::Comment, &document[position..end]);
333 containers[innermost].line().push(token);
334 } else if byte == b'\r' || byte == b'\n' {
335 if byte == b'\r' && input.get(end) == Some(&b'\n') {
336 end += 1;
337 }
338 let token = CstNode::token(CstKind::Newline, &document[position..end]);
339 containers[innermost].line().push(token);
340 containers[innermost].close_line();
341 } else if byte == b' ' || byte == b'\t' {
342 while end < input.len() && (input[end] == b' ' || input[end] == b'\t') {
343 end += 1;
344 }
345 let token = CstNode::token(CstKind::Whitespace, &document[position..end]);
346 containers[innermost].line().push(token);
347 } else if byte == b'(' {
348 // Open the line the group belongs to before the group is filled.
349 containers[innermost].line();
350 let open = CstNode::token(CstKind::OpenParen, &document[position..end]);
351 containers.push(Container::new(CstKind::Group, vec![open]));
352 } else if byte == b')' {
353 if containers.len() == 1 {
354 return Err(ParseError::InternalError(format!(
355 "Unbalanced \")\" at offset {position}"
356 )));
357 }
358 let mut group = containers.pop().expect("a group is open");
359 group.close_line();
360 group.children.push(CstNode::token(
361 CstKind::CloseParen,
362 &document[position..end],
363 ));
364 let group = CstNode::node(CstKind::Group, group.children);
365 containers
366 .last_mut()
367 .expect("the document is open")
368 .line()
369 .push(group);
370 } else if byte == b':' {
371 let token = CstNode::token(CstKind::Colon, &document[position..end]);
372 containers[innermost].line().push(token);
373 } else {
374 let (reference_end, value, quote) =
375 read_reference(document, prepared, position, &references);
376 end = reference_end;
377 let mut token = CstNode::token(CstKind::Reference, &document[position..end]);
378 token.value = Some(value);
379 token.quote = quote;
380 containers[innermost].line().push(token);
381 }
382 position = end;
383 }
384
385 if containers.len() != 1 {
386 return Err(ParseError::InternalError(
387 "Unbalanced \"(\" at the end of the document".to_string(),
388 ));
389 }
390 Ok(containers.pop().expect("the document is open").finish())
391}
392
393/// The reference that starts at `start`, read as the parser reads it: where it
394/// ends, what it means, and the delimiter it was written between.
395fn read_reference(
396 document: &str,
397 prepared: &str,
398 start: usize,
399 references: &DelimitedReferences,
400) -> (usize, String, Option<char>) {
401 let blanked = prepared.as_bytes();
402 if let Some(end) = reference_literal_end(document, start) {
403 let text = &document[start..end];
404 let value = decode_reference_literal(text).unwrap_or_else(|_| text.to_owned());
405 return (end, value, None);
406 }
407 let first = blanked[start];
408 if QUOTES.contains(&first) {
409 if let Some(reading) = references.read(&document[start..]) {
410 return (start + reading.length, reading.value, Some(first as char));
411 }
412 }
413 let mut end = start + 1;
414 while end < blanked.len() && !BARE_REFERENCE_END.contains(&blanked[end]) {
415 end += 1;
416 }
417 (end, document[start..end].to_owned(), None)
418}