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;
6use std::cell::Cell;
7
8// ---------------------------------------------------------------------------
9// Per-parse runtime option flags (thread-local for zero call-site overhead)
10// ---------------------------------------------------------------------------
11
12thread_local! {
13    static TRACK_POSITIONS: Cell<bool> = const { Cell::new(true) };
14    static PARSE_MATH: Cell<bool>      = const { Cell::new(true) };
15    static PARSE_DIAGRAMS: Cell<bool>  = const { Cell::new(true) };
16}
17
18/// RAII guard that sets per-thread parse options and restores them on drop.
19///
20/// Created by [`parse_with_options`] before calling the parse pipeline; dropped
21/// (and thus restored) when the parse call returns or panics.
22pub(crate) struct ParseOptionsGuard {
23    prev_track: bool,
24    prev_math: bool,
25    prev_diagrams: bool,
26}
27
28impl ParseOptionsGuard {
29    pub(crate) fn new(track: bool, math: bool, diagrams: bool) -> Self {
30        let prev_track = TRACK_POSITIONS.with(|c| c.replace(track));
31        let prev_math = PARSE_MATH.with(|c| c.replace(math));
32        let prev_diagrams = PARSE_DIAGRAMS.with(|c| c.replace(diagrams));
33        Self {
34            prev_track,
35            prev_math,
36            prev_diagrams,
37        }
38    }
39}
40
41impl Drop for ParseOptionsGuard {
42    fn drop(&mut self) {
43        TRACK_POSITIONS.with(|c| c.set(self.prev_track));
44        PARSE_MATH.with(|c| c.set(self.prev_math));
45        PARSE_DIAGRAMS.with(|c| c.set(self.prev_diagrams));
46    }
47}
48
49/// Returns whether math parsing is enabled in the current parse context.
50#[inline]
51pub(crate) fn parse_math_enabled() -> bool {
52    PARSE_MATH.with(|c| c.get())
53}
54
55/// Returns whether diagram parsing is enabled in the current parse context.
56#[inline]
57pub(crate) fn parse_diagrams_enabled() -> bool {
58    PARSE_DIAGRAMS.with(|c| c.get())
59}
60
61// ---------------------------------------------------------------------------
62// opt_span helpers — use these in parser code instead of `Some(to_parser_span(...))`
63// ---------------------------------------------------------------------------
64
65/// Returns `None` (skipping O(n) string scans) when position tracking is disabled,
66/// or `Some(span)` with real line/column data when enabled.
67///
68/// Replace `span: Some(to_parser_span(x))` with `span: opt_span(x)`.
69#[inline]
70pub fn opt_span(span: GrammarSpan) -> Option<ParserSpan> {
71    if !TRACK_POSITIONS.with(|c| c.get()) {
72        return None;
73    }
74    Some(to_parser_span(span))
75}
76
77/// Like [`opt_span`] but takes a start/end range using exclusive end semantics.
78///
79/// Replace `span: Some(to_parser_span_range(start, end))` with
80/// `span: opt_span_range(start, end)`.
81#[inline]
82pub fn opt_span_range(start: GrammarSpan, end: GrammarSpan) -> Option<ParserSpan> {
83    if !TRACK_POSITIONS.with(|c| c.get()) {
84        return None;
85    }
86    Some(to_parser_span_range(start, end))
87}
88
89/// Like [`opt_span`] but takes a start/end range using inclusive end semantics.
90///
91/// Replace `span: Some(to_parser_span_range_inclusive(start, end))` with
92/// `span: opt_span_range_inclusive(start, end)`.
93#[inline]
94pub fn opt_span_range_inclusive(start: GrammarSpan, end: GrammarSpan) -> Option<ParserSpan> {
95    if !TRACK_POSITIONS.with(|c| c.get()) {
96        return None;
97    }
98    Some(to_parser_span_range_inclusive(start, end))
99}
100
101/// Grammar span type (nom_locate::LocatedSpan)
102pub type GrammarSpan<'a> = LocatedSpan<&'a str>;
103
104/// Convert grammar span (LocatedSpan) to parser span (line/column)
105///
106/// This handles multi-line fragments by computing end line/column
107/// based on newline count and last-line length. Columns are byte-based
108/// 1-based offsets to match `Position` semantics.
109pub fn to_parser_span(span: GrammarSpan) -> ParserSpan {
110    let start_line = span.location_line() as usize; // 1-based
111    let frag = span.fragment().as_bytes();
112
113    // Single O(n) pass: count newlines and record the last newline byte position.
114    let mut newline_count = 0usize;
115    let mut last_nl: Option<usize> = None;
116    for (i, &b) in frag.iter().enumerate() {
117        if b == b'\n' {
118            newline_count += 1;
119            last_nl = Some(i);
120        }
121    }
122    let end_line = start_line + newline_count;
123
124    let end_column = match last_nl {
125        Some(pos) if pos == frag.len() - 1 => {
126            // Fragment ends with '\n' — end column is column 1 of the next line.
127            1
128        }
129        Some(pos) => {
130            // Multi-line: bytes after last newline + 1 (1-based).
131            frag.len() - pos - 1 + 1
132        }
133        None => {
134            // Single-line: start column (byte-based) + fragment byte length.
135            span.get_column() + frag.len()
136        }
137    };
138
139    let start = Position::new(start_line, span.get_column(), span.location_offset());
140    let end = Position::new(
141        end_line,
142        end_column,
143        span.location_offset() + span.fragment().len(),
144    );
145    ParserSpan::new(start, end)
146}
147
148/// Convert grammar span range (start, end) to parser span
149/// Convert a grammar span range where `end` is the remainder span
150/// (i.e. the nom `rest` after a match). This sets the end position to the
151/// `end.location_offset()` (start of the remainder), matching inline parser
152/// usage patterns like `to_parser_span_range(start, rest)`.
153pub fn to_parser_span_range(start: GrammarSpan, end: GrammarSpan) -> ParserSpan {
154    let start_pos = Position::new(
155        start.location_line() as usize,
156        start.get_column(),
157        start.location_offset(),
158    );
159    let end_pos = Position::new(
160        end.location_line() as usize,
161        end.get_column(),
162        end.location_offset(),
163    );
164    ParserSpan::new(start_pos, end_pos)
165}
166
167/// Convert a grammar span range where `end` is the final fragment of the
168/// matched range (i.e. inclusive). This preserves the previous block-level
169/// semantics where callers pass the last fragment and expect the end to be
170/// at `end.location_offset() + end.fragment().len()`.
171pub fn to_parser_span_range_inclusive(start: GrammarSpan, end: GrammarSpan) -> ParserSpan {
172    let start_pos = Position::new(
173        start.location_line() as usize,
174        start.get_column(),
175        start.location_offset(),
176    );
177    let end_pos = Position::new(
178        end.location_line() as usize,
179        end.get_column() + end.fragment().len(),
180        end.location_offset() + end.fragment().len(),
181    );
182    ParserSpan::new(start_pos, end_pos)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_to_parser_span_ascii() {
191        let input = GrammarSpan::new("hello");
192        let span = to_parser_span(input);
193        assert_eq!(span.start.line, 1);
194        assert_eq!(span.start.column, 1);
195        assert_eq!(span.end.column, 6); // 5 bytes + 1-based
196    }
197
198    #[test]
199    fn test_to_parser_span_utf8_and_emoji() {
200        let input = GrammarSpan::new("Tëst");
201        let span = to_parser_span(input);
202        assert_eq!(span.start.column, 1);
203        // 'Tëst' is 5 bytes; end.column should be 6
204        assert_eq!(span.end.column, 6);
205
206        let input2 = GrammarSpan::new("🎨");
207        let span2 = to_parser_span(input2);
208        assert_eq!(span2.start.column, 1);
209        // emoji 4 bytes -> end column 5
210        assert_eq!(span2.end.column, 5);
211    }
212}