Skip to main content

links_notation/
parser.rs

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