Skip to main content

oxc_markdown_parser/inline/
emphasis.rs

1//! Emphasis and strikethrough via the CommonMark delimiter stack.
2//!
3//! `*`/`_`/`~` runs are pushed as text nodes plus a [`Delim`] carrying their flanking classification,
4//! then resolved in two family passes (attention vs strikethrough) mirroring micromark's resolver mechanics:
5//!
6//! - At the end of a text run, the family whose first delimiter appeared first resolves first
7//!   (micromark registers resolvers in first-use order);
8//!   inside brackets and inside every formed pair,
9//!   strikethrough resolves before attention (micromark's fixed `insideSpan` order).
10//! - When a pair forms, live delimiters of the *other* family between the pair are re-resolved recursively
11//!   within the new node's children;
12//!   the pair's own family between them is dead (the spec's rule).
13//!
14//! Resolution never moves a node: a pair only trims its two delimiter runs and is recorded as a [`Formed`],
15//! so every index stays valid; [`build`] makes the tree once at the end.
16//! Exhausted delimiters stay in place with `remaining == 0`.
17//! Node spans come straight from the delimiter runs' own ranges, never from arithmetic over child spans.
18
19use std::ops::Range;
20
21use crate::{
22    Constructs, syntax,
23    syntax::unicode::{is_punctuation, is_whitespace},
24};
25
26use super::{PN, Tokenizer};
27
28#[derive(Clone, Copy)]
29pub struct Delim {
30    pub node: usize,
31    pub marker: u8,
32    /// Unconsumed delimiter characters (0 = dead entry).
33    pub remaining: usize,
34    pub can_open: bool,
35    pub can_close: bool,
36}
37
38/// A pair the resolver formed:
39/// the node's extent (markers included), its marker and marker length.
40pub struct Formed {
41    pub r: Range<usize>,
42    pub marker: u8,
43    pub len: u8,
44}
45
46impl Tokenizer<'_, '_> {
47    pub(crate) fn delimiter_run(&mut self) {
48        let bytes = self.t.as_bytes();
49        let marker = bytes[self.pos];
50        let start = self.pos;
51        let len = syntax::run_len(&bytes[start..], marker);
52        let prev = self.t[..start].chars().next_back();
53        let next = self.t[start + len..].chars().next();
54        let run = Run::new(marker, len, prev, next, self.constructs);
55        self.flush_text(start);
56        self.nodes.push(PN::Text(start..start + len));
57        // micromark registers a family's resolver when its construct succeeds,
58        // which is every `*`/`_` run and every valid-length `~` run,
59        // regardless of whether the run can open or close.
60        if run.valid {
61            self.first_delim_tilde.get_or_insert(marker == b'~');
62        }
63        if run.can_open || run.can_close {
64            self.delims.push(Delim {
65                node: self.nodes.len() - 1,
66                marker,
67                remaining: len,
68                can_open: run.can_open,
69                can_close: run.can_close,
70            });
71        }
72        self.pos = start + len;
73        self.text_start = self.pos;
74    }
75
76    /// Resolution inside a bracket label:
77    /// micromark's fixed `insideSpan` order (strikethrough, then attention).
78    pub(crate) fn process_emphasis(&mut self, bottom: usize) {
79        resolve(&mut self.nodes, &mut self.delims[bottom..], true, &mut self.formed);
80    }
81
82    /// End-of-run resolution: the first-used family resolves first.
83    pub(crate) fn process_emphasis_final(&mut self) {
84        let tilde_first = self.first_delim_tilde.unwrap_or(false);
85        resolve(&mut self.nodes, &mut self.delims, tilde_first, &mut self.formed);
86    }
87}
88
89fn resolve(nodes: &mut [PN], delims: &mut [Delim], tilde_first: bool, formed: &mut Vec<Formed>) {
90    let order = if tilde_first { [true, false] } else { [false, true] };
91    for tilde_pass in order {
92        if delims.iter().any(|d| d.remaining > 0 && d.can_close && (d.marker == b'~') == tilde_pass)
93        {
94            pass(nodes, delims, tilde_pass, formed);
95        }
96    }
97}
98
99/// One family pass of the spec's "process emphasis" over `delims`.
100#[expect(clippy::cast_possible_truncation)] // a pair uses one or two characters
101fn pass(nodes: &mut [PN], delims: &mut [Delim], tilde_pass: bool, formed: &mut Vec<Formed>) {
102    // Lowest index worth scanning per closer class:
103    // [marker is `_`][closer len % 3][closer can also open].
104    let mut openers_bottom = [[[0; 2]; 3]; 2];
105    // Strikethrough pairs equal-sized runs only:
106    // a failed scan rules out every opener of that size below.
107    let mut tilde_bottom = [0; 3];
108    let mut closer = 0;
109    while closer < delims.len() {
110        let c = &delims[closer];
111        if (c.marker == b'~') != tilde_pass || !c.can_close || c.remaining == 0 {
112            closer += 1;
113            continue;
114        }
115        let class = (usize::from(c.marker == b'_'), c.remaining % 3, usize::from(c.can_open));
116        let scan_bottom = if tilde_pass {
117            tilde_bottom[c.remaining]
118        } else {
119            openers_bottom[class.0][class.1][class.2]
120        };
121
122        let mut opener = None;
123        let mut i = closer;
124        while i > scan_bottom {
125            i -= 1;
126            let o = &delims[i];
127            if o.marker != c.marker || !o.can_open || o.remaining == 0 {
128                continue;
129            }
130            if tilde_pass {
131                if o.remaining == c.remaining {
132                    opener = Some(i);
133                    break;
134                }
135                continue;
136            }
137            // Rule of three, micromark's exact form:
138            // current (partially consumed) sizes, not original run lengths.
139            // micromark mutates sequence boundaries as it pairs.
140            let forbidden = (o.can_close || c.can_open)
141                && !c.remaining.is_multiple_of(3)
142                && (o.remaining + c.remaining).is_multiple_of(3);
143            if !forbidden {
144                opener = Some(i);
145                break;
146            }
147        }
148        let Some(opener) = opener else {
149            if tilde_pass {
150                tilde_bottom[c.remaining] = closer;
151            } else {
152                openers_bottom[class.0][class.1][class.2] = closer;
153            }
154            if !delims[closer].can_open {
155                delims[closer].remaining = 0;
156            }
157            closer += 1;
158            continue;
159        };
160
161        let use_n = if tilde_pass {
162            delims[closer].remaining
163        } else if delims[opener].remaining >= 2 && delims[closer].remaining >= 2 {
164            2
165        } else {
166            1
167        };
168        let opener_node = delims[opener].node;
169        let closer_node = delims[closer].node;
170
171        // Live delimiters of the other family between the pair resolve
172        // within the new node's children (micromark's `insideSpan`);
173        // the pair's own family between them is dead either way.
174        let between = &mut delims[opener + 1..closer];
175        if between.iter().any(|d| d.remaining > 0 && (d.marker == b'~') != tilde_pass) {
176            for d in between.iter_mut() {
177                if (d.marker == b'~') == tilde_pass {
178                    d.remaining = 0;
179                }
180            }
181            resolve(nodes, between, true, formed);
182        }
183        for d in between.iter_mut() {
184            d.remaining = 0;
185        }
186
187        // Trim the used characters off the opener's tail and the closer's head;
188        // they become the new node's span.
189        let o_end = text_range(nodes, opener_node).end;
190        text_range(nodes, opener_node).end = o_end - use_n;
191        let c_start = text_range(nodes, closer_node).start;
192        text_range(nodes, closer_node).start = c_start + use_n;
193        formed.push(Formed {
194            r: o_end - use_n..c_start + use_n,
195            marker: delims[opener].marker,
196            len: use_n as u8,
197        });
198
199        delims[opener].remaining -= use_n;
200        delims[closer].remaining -= use_n;
201        // The opener's size changed mod 3,
202        // so rule-of-three verdicts recorded in openers_bottom above it are stale
203        // (micromark judges with current sizes and rescans everything):
204        // rewind same-marker bottoms down to the opener.
205        // A used-up opener needs no rewind, it can never pair again,
206        // so its recorded verdicts stay correct.
207        if !tilde_pass && delims[opener].remaining > 0 {
208            let m = usize::from(delims[opener].marker == b'_');
209            for b in openers_bottom[m].iter_mut().flatten() {
210                *b = (*b).min(opener);
211            }
212        }
213        if delims[closer].remaining == 0 {
214            closer += 1;
215        }
216    }
217}
218
219fn text_range(nodes: &mut [PN], node: usize) -> &mut Range<usize> {
220    match &mut nodes[node] {
221        PN::Text(r) => r,
222        _ => unreachable!("delimiter nodes are text runs"),
223    }
224}
225
226/// The tree of the flat `nodes` and the pairs `formed` over them (consumed; they nest properly).
227/// Emptied delimiter runs are dropped.
228pub fn build(nodes: impl IntoIterator<Item = PN>, formed: &mut Vec<Formed>) -> Vec<PN> {
229    // Pre-order
230    formed.sort_unstable_by(|a, b| a.r.start.cmp(&b.r.start).then(b.r.end.cmp(&a.r.end)));
231    let mut pending = formed.drain(..).peekable();
232    let mut out = Vec::new();
233    let mut stack: Vec<(Formed, Vec<PN>)> = Vec::new();
234    let close = |stack: &mut Vec<(Formed, Vec<PN>)>, out: &mut Vec<PN>| {
235        let (pair, children) = stack.pop().expect("a pair is open");
236        let node = PN::Emph { marker: pair.marker, len: pair.len, children, r: pair.r };
237        stack.last_mut().map_or(out, |(_, children)| children).push(node);
238    };
239    for node in nodes {
240        let at = node.start();
241        // A pair holds at least one node, so none both ends and starts before this one
242        while stack.last().is_some_and(|(pair, _)| pair.r.end <= at) {
243            close(&mut stack, &mut out);
244        }
245        while pending.peek().is_some_and(|pair| pair.r.start <= at) {
246            stack.push((pending.next().expect("peeked"), Vec::new()));
247        }
248        if matches!(&node, PN::Text(r) if r.is_empty()) {
249            continue;
250        }
251        stack.last_mut().map_or(&mut out, |(_, children)| children).push(node);
252    }
253    debug_assert_eq!(pending.len(), 0, "every pair starts before the last node");
254    while !stack.is_empty() {
255        close(&mut stack, &mut out);
256    }
257    out
258}
259
260/// A `*` / `_` / `~` run as the tokenizer sees it: its marker, length and flanking.
261/// Built by [`Run::new`], for [`pairs`] on a text a printer is about to emit.
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub struct Run {
264    pub marker: u8,
265    pub len: usize,
266    pub can_open: bool,
267    pub can_close: bool,
268    /// The run registers its family's resolver (every `*` / `_` run, a `~` run of a valid length).
269    pub valid: bool,
270}
271
272impl Run {
273    /// `prev` / `next` are the characters around the run (`None` at a boundary).
274    pub fn new(
275        marker: u8,
276        len: usize,
277        prev: Option<char>,
278        next: Option<char>,
279        constructs: &Constructs,
280    ) -> Self {
281        // GFM strikethrough: runs of one (only with `singleTilde`) or two tildes;
282        // longer runs never participate (micromark's construct fails on them).
283        let min = if constructs.gfm_strikethrough_single_tilde { 1 } else { 2 };
284        let valid = marker != b'~' || (min..=2).contains(&len);
285        let (can_open, can_close) = if valid {
286            classify(marker, prev, next, constructs.gfm_strikethrough)
287        } else {
288            (false, false)
289        };
290        Self { marker, len, can_open, can_close, valid }
291    }
292}
293
294/// One emphasis / strong / strikethrough node [`pairs`] would form: the runs' indices and its kind.
295#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
296pub struct Pair {
297    pub opener: usize,
298    pub closer: usize,
299    /// Two characters of each `*` / `_` run (`**` / `__`); a `~` pair is strikethrough, never strong.
300    pub strong: bool,
301}
302
303/// The nodes the delimiter runs of one inline sequence form, sorted by position.
304///
305/// Decided by the parser's own resolver (flanking, rule of three, family order).
306/// The runs are those of one bracket level (`in_link`: a link's text resolves on its own,
307/// strikethrough first); text between them is opaque, only the runs' flanking matters.
308/// At the top level the family used first resolves first.
309///
310/// A printer that changes a marker or joins lines runs it on what it is about to emit
311/// and compares the result with the tree it prints, before committing to the change.
312pub fn pairs(runs: &[Run], in_link: bool) -> Vec<Pair> {
313    let tilde_first = in_link || runs.iter().find(|r| r.valid).is_some_and(|r| r.marker == b'~');
314    // Lay the runs out as text nodes with one opaque text node between neighbors
315    // (same-marker runs never touch; a pair always has a node between it).
316    let mut nodes = Vec::with_capacity(runs.len() * 2);
317    let mut delims = Vec::new();
318    let mut starts = Vec::with_capacity(runs.len());
319    let mut pos = 0;
320    for run in runs {
321        if !nodes.is_empty() {
322            nodes.push(PN::Text(pos..pos + 1));
323            pos += 1;
324        }
325        starts.push(pos);
326        if run.can_open || run.can_close {
327            delims.push(Delim {
328                node: nodes.len(),
329                marker: run.marker,
330                remaining: run.len,
331                can_open: run.can_open,
332                can_close: run.can_close,
333            });
334        }
335        nodes.push(PN::Text(pos..pos + run.len));
336        pos += run.len;
337    }
338    let mut formed = Vec::new();
339    resolve(&mut nodes, &mut delims, tilde_first, &mut formed);
340    let run_at = |offset: usize| starts.partition_point(|&s| s <= offset) - 1;
341    let mut out: Vec<Pair> = formed
342        .iter()
343        .map(|pair| Pair {
344            opener: run_at(pair.r.start),
345            closer: run_at(pair.r.end - 1),
346            strong: pair.marker != b'~' && pair.len == 2,
347        })
348        .collect();
349    out.sort_unstable();
350    out
351}
352
353/// Flanking classification, micromark's `attention.js` version.
354///
355/// This deliberately follows micromark rather than the spec's prose:
356/// on top of the left/right-flanking rules,
357/// a `*`/`_` run whose neighboring character is itself an attention marker
358/// (`*` `_`, and `~` with strikethrough on) may always open (next) / close (prev).
359/// A `~` run gets the plain attention rules,
360/// no marker relaxation, no `_` adjustment (micromark's `gfm-strikethrough`).
361/// Parse behavior targets micromark exactly, quirks included.
362///
363/// `prev` / `next` are the characters around the run; `None` is a boundary (whitespace class).
364/// Returns `(can_open, can_close)`.
365/// Public as [`crate::attention`] for printers that must know whether a literal run they emit would pair up.
366pub fn classify(marker: u8, prev: Option<char>, next: Option<char>, tilde: bool) -> (bool, bool) {
367    let is_marker = |c: Option<char>| {
368        marker != b'~' && (matches!(c, Some('*' | '_')) || (tilde && c == Some('~')))
369    };
370    let before = group(prev);
371    let after = group(next);
372
373    let open = after == 0 || (after == 2 && before != 0) || is_marker(next);
374    let close = before == 0 || (before == 2 && after != 0) || is_marker(prev);
375    if marker == b'_' {
376        (open && (before != 0 || !close), close && (after != 0 || !open))
377    } else {
378        (open, close)
379    }
380}
381
382/// 1 = whitespace (sequence boundaries included), 2 = punctuation, 0 = other.
383///
384/// Whitespace is micromark's `unicodeWhitespace` (JS `\s`),
385/// not Rust's `char::is_whitespace`: U+0085 is "other", U+FEFF is whitespace.
386fn group(c: Option<char>) -> u8 {
387    match c {
388        None => 1,
389        Some(c) if is_whitespace(c) => 1,
390        Some(c) if is_punctuation(c) => 2,
391        Some(_) => 0,
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    fn run(marker: u8, len: usize, prev: Option<char>, next: Option<char>) -> Run {
400        Run::new(marker, len, prev, next, &Constructs::markdown())
401    }
402
403    #[test]
404    fn pairs_nested_and_rule_of_three() {
405        // `***b***`: strong inside emphasis, both from the same two runs
406        let runs = [run(b'*', 3, None, Some('b')), run(b'*', 3, Some('b'), None)];
407        assert_eq!(
408            pairs(&runs, false),
409            vec![
410                Pair { opener: 0, closer: 1, strong: false },
411                Pair { opener: 0, closer: 1, strong: true }
412            ]
413        );
414        // `*a**b*`: the `**` cannot pair (rule of three), the outer pair forms
415        let runs = [
416            run(b'*', 1, None, Some('a')),
417            run(b'*', 2, Some('a'), Some('b')),
418            run(b'*', 1, Some('b'), None),
419        ];
420        assert_eq!(pairs(&runs, false), vec![Pair { opener: 0, closer: 2, strong: false }]);
421        // `**a _**b**_ c**` (micromark): `**a _` strong, then `**_ c**` strong
422        let runs = [
423            run(b'*', 2, None, Some('a')),
424            run(b'_', 1, Some(' '), Some('*')),
425            run(b'*', 2, Some('_'), Some('b')),
426            run(b'*', 2, Some('b'), Some('_')),
427            run(b'_', 1, Some('*'), Some(' ')),
428            run(b'*', 2, Some('c'), None),
429        ];
430        assert_eq!(
431            pairs(&runs, false),
432            vec![
433                Pair { opener: 0, closer: 2, strong: true },
434                Pair { opener: 3, closer: 5, strong: true }
435            ]
436        );
437    }
438
439    #[test]
440    fn pairs_family_order() {
441        // `~~*a~~*`: strikethrough first (its run comes first) swallows the `*` opener
442        let runs = [
443            run(b'~', 2, None, Some('*')),
444            run(b'*', 1, Some('~'), Some('a')),
445            run(b'~', 2, Some('a'), Some('*')),
446            run(b'*', 1, Some('~'), None),
447        ];
448        assert_eq!(pairs(&runs, true), vec![Pair { opener: 0, closer: 2, strong: false }]);
449    }
450}