Skip to main content

marco_core/parser/
shared.rs

1// Canonical span conversion helpers for the parser layer
2// Centralized to ensure blocks and inlines use the same logic.
3
4use crate::parser::position::{Position, Span as ParserSpan};
5use nom_locate::LocatedSpan;
6
7/// Grammar span type (nom_locate::LocatedSpan)
8pub type GrammarSpan<'a> = LocatedSpan<&'a str>;
9
10/// Convert grammar span (LocatedSpan) to parser span (line/column)
11///
12/// This handles multi-line fragments by computing end line/column
13/// based on newline count and last-line length. Columns are byte-based
14/// 1-based offsets to match `Position` semantics.
15pub fn to_parser_span(span: GrammarSpan) -> ParserSpan {
16    let start_line = span.location_line() as usize; // 1-based
17    let newline_count = span.fragment().matches('\n').count();
18    let end_line = start_line + newline_count;
19
20    let end_column = if span.fragment().ends_with('\n') {
21        // If the fragment ends with newline, end column is column 1 of next line
22        1
23    } else if let Some(last_newline_pos) = span.fragment().rfind('\n') {
24        // Multi-line: bytes after last newline + 1 for 1-based
25        span.fragment()[last_newline_pos + 1..].len() + 1
26    } else {
27        // Single-line: start column (byte-based) + fragment byte length
28        span.get_column() + span.fragment().len()
29    };
30
31    let start = Position::new(start_line, span.get_column(), span.location_offset());
32    let end = Position::new(
33        end_line,
34        end_column,
35        span.location_offset() + span.fragment().len(),
36    );
37    ParserSpan::new(start, end)
38}
39
40/// Convert grammar span range (start, end) to parser span
41/// Convert a grammar span range where `end` is the remainder span
42/// (i.e. the nom `rest` after a match). This sets the end position to the
43/// `end.location_offset()` (start of the remainder), matching inline parser
44/// usage patterns like `to_parser_span_range(start, rest)`.
45pub fn to_parser_span_range(start: GrammarSpan, end: GrammarSpan) -> ParserSpan {
46    let start_pos = Position::new(
47        start.location_line() as usize,
48        start.get_column(),
49        start.location_offset(),
50    );
51    let end_pos = Position::new(
52        end.location_line() as usize,
53        end.get_column(),
54        end.location_offset(),
55    );
56    ParserSpan::new(start_pos, end_pos)
57}
58
59/// Convert a grammar span range where `end` is the final fragment of the
60/// matched range (i.e. inclusive). This preserves the previous block-level
61/// semantics where callers pass the last fragment and expect the end to be
62/// at `end.location_offset() + end.fragment().len()`.
63pub fn to_parser_span_range_inclusive(start: GrammarSpan, end: GrammarSpan) -> ParserSpan {
64    let start_pos = Position::new(
65        start.location_line() as usize,
66        start.get_column(),
67        start.location_offset(),
68    );
69    let end_pos = Position::new(
70        end.location_line() as usize,
71        end.get_column() + end.fragment().len(),
72        end.location_offset() + end.fragment().len(),
73    );
74    ParserSpan::new(start_pos, end_pos)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_to_parser_span_ascii() {
83        let input = GrammarSpan::new("hello");
84        let span = to_parser_span(input);
85        assert_eq!(span.start.line, 1);
86        assert_eq!(span.start.column, 1);
87        assert_eq!(span.end.column, 6); // 5 bytes + 1-based
88    }
89
90    #[test]
91    fn test_to_parser_span_utf8_and_emoji() {
92        let input = GrammarSpan::new("Tëst");
93        let span = to_parser_span(input);
94        assert_eq!(span.start.column, 1);
95        // 'Tëst' is 5 bytes; end.column should be 6
96        assert_eq!(span.end.column, 6);
97
98        let input2 = GrammarSpan::new("🎨");
99        let span2 = to_parser_span(input2);
100        assert_eq!(span2.start.column, 1);
101        // emoji 4 bytes -> end column 5
102        assert_eq!(span2.end.column, 5);
103    }
104}