Skip to main content

markdown_prose_hooks/
code_span.rs

1//! Inline code spans: the one pattern in the tool that cannot be a regex here.
2//!
3//! `_SUB_CODE_SPAN` is ``(`+)(?:(?!\1).)*\1``, which needs a backreference and a
4//! negative lookahead. The `regex` crate excludes both by design, so this is
5//! hand-written whatever else the port decides — and it is an **approximation of
6//! CommonMark that is reproduced rather than corrected**. A real CommonMark span
7//! closes on a run of exactly the opener's length; this closes on the first
8//! position carrying that many or more, so `` `a``` `` is two spans where a
9//! renderer sees one. The corpus pins the approximation. Correcting it here
10//! would make the two implementations disagree with each other, which is the
11//! only thing this port is not allowed to do.
12//!
13//! Every expected value in the tests below came from running the Python.
14
15/// The one byte the whole module is about.
16const TICK: u8 = b'`';
17
18/// Iterator over the byte ranges `_SUB_CODE_SPAN` would replace.
19///
20/// Yields the same sequence `re.finditer` does, which is non-overlapping and
21/// left to right. The pattern needs at least two backticks, so it can never
22/// match empty and the zero-width-advance rule never applies.
23pub struct CodeSpans<'a> {
24    bytes: &'a [u8],
25    at: usize,
26}
27
28impl Iterator for CodeSpans<'_> {
29    type Item = (usize, usize);
30
31    fn next(&mut self) -> Option<(usize, usize)> {
32        let (start, end) = next_span(self.bytes, self.at)?;
33        self.at = end;
34        Some((start, end))
35    }
36}
37
38/// Every inline code span in `body`, as byte ranges.
39#[must_use]
40pub fn code_spans(body: &str) -> CodeSpans<'_> {
41    CodeSpans {
42        bytes: body.as_bytes(),
43        at: 0,
44    }
45}
46
47/// `_masked_code_spans`: every span blanked to spaces, same length out as in.
48///
49/// "Same length" is Python's, and Python counts characters: the replacement is
50/// `' ' * len(match.group())`. A span holding non-ASCII therefore masks to
51/// *fewer bytes* than it occupied, and a caller reasoning about columns is
52/// reasoning about the same columns Python would.
53#[must_use]
54pub fn mask_code_spans(body: &str) -> String {
55    let mut masked = String::with_capacity(body.len());
56    let mut cursor = 0;
57    for (start, end) in code_spans(body) {
58        masked.push_str(&body[cursor..start]);
59        for _ in 0..body[start..end].chars().count() {
60            masked.push(' ');
61        }
62        cursor = end;
63    }
64    masked.push_str(&body[cursor..]);
65    masked
66}
67
68/// `'|' in _masked_code_spans(body)`, without building the mask.
69///
70/// Observably identical to asking [`mask_code_spans`] and searching its result:
71/// masking only ever replaces span bytes with spaces, so a pipe survives exactly
72/// when it sits outside every span.
73#[must_use]
74pub fn contains_unmasked_pipe(body: &str) -> bool {
75    let mut cursor = 0;
76    for (start, end) in code_spans(body) {
77        if body[cursor..start].contains('|') {
78            return true;
79        }
80        cursor = end;
81    }
82    body[cursor..].contains('|')
83}
84
85/// The next span at or after `from`, or `None` when the rest holds none.
86fn next_span(bytes: &[u8], from: usize) -> Option<(usize, usize)> {
87    let mut i = from;
88    while i < bytes.len() {
89        if bytes[i] != TICK {
90            i += 1;
91            continue;
92        }
93        let mut run = 0;
94        while bytes.get(i + run) == Some(&TICK) {
95            run += 1;
96        }
97        // ``(`+)`` is greedy, so the engine tries the whole run first and gives
98        // back one backtick at a time. The first opener length that finds a
99        // closer wins, and the order is observable: ``` ```a` ``` closes on a
100        // one-backtick opener only because three and two both failed first.
101        for k in (1..=run).rev() {
102            if let Some(end) = find_closer(bytes, i + k, k) {
103                return Some((i, end));
104            }
105        }
106        // `i + 1` restarts inside the run, which is what the engine does. It is
107        // also indistinguishable from `i + run`, and provably so rather than by
108        // luck: reaching this line means every opener length failed, including
109        // one, and a one-backtick opener can only fail when the run is one
110        // backtick long — with two or more, `bytes[i + 1]` is itself a backtick
111        // and closes the span immediately. So the fallthrough is unreachable
112        // unless `run == 1`, where the two are the same number.
113        i += 1;
114    }
115    None
116}
117
118/// The whole of ``(?:(?!\1).)*\1``: where a span opened with `k` backticks ends.
119///
120/// The body loop consumes one character at a time and refuses to start one where
121/// the closer would match, so it halts at the first position carrying `k`
122/// backticks — where the closer then matches — or at a `\n` or the end of input,
123/// where it cannot. Every position it did consume failed the same test the
124/// closer applies, so backtracking the body can never rescue the attempt and
125/// this linear scan is exact rather than approximate.
126fn find_closer(bytes: &[u8], start: usize, k: usize) -> Option<usize> {
127    let mut j = start;
128    loop {
129        if j + k <= bytes.len() && bytes[j..j + k].iter().all(|b| *b == TICK) {
130            return Some(j + k);
131        }
132        // `.` does not match `\n` without `re.DOTALL`, and matches every other
133        // character — including `\r`, `\x0b`, `\x0c`, U+0085, U+2028 and U+2029,
134        // none of which this tool calls a line boundary either.
135        if j >= bytes.len() || bytes[j] == b'\n' {
136            return None;
137        }
138        j += 1;
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn matched(body: &str) -> Vec<&str> {
147        code_spans(body).map(|(s, e)| &body[s..e]).collect()
148    }
149
150    #[test]
151    fn the_approximation_closes_on_the_first_backtick_of_a_longer_run() {
152        // CommonMark would make this one span. The Python does not, the corpus
153        // pins the Python, and correcting it is an explicit non-goal.
154        assert_eq!(matched("`a```"), ["`a`", "``"]);
155    }
156
157    #[test]
158    fn openers_are_tried_longest_first() {
159        // Three backticks open, and the closer is the *first* run of three, so
160        // the trailing run is left over rather than swallowed.
161        assert_eq!(matched("``` ``` ```"), ["``` ```", "``"]);
162        // Here three and two both fail to find a closer, so the opener backs
163        // down to one backtick and closes on the very next one.
164        assert_eq!(matched("```a`"), ["``", "`a`"]);
165    }
166
167    #[test]
168    fn a_bare_double_run_is_an_empty_span() {
169        assert_eq!(matched("``"), ["``"]);
170        // An odd run leaves its last backtick behind.
171        assert_eq!(matched("```"), ["``"]);
172        assert_eq!(matched("````"), ["````"]);
173        assert_eq!(matched("`````"), ["````"]);
174        assert_eq!(matched("``````"), ["``````"]);
175    }
176
177    #[test]
178    fn a_run_of_two_always_opens_a_span_where_it_starts() {
179        // The fact the restart step rests on. A two-backtick run closes on its
180        // own second backtick whatever follows it, so an opener search that
181        // begins on one can never come away empty — which is why restarting at
182        // `i + 1` and at `i + run` are the same walk.
183        for tail in ["", "a", "`", "\n", "\\", "a`", "\n`"] {
184            for run in 2..=4 {
185                let body = format!("x{}{tail}", "`".repeat(run));
186                assert_eq!(
187                    code_spans(&body).next().map(|(start, _)| start),
188                    Some(1),
189                    "run of {run} before {tail:?}"
190                );
191            }
192        }
193    }
194
195    #[test]
196    fn an_unterminated_run_matches_nothing() {
197        assert_eq!(matched("`a"), Vec::<&str>::new());
198        assert!(matched("").is_empty());
199        assert!(matched("no ticks here").is_empty());
200    }
201
202    #[test]
203    fn a_longer_opener_swallows_a_shorter_inner_run() {
204        assert_eq!(matched("``a | b` c``"), ["``a | b` c``"]);
205        assert!(!contains_unmasked_pipe("``a | b` c``"));
206        assert!(!contains_unmasked_pipe("``|`|``"));
207    }
208
209    #[test]
210    fn a_bare_pipe_survives_masking() {
211        assert!(contains_unmasked_pipe("a | b"));
212        assert!(contains_unmasked_pipe("a `b` | c"));
213        // A pipe outside a span and another inside one: the outside one wins.
214        assert!(contains_unmasked_pipe("|`|`|"));
215        assert!(!contains_unmasked_pipe("`|`"));
216    }
217
218    #[test]
219    fn mask_is_char_length_not_byte_length() {
220        // Python's `' ' * len(group)` counts characters, so a non-ASCII span
221        // masks to fewer bytes than it occupied: four here, against eight.
222        assert_eq!(mask_code_spans("`\u{65e5}\u{672c}`"), "    ");
223        assert_eq!(mask_code_spans("a`b`c"), "a   c");
224        assert_eq!(mask_code_spans("``` ``` ```"), "          `");
225        assert_eq!(mask_code_spans("````a``"), "    a  ");
226        assert_eq!(mask_code_spans("`a"), "`a");
227    }
228
229    #[test]
230    fn a_backslash_does_not_escape_a_backtick() {
231        // The Python pattern has no escape handling. Adding CommonMark-correct
232        // escaping would be a silent behavior change, so the span opens on the
233        // backtick after the backslash and the backslash stays outside it.
234        assert_eq!(matched("\\`a\\`"), ["`a\\`"]);
235    }
236
237    #[test]
238    fn a_newline_stops_a_span_and_nothing_else_does() {
239        // Python's `.` does not match `\n` without `re.DOTALL`.
240        assert_eq!(matched("`a\nb`"), Vec::<&str>::new());
241        // Every other separator this tool knows about is ordinary content.
242        assert_eq!(matched("`a\rb`"), ["`a\rb`"]);
243        assert_eq!(matched("`a\u{2028}b`"), ["`a\u{2028}b`"]);
244        assert_eq!(matched("`a\u{b}b`"), ["`a\u{b}b`"]);
245    }
246
247    #[test]
248    fn the_pipe_shortcut_agrees_with_the_mask_it_stands_for() {
249        for body in [
250            "", "|", "`|`", "``|`|``", "a | b", "|`a`", "`a`|", "`a|", "```|```", "x``y``z|",
251        ] {
252            assert_eq!(
253                contains_unmasked_pipe(body),
254                mask_code_spans(body).contains('|'),
255                "disagreed on {body:?}"
256            );
257        }
258    }
259}