Skip to main content

links_notation/
parser.rs

1use crate::quotes::{read_reference, DelimitedReferences, Reading};
2use nom::{
3    branch::alt,
4    bytes::complete::{tag, take_while, take_while1},
5    character::complete::{char, line_ending},
6    combinator::eof,
7    multi::{many0, many1},
8    sequence::{preceded, terminated},
9    IResult, Parser,
10};
11use std::cell::{OnceCell, RefCell};
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct Link {
15    pub id: Option<String>,
16    pub values: Vec<Link>,
17    pub children: Vec<Link>,
18    pub is_indented_id: bool,
19    /// Body of a parenthesized group, kept unflattened until the whole document
20    /// is transformed. `None` for every link that is not a parenthesized group.
21    pub nested: Option<Vec<Link>>,
22}
23
24impl Link {
25    pub fn new_singlet(id: String) -> Self {
26        Link {
27            id: Some(id),
28            values: vec![],
29            children: vec![],
30            is_indented_id: false,
31            nested: None,
32        }
33    }
34
35    pub fn new_indented_id(id: String) -> Self {
36        Link {
37            id: Some(id),
38            values: vec![],
39            children: vec![],
40            is_indented_id: true,
41            nested: None,
42        }
43    }
44
45    pub fn new_value(values: Vec<Link>) -> Self {
46        Link {
47            id: None,
48            values,
49            children: vec![],
50            is_indented_id: false,
51            nested: None,
52        }
53    }
54
55    pub fn new_link(id: Option<String>, values: Vec<Link>) -> Self {
56        Link {
57            id,
58            values,
59            children: vec![],
60            is_indented_id: false,
61            nested: None,
62        }
63    }
64
65    /// Creates a link that stands for a parenthesized group, keeping the links
66    /// parsed inside the parentheses as they were written.
67    pub fn new_nested(body: Vec<Link>) -> Self {
68        Link {
69            id: None,
70            values: vec![],
71            children: vec![],
72            is_indented_id: false,
73            nested: Some(body),
74        }
75    }
76
77    pub fn with_children(mut self, children: Vec<Link>) -> Self {
78        self.children = children;
79        self
80    }
81}
82
83pub struct ParserState {
84    indentation_stack: RefCell<Vec<usize>>,
85    base_indentation: RefCell<Option<usize>>,
86    nested_depth: RefCell<usize>,
87    furthest: RefCell<FurthestFailure>,
88    /// The delimited references of the document being parsed.
89    references: OnceCell<DelimitedReferences>,
90}
91
92/// The furthest position any alternative reached before failing, and what could
93/// have continued the document there.
94///
95/// The parser backtracks, so the position the last alternative happens to fail
96/// at says little about where the document stops making sense: a defect in the
97/// middle of line two is reported by `nom` as "expected end of input" at the
98/// start of line two, because that is where the document last parsed cleanly.
99/// The furthest position reached is what a PEG parser points at, and it is what
100/// the JavaScript port reports.
101#[derive(Debug, Clone, Default)]
102struct FurthestFailure {
103    /// Address of the furthest failing position, as a pointer into the document
104    /// being parsed. Turned into an offset once the document is at hand again.
105    address: Option<usize>,
106    expected: Vec<&'static str>,
107}
108
109/// Where the parser stopped, and what it could have accepted there.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ParseFailure {
112    /// Byte offset into the document the parser stopped at.
113    pub offset: usize,
114    /// What could have continued the document at `offset`, in the wording used
115    /// by the error message. Empty when the failure came from a place that
116    /// names no expectation.
117    pub expected: Vec<&'static str>,
118    /// The `nom` error kind. An internal detail of this parser: it says which
119    /// combinator gave up, not what is wrong with the document, so it is kept
120    /// out of the error message and reachable only through `Debug`.
121    pub kind: Option<nom::error::ErrorKind>,
122}
123
124/// Indentation state of the context a parenthesized group was opened in.
125pub struct SavedContext {
126    indentation_stack: Vec<usize>,
127    base_indentation: Option<usize>,
128}
129
130impl Default for ParserState {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl ParserState {
137    pub fn new() -> Self {
138        ParserState {
139            indentation_stack: RefCell::new(vec![0]),
140            base_indentation: RefCell::new(None),
141            nested_depth: RefCell::new(0),
142            furthest: RefCell::new(FurthestFailure::default()),
143            references: OnceCell::new(),
144        }
145    }
146
147    pub fn set_base_indentation(&self, indent: usize) {
148        let mut base = self.base_indentation.borrow_mut();
149        if base.is_none() {
150            *base = Some(indent);
151        }
152    }
153
154    pub fn get_base_indentation(&self) -> usize {
155        self.base_indentation.borrow().unwrap_or(0)
156    }
157
158    pub fn normalize_indentation(&self, indent: usize) -> usize {
159        let base = self.get_base_indentation();
160        indent.saturating_sub(base)
161    }
162
163    pub fn push_indentation(&self, indent: usize) {
164        self.indentation_stack.borrow_mut().push(indent);
165    }
166
167    pub fn pop_indentation(&self) {
168        let mut stack = self.indentation_stack.borrow_mut();
169        if stack.len() > 1 {
170            stack.pop();
171        }
172    }
173
174    pub fn current_indentation(&self) -> usize {
175        *self.indentation_stack.borrow().last().unwrap_or(&0)
176    }
177
178    pub fn check_indentation(&self, indent: usize) -> bool {
179        indent >= self.current_indentation()
180    }
181
182    /// Opens a nested context: the group body starts fresh at indentation level
183    /// zero and follows the same rules as the root document.
184    pub fn enter_nested_context(&self) -> SavedContext {
185        let saved = SavedContext {
186            indentation_stack: self.indentation_stack.replace(vec![0]),
187            base_indentation: self.base_indentation.replace(None),
188        };
189        *self.nested_depth.borrow_mut() += 1;
190        saved
191    }
192
193    /// Restores the context the parenthesized group was opened in.
194    pub fn exit_nested_context(&self, saved: SavedContext) {
195        *self.indentation_stack.borrow_mut() = saved.indentation_stack;
196        *self.base_indentation.borrow_mut() = saved.base_indentation;
197        let mut depth = self.nested_depth.borrow_mut();
198        if *depth > 0 {
199            *depth -= 1;
200        }
201    }
202
203    pub fn is_inside_nested_context(&self) -> bool {
204        *self.nested_depth.borrow() > 0
205    }
206
207    /// Records that `what` could have continued the document at `at`, and that
208    /// nothing there did. Only the furthest such position is kept; every
209    /// expectation recorded at that same position is kept alongside it.
210    fn expected_at(&self, at: &str, what: &'static str) {
211        let address = at.as_ptr() as usize;
212        let mut furthest = self.furthest.borrow_mut();
213        match furthest.address {
214            Some(recorded) if recorded > address => {}
215            Some(recorded) if recorded == address => {
216                if !furthest.expected.contains(&what) {
217                    furthest.expected.push(what);
218                }
219            }
220            _ => {
221                furthest.address = Some(address);
222                furthest.expected = vec![what];
223            }
224        }
225    }
226
227    /// Turns everything recorded during a failed parse into a position in
228    /// `document`.
229    ///
230    /// `nom`'s own error position is the fallback and the floor: the parser
231    /// reached at least that far, whatever the tracked alternatives say.
232    fn failure(&self, document: &str, error: &nom::Err<nom::error::Error<&str>>) -> ParseFailure {
233        let base = document.as_ptr() as usize;
234        let (nom_offset, kind) = match error {
235            nom::Err::Error(e) | nom::Err::Failure(e) => (
236                (e.input.as_ptr() as usize).saturating_sub(base),
237                Some(e.code),
238            ),
239            nom::Err::Incomplete(_) => (document.len(), None),
240        };
241        let furthest = self.furthest.borrow();
242        let tracked = furthest
243            .address
244            .map(|address| address.saturating_sub(base))
245            .unwrap_or(0);
246        let offset = tracked.max(nom_offset).min(document.len());
247        let expected = if tracked == offset {
248            furthest.expected.clone()
249        } else {
250            // The tracked expectations belong to an earlier position, so they
251            // do not describe the place being reported.
252            Vec::new()
253        };
254        ParseFailure {
255            offset,
256            expected,
257            kind,
258        }
259    }
260}
261
262/// Fails the way `nom` does, after recording what was expected at `input`.
263fn expected<'a, T>(
264    input: &'a str,
265    state: &ParserState,
266    what: &'static str,
267    kind: nom::error::ErrorKind,
268) -> IResult<&'a str, T> {
269    state.expected_at(input, what);
270    Err(nom::Err::Error(nom::error::Error::new(input, kind)))
271}
272
273pub(crate) fn is_whitespace_char(c: char) -> bool {
274    c == ' ' || c == '\t' || c == '\n' || c == '\r'
275}
276
277fn is_horizontal_whitespace(c: char) -> bool {
278    c == ' ' || c == '\t'
279}
280
281fn is_reference_char(c: char) -> bool {
282    !is_whitespace_char(c) && c != '(' && c != ':' && c != ')'
283}
284
285fn horizontal_whitespace(input: &str) -> IResult<&str, &str> {
286    take_while(is_horizontal_whitespace)(input)
287}
288
289fn whitespace(input: &str) -> IResult<&str, &str> {
290    take_while(is_whitespace_char)(input)
291}
292
293fn simple_reference(input: &str) -> IResult<&str, String> {
294    take_while1(is_reference_char)
295        .map(|s: &str| s.to_string())
296        .parse(input)
297}
298
299/// The offset just past the delimited reference that starts at `start`, or
300/// `None` when nothing that far into `document` opens one.
301///
302/// Comment stripping needs to know how far a delimited reference reaches so
303/// that a `#` written inside one stays content, and it has to agree with the
304/// parser about it, which is why both read references the same way.
305pub fn quoted_reference_end(document: &str, start: usize) -> Option<usize> {
306    let reading = read_reference(document.get(start..)?)?;
307    Some(start + reading.length)
308}
309
310/// A delimited reference with any number of delimiters. A run of an even
311/// number of delimiters that does not open a reference with a substantive body
312/// is the empty reference.
313fn delimited_reference<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, String> {
314    let references = state
315        .references
316        .get_or_init(|| DelimitedReferences::new(input));
317    match references.read(input) {
318        Some(Reading { value, length }) => Ok((&input[length..], value)),
319        None => Err(nom::Err::Error(nom::error::Error::new(
320            input,
321            nom::error::ErrorKind::Tag,
322        ))),
323    }
324}
325
326fn reference<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, String> {
327    // Try quoted strings with dynamic quote detection (supports any N quotes)
328    // Then fall back to simple unquoted reference
329    let parsed = alt((|i| delimited_reference(i, state), simple_reference)).parse(input);
330    if parsed.is_err() {
331        state.expected_at(input, "a reference");
332    }
333    parsed
334}
335
336fn eol<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, &'a str> {
337    let parsed = alt((
338        preceded(horizontal_whitespace, alt((line_ending, tag("\r")))),
339        preceded(horizontal_whitespace, eof),
340        |i| nested_group_end(i, state),
341    ))
342    .parse(input);
343    if parsed.is_err() {
344        state.expected_at(input, "end of line");
345    }
346    parsed
347}
348
349/// Inside a parenthesized group the closing parenthesis ends the last line,
350/// just like a line break does at the root.
351fn nested_group_end<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, &'a str> {
352    if !state.is_inside_nested_context() {
353        return Err(nom::Err::Error(nom::error::Error::new(
354            input,
355            nom::error::ErrorKind::Verify,
356        )));
357    }
358    let (rest, _) = horizontal_whitespace(input)?;
359    if rest.starts_with(')') {
360        Ok((rest, ""))
361    } else {
362        expected(rest, state, "\")\"", nom::error::ErrorKind::Char)
363    }
364}
365
366/// Skips the line breaks and blank lines that separate `(` from the first line
367/// of the group body.
368fn skip_empty_lines(input: &str) -> &str {
369    let mut rest = input;
370    loop {
371        let line_start = rest.trim_start_matches(is_horizontal_whitespace);
372        match strip_line_ending(line_start) {
373            Some(next) => rest = next,
374            None => return rest,
375        }
376    }
377}
378
379fn strip_line_ending(input: &str) -> Option<&str> {
380    input
381        .strip_prefix("\r\n")
382        .or_else(|| input.strip_prefix('\n'))
383        .or_else(|| input.strip_prefix('\r'))
384}
385
386fn reference_or_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
387    alt((
388        |i| nested_group(i, state),
389        (|i| reference(i, state)).map(Link::new_singlet),
390    ))
391    .parse(input)
392}
393
394fn single_line_value_and_whitespace<'a>(
395    input: &'a str,
396    state: &ParserState,
397) -> IResult<&'a str, Link> {
398    preceded(horizontal_whitespace, |i| reference_or_link(i, state)).parse(input)
399}
400
401fn single_line_values<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Vec<Link>> {
402    many1(|i| single_line_value_and_whitespace(i, state)).parse(input)
403}
404
405fn single_line_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
406    let (input, _) = horizontal_whitespace(input)?;
407    let (input, id) = reference(input, state)?;
408    let (input, _) = horizontal_whitespace(input)?;
409    let (input, _) = colon(input, state)?;
410    let (input, values) = single_line_values(input, state)?;
411    Ok((input, Link::new_link(Some(id), values)))
412}
413
414/// The colon that separates an identifier from its values.
415fn colon<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, char> {
416    character(':', input, state, "\":\"")
417}
418
419/// Matches one character, recording what was expected when it is not there.
420fn character<'a>(
421    wanted: char,
422    input: &'a str,
423    state: &ParserState,
424    what: &'static str,
425) -> IResult<&'a str, char> {
426    let parsed: IResult<&'a str, char> = char(wanted).parse(input);
427    match parsed {
428        Ok(parsed) => Ok(parsed),
429        Err(_) => expected(input, state, what, nom::error::ErrorKind::Char),
430    }
431}
432
433fn single_line_value_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
434    (|i| single_line_values(i, state))
435        .map(Link::new_value)
436        .parse(input)
437}
438
439fn indented_id_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
440    let (input, id) = reference(input, state)?;
441    let (input, _) = horizontal_whitespace(input)?;
442    let (input, _) = colon(input, state)?;
443    let (input, _) = eol(input, state)?;
444    Ok((input, Link::new_indented_id(id)))
445}
446
447/// A parenthesized group opens a nested context: its body starts fresh at
448/// indentation level zero and is parsed with the same rules as the root
449/// document, so indentation is structural inside parentheses as well.
450fn nested_group<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
451    let (body_input, _) = character('(', input, state, "\"(\"")?;
452    let saved = state.enter_nested_context();
453    let result = nested_group_body(body_input, state);
454    state.exit_nested_context(saved);
455    result
456}
457
458fn nested_group_body<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
459    if let Ok((rest, body)) = links(skip_empty_lines(input), state) {
460        let (rest, _) = whitespace(rest)?;
461        let (rest, _) = closing_parenthesis(rest, state)?;
462        return Ok((rest, Link::new_nested(body)));
463    }
464    let (rest, _) = whitespace(input)?;
465    let (rest, _) = closing_parenthesis(rest, state)?;
466    Ok((rest, Link::new_nested(vec![])))
467}
468
469/// The parenthesis that closes a group.
470fn closing_parenthesis<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, char> {
471    character(')', input, state, "\")\"")
472}
473
474fn single_line_any_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
475    alt((
476        terminated(|i| single_line_link(i, state), |i| eol(i, state)),
477        terminated(|i| single_line_value_link(i, state), |i| eol(i, state)),
478    ))
479    .parse(input)
480}
481
482fn any_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
483    alt((
484        terminated(|i| nested_group(i, state), |i| eol(i, state)),
485        |i| indented_id_link(i, state),
486        |i| single_line_any_link(i, state),
487    ))
488    .parse(input)
489}
490
491fn count_indentation(input: &str) -> IResult<&str, usize> {
492    take_while(|c| c == ' ').map(|s: &str| s.len()).parse(input)
493}
494
495fn push_indentation<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, ()> {
496    let (input, spaces) = count_indentation(skip_empty_lines(input))?;
497    let normalized_spaces = state.normalize_indentation(spaces);
498    let current = state.current_indentation();
499
500    if normalized_spaces > current {
501        state.push_indentation(normalized_spaces);
502        Ok((input, ()))
503    } else {
504        Err(nom::Err::Error(nom::error::Error::new(
505            input,
506            nom::error::ErrorKind::Verify,
507        )))
508    }
509}
510
511fn check_indentation<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, ()> {
512    let (input, spaces) = count_indentation(input)?;
513    let normalized_spaces = state.normalize_indentation(spaces);
514
515    if state.check_indentation(normalized_spaces) {
516        Ok((input, ()))
517    } else {
518        Err(nom::Err::Error(nom::error::Error::new(
519            input,
520            nom::error::ErrorKind::Verify,
521        )))
522    }
523}
524
525fn element<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
526    let (input, link) = any_link(input, state)?;
527
528    let indentation = state.indentation_stack.borrow().clone();
529    if let Ok((child_input, _)) = push_indentation(input, state) {
530        if let Ok((rest, children)) = links(child_input, state) {
531            return Ok((rest, link.with_children(children)));
532        }
533        // No child line followed the indentation. Backtrack to the link so
534        // the document can consume the remaining spaces as whitespace.
535        state.indentation_stack.replace(indentation);
536    }
537    Ok((input, link))
538}
539
540fn first_line<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
541    // Set base indentation from the first line and consume it, so that the first
542    // line is parsed exactly like every following line.
543    let (input, spaces) = count_indentation(input)?;
544    state.set_base_indentation(spaces);
545    element(input, state)
546}
547
548fn line<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
549    // Blank lines do not break a document, they are simply skipped
550    preceded(|i| check_indentation(i, state), |i| element(i, state)).parse(skip_empty_lines(input))
551}
552
553fn links<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Vec<Link>> {
554    let (input, first) = first_line(input, state)?;
555    let (input, rest) = many0(|i| line(i, state)).parse(input)?;
556
557    state.pop_indentation();
558
559    let mut result = vec![first];
560    result.extend(rest);
561    Ok((input, result))
562}
563
564pub fn parse_document(input: &str) -> IResult<&str, Vec<Link>> {
565    let state = ParserState::new();
566    document(input, &state)
567}
568
569/// Parses a document and, when it does not parse, says where it stopped.
570///
571/// `parse_document` reports a failure the way `nom` does: with the whole
572/// unconsumed remainder of the input and the combinator that gave up. Neither
573/// tells a reader which line to look at, and the remainder grows with the size
574/// of the document. This is the entry point the library uses.
575pub fn parse_document_with_diagnostics(input: &str) -> Result<Vec<Link>, ParseFailure> {
576    let state = ParserState::new();
577    match document(input, &state) {
578        Ok((_, links)) => Ok(links),
579        Err(error) => Err(state.failure(input, &error)),
580    }
581}
582
583fn document<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Vec<Link>> {
584    // Every reference is read from a part of this document, so the runs of
585    // delimiters are listed from all of it.
586    state
587        .references
588        .get_or_init(|| DelimitedReferences::new(input));
589
590    // Skip leading blank lines but preserve the line structure
591    let document = skip_empty_lines(input);
592
593    // Handle empty or whitespace-only documents
594    if document.trim_matches(is_whitespace_char).is_empty() {
595        return Ok(("", vec![]));
596    }
597
598    let (rest, result) = links(document, state)?;
599    let (rest, _) = whitespace(rest)?;
600    let end: IResult<&'a str, &'a str> = eof(rest);
601    let (rest, _) = match end {
602        Ok(parsed) => parsed,
603        Err(_) => return expected(rest, state, "end of input", nom::error::ErrorKind::Eof),
604    };
605
606    Ok((rest, result))
607}