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}