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}
87
88/// Indentation state of the context a parenthesized group was opened in.
89pub struct SavedContext {
90    indentation_stack: Vec<usize>,
91    base_indentation: Option<usize>,
92}
93
94impl Default for ParserState {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl ParserState {
101    pub fn new() -> Self {
102        ParserState {
103            indentation_stack: RefCell::new(vec![0]),
104            base_indentation: RefCell::new(None),
105            nested_depth: RefCell::new(0),
106        }
107    }
108
109    pub fn set_base_indentation(&self, indent: usize) {
110        let mut base = self.base_indentation.borrow_mut();
111        if base.is_none() {
112            *base = Some(indent);
113        }
114    }
115
116    pub fn get_base_indentation(&self) -> usize {
117        self.base_indentation.borrow().unwrap_or(0)
118    }
119
120    pub fn normalize_indentation(&self, indent: usize) -> usize {
121        let base = self.get_base_indentation();
122        indent.saturating_sub(base)
123    }
124
125    pub fn push_indentation(&self, indent: usize) {
126        self.indentation_stack.borrow_mut().push(indent);
127    }
128
129    pub fn pop_indentation(&self) {
130        let mut stack = self.indentation_stack.borrow_mut();
131        if stack.len() > 1 {
132            stack.pop();
133        }
134    }
135
136    pub fn current_indentation(&self) -> usize {
137        *self.indentation_stack.borrow().last().unwrap_or(&0)
138    }
139
140    pub fn check_indentation(&self, indent: usize) -> bool {
141        indent >= self.current_indentation()
142    }
143
144    /// Opens a nested context: the group body starts fresh at indentation level
145    /// zero and follows the same rules as the root document.
146    pub fn enter_nested_context(&self) -> SavedContext {
147        let saved = SavedContext {
148            indentation_stack: self.indentation_stack.replace(vec![0]),
149            base_indentation: self.base_indentation.replace(None),
150        };
151        *self.nested_depth.borrow_mut() += 1;
152        saved
153    }
154
155    /// Restores the context the parenthesized group was opened in.
156    pub fn exit_nested_context(&self, saved: SavedContext) {
157        *self.indentation_stack.borrow_mut() = saved.indentation_stack;
158        *self.base_indentation.borrow_mut() = saved.base_indentation;
159        let mut depth = self.nested_depth.borrow_mut();
160        if *depth > 0 {
161            *depth -= 1;
162        }
163    }
164
165    pub fn is_inside_nested_context(&self) -> bool {
166        *self.nested_depth.borrow() > 0
167    }
168}
169
170fn is_whitespace_char(c: char) -> bool {
171    c == ' ' || c == '\t' || c == '\n' || c == '\r'
172}
173
174fn is_horizontal_whitespace(c: char) -> bool {
175    c == ' ' || c == '\t'
176}
177
178fn is_reference_char(c: char) -> bool {
179    !is_whitespace_char(c) && c != '(' && c != ':' && c != ')'
180}
181
182fn horizontal_whitespace(input: &str) -> IResult<&str, &str> {
183    take_while(is_horizontal_whitespace)(input)
184}
185
186fn whitespace(input: &str) -> IResult<&str, &str> {
187    take_while(is_whitespace_char)(input)
188}
189
190fn simple_reference(input: &str) -> IResult<&str, String> {
191    take_while1(is_reference_char)
192        .map(|s: &str| s.to_string())
193        .parse(input)
194}
195
196/// Parse a multi-quote string with a given quote character and count.
197/// For N quotes: opening = N quotes, closing = N quotes, escape = 2*N quotes -> N quotes
198fn parse_multi_quote_string(
199    input: &str,
200    quote_char: char,
201    quote_count: usize,
202) -> IResult<&str, String> {
203    let open_close = quote_char.to_string().repeat(quote_count);
204    let escape_seq = quote_char.to_string().repeat(quote_count * 2);
205    let escape_val = quote_char.to_string().repeat(quote_count);
206
207    // Check for opening quotes
208    if !input.starts_with(&open_close) {
209        return Err(nom::Err::Error(nom::error::Error::new(
210            input,
211            nom::error::ErrorKind::Tag,
212        )));
213    }
214
215    let mut remaining = &input[open_close.len()..];
216    let mut content = String::new();
217
218    loop {
219        if remaining.is_empty() {
220            return Err(nom::Err::Error(nom::error::Error::new(
221                input,
222                nom::error::ErrorKind::Tag,
223            )));
224        }
225
226        // Check for escape sequence (2*N quotes)
227        if remaining.starts_with(&escape_seq) {
228            content.push_str(&escape_val);
229            remaining = &remaining[escape_seq.len()..];
230            continue;
231        }
232
233        // Check for closing quotes (N quotes not followed by more quotes)
234        if remaining.starts_with(&open_close) {
235            let after_close = &remaining[open_close.len()..];
236            // Make sure this is exactly N quotes (not more)
237            if after_close.is_empty() || !after_close.starts_with(quote_char) {
238                return Ok((after_close, content));
239            }
240        }
241
242        // Take the next character
243        let c = remaining.chars().next().unwrap();
244        content.push(c);
245        remaining = &remaining[c.len_utf8()..];
246    }
247}
248
249/// A body written between an even run of delimiters is substantive when it
250/// holds at least one visible character and does not straddle a parenthesis.
251/// An even run can always be read as delimiter pairs enclosing nothing, so the
252/// n-quote reading is only taken when it carries something the pairs cannot.
253fn is_substantive_body(content: &str) -> bool {
254    let mut depth: isize = 0;
255    let mut has_visible = false;
256
257    for c in content.chars() {
258        match c {
259            '(' => depth += 1,
260            ')' => {
261                depth -= 1;
262                if depth < 0 {
263                    return false;
264                }
265            }
266            _ => {}
267        }
268        if !c.is_whitespace() {
269            has_visible = true;
270        }
271    }
272
273    has_visible && depth == 0
274}
275
276/// Parse a quoted string with dynamically detected quote count.
277///
278/// Counts opening quotes and uses that count for parsing. A run of an even
279/// number of delimiters that does not open a reference with a substantive body
280/// is the empty reference: the shortest reading, a bare delimiter pair
281/// enclosing nothing, wins over a longer n-quote delimiter.
282fn parse_dynamic_quote_string(input: &str, quote_char: char) -> IResult<&str, String> {
283    // Count opening quotes
284    let quote_count = input.chars().take_while(|&c| c == quote_char).count();
285
286    if quote_count == 0 {
287        return Err(nom::Err::Error(nom::error::Error::new(
288            input,
289            nom::error::ErrorKind::Tag,
290        )));
291    }
292
293    let is_even_run = quote_count % 2 == 0;
294
295    if let Ok((rest, content)) = parse_multi_quote_string(input, quote_char, quote_count) {
296        if !is_even_run || is_substantive_body(&content) {
297            return Ok((rest, content));
298        }
299    }
300
301    if is_even_run {
302        return Ok((&input[quote_count * quote_char.len_utf8()..], String::new()));
303    }
304
305    Err(nom::Err::Error(nom::error::Error::new(
306        input,
307        nom::error::ErrorKind::Tag,
308    )))
309}
310
311fn double_quoted_dynamic(input: &str) -> IResult<&str, String> {
312    parse_dynamic_quote_string(input, '"')
313}
314
315fn single_quoted_dynamic(input: &str) -> IResult<&str, String> {
316    parse_dynamic_quote_string(input, '\'')
317}
318
319fn backtick_quoted_dynamic(input: &str) -> IResult<&str, String> {
320    parse_dynamic_quote_string(input, '`')
321}
322
323fn reference(input: &str) -> IResult<&str, String> {
324    // Try quoted strings with dynamic quote detection (supports any N quotes)
325    // Then fall back to simple unquoted reference
326    alt((
327        double_quoted_dynamic,
328        single_quoted_dynamic,
329        backtick_quoted_dynamic,
330        simple_reference,
331    ))
332    .parse(input)
333}
334
335fn eol<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, &'a str> {
336    alt((
337        preceded(horizontal_whitespace, line_ending),
338        preceded(horizontal_whitespace, eof),
339        |i| nested_group_end(i, state),
340    ))
341    .parse(input)
342}
343
344/// Inside a parenthesized group the closing parenthesis ends the last line,
345/// just like a line break does at the root.
346fn nested_group_end<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, &'a str> {
347    if !state.is_inside_nested_context() {
348        return Err(nom::Err::Error(nom::error::Error::new(
349            input,
350            nom::error::ErrorKind::Verify,
351        )));
352    }
353    let (rest, _) = horizontal_whitespace(input)?;
354    if rest.starts_with(')') {
355        Ok((rest, ""))
356    } else {
357        Err(nom::Err::Error(nom::error::Error::new(
358            input,
359            nom::error::ErrorKind::Char,
360        )))
361    }
362}
363
364/// Skips the line breaks and blank lines that separate `(` from the first line
365/// of the group body.
366fn skip_empty_lines(input: &str) -> &str {
367    let mut rest = input;
368    loop {
369        let line_start = rest.trim_start_matches(is_horizontal_whitespace);
370        match strip_line_ending(line_start) {
371            Some(next) => rest = next,
372            None => return rest,
373        }
374    }
375}
376
377fn strip_line_ending(input: &str) -> Option<&str> {
378    input
379        .strip_prefix("\r\n")
380        .or_else(|| input.strip_prefix('\n'))
381}
382
383fn reference_or_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
384    alt((|i| nested_group(i, state), reference.map(Link::new_singlet))).parse(input)
385}
386
387fn single_line_value_and_whitespace<'a>(
388    input: &'a str,
389    state: &ParserState,
390) -> IResult<&'a str, Link> {
391    preceded(horizontal_whitespace, |i| reference_or_link(i, state)).parse(input)
392}
393
394fn single_line_values<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Vec<Link>> {
395    many1(|i| single_line_value_and_whitespace(i, state)).parse(input)
396}
397
398fn single_line_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
399    (
400        horizontal_whitespace,
401        reference,
402        horizontal_whitespace,
403        char(':'),
404        |i| single_line_values(i, state),
405    )
406        .map(|(_, id, _, _, values)| Link::new_link(Some(id), values))
407        .parse(input)
408}
409
410fn single_line_value_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
411    (|i| single_line_values(i, state))
412        .map(|values| {
413            if values.len() == 1
414                && values[0].id.is_some()
415                && values[0].values.is_empty()
416                && values[0].children.is_empty()
417            {
418                Link::new_singlet(values[0].id.clone().unwrap())
419            } else {
420                Link::new_value(values)
421            }
422        })
423        .parse(input)
424}
425
426fn indented_id_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
427    (reference, horizontal_whitespace, char(':'), |i| {
428        eol(i, state)
429    })
430        .map(|(id, _, _, _)| Link::new_indented_id(id))
431        .parse(input)
432}
433
434/// A parenthesized group opens a nested context: its body starts fresh at
435/// indentation level zero and is parsed with the same rules as the root
436/// document, so indentation is structural inside parentheses as well.
437fn nested_group<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
438    let (body_input, _) = char('(').parse(input)?;
439    let saved = state.enter_nested_context();
440    let result = nested_group_body(body_input, state);
441    state.exit_nested_context(saved);
442    result
443}
444
445fn nested_group_body<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
446    if let Ok((rest, body)) = links(skip_empty_lines(input), state) {
447        let (rest, _) = whitespace(rest)?;
448        let (rest, _) = char(')').parse(rest)?;
449        return Ok((rest, Link::new_nested(body)));
450    }
451    let (rest, _) = whitespace(input)?;
452    let (rest, _) = char(')').parse(rest)?;
453    Ok((rest, Link::new_nested(vec![])))
454}
455
456fn single_line_any_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
457    alt((
458        terminated(|i| single_line_link(i, state), |i| eol(i, state)),
459        terminated(|i| single_line_value_link(i, state), |i| eol(i, state)),
460    ))
461    .parse(input)
462}
463
464fn any_link<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
465    alt((
466        terminated(|i| nested_group(i, state), |i| eol(i, state)),
467        |i| indented_id_link(i, state),
468        |i| single_line_any_link(i, state),
469    ))
470    .parse(input)
471}
472
473fn count_indentation(input: &str) -> IResult<&str, usize> {
474    take_while(|c| c == ' ').map(|s: &str| s.len()).parse(input)
475}
476
477fn push_indentation<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, ()> {
478    let (input, spaces) = count_indentation(skip_empty_lines(input))?;
479    let normalized_spaces = state.normalize_indentation(spaces);
480    let current = state.current_indentation();
481
482    if normalized_spaces > current {
483        state.push_indentation(normalized_spaces);
484        Ok((input, ()))
485    } else {
486        Err(nom::Err::Error(nom::error::Error::new(
487            input,
488            nom::error::ErrorKind::Verify,
489        )))
490    }
491}
492
493fn check_indentation<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, ()> {
494    let (input, spaces) = count_indentation(input)?;
495    let normalized_spaces = state.normalize_indentation(spaces);
496
497    if state.check_indentation(normalized_spaces) {
498        Ok((input, ()))
499    } else {
500        Err(nom::Err::Error(nom::error::Error::new(
501            input,
502            nom::error::ErrorKind::Verify,
503        )))
504    }
505}
506
507fn element<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
508    let (input, link) = any_link(input, state)?;
509
510    if let Ok((input, _)) = push_indentation(input, state) {
511        let (input, children) = links(input, state)?;
512        Ok((input, link.with_children(children)))
513    } else {
514        Ok((input, link))
515    }
516}
517
518fn first_line<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
519    // Set base indentation from the first line and consume it, so that the first
520    // line is parsed exactly like every following line.
521    let (input, spaces) = count_indentation(input)?;
522    state.set_base_indentation(spaces);
523    element(input, state)
524}
525
526fn line<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Link> {
527    // Blank lines do not break a document, they are simply skipped
528    preceded(|i| check_indentation(i, state), |i| element(i, state)).parse(skip_empty_lines(input))
529}
530
531fn links<'a>(input: &'a str, state: &ParserState) -> IResult<&'a str, Vec<Link>> {
532    let (input, first) = first_line(input, state)?;
533    let (input, rest) = many0(|i| line(i, state)).parse(input)?;
534
535    state.pop_indentation();
536
537    let mut result = vec![first];
538    result.extend(rest);
539    Ok((input, result))
540}
541
542pub fn parse_document(input: &str) -> IResult<&str, Vec<Link>> {
543    let state = ParserState::new();
544
545    // Skip leading blank lines but preserve the line structure
546    let input = skip_empty_lines(input);
547
548    // Handle empty or whitespace-only documents
549    if input.trim().is_empty() {
550        return Ok(("", vec![]));
551    }
552
553    let (input, result) = links(input, &state)?;
554    let (input, _) = whitespace(input)?;
555    let (input, _) = eof(input)?;
556
557    Ok((input, result))
558}