Skip to main content

satteri_pulldown_cmark/
parse.rs

1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21//! Tree-based two pass parser.
22
23use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, string::String, vec::Vec};
24use core::{
25    cell::Cell,
26    cmp::{max, min},
27    iter::FusedIterator,
28    num::NonZeroUsize,
29    ops::{Index, Range},
30};
31use rustc_hash::FxHashMap;
32use unicase::UniCase;
33
34#[cfg(feature = "mdx")]
35use crate::mdx::*;
36use crate::{
37    Alignment, BlockQuoteKind, CodeBlockKind, DirectiveKind, Event, HeadingLevel, LinkType,
38    MetadataBlockKind, Options, Tag, TagEnd,
39    firstpass::run_first_pass,
40    linklabel::{FootnoteLabel, LinkLabel, ReferenceLabel, scan_link_label_rest},
41    scanners::*,
42    strings::CowStr,
43    tree::{Tree, TreeIndex},
44};
45
46// Allowing arbitrary depth nested parentheses inside link destinations
47// can create denial of service vulnerabilities if we're not careful.
48// The simplest countermeasure is to limit their depth, which is
49// explicitly allowed by the spec as long as the limit is at least 3:
50// https://spec.commonmark.org/0.29/#link-destination
51pub(crate) const LINK_MAX_NESTED_PARENS: usize = 32;
52
53#[derive(Debug, Default, Clone, Copy)]
54pub(crate) struct Item {
55    pub start: usize,
56    pub end: usize,
57    pub body: ItemBody,
58}
59
60#[derive(Debug, PartialEq, Clone, Copy, Default)]
61pub(crate) enum ItemBody {
62    // These are possible inline items, need to be resolved in second pass.
63
64    // repeats, can_open, can_close
65    MaybeEmphasis(usize, bool, bool),
66    /// Head of a run the first pass's escape handler would have swallowed,
67    /// sitting on the byte after a deferred autolink candidate's last. Carries
68    /// the flags for the run the link firing creates; blocked until it does.
69    // repeats, can_open, can_close
70    MaybeEmphasisEscaped(usize, bool, bool),
71    // preceded_by_backslash, brace context
72    MaybeMath(bool, u8),
73    // quote byte, can_open, can_close
74    MaybeSmartQuote(u8, bool, bool),
75    MaybeCode(usize, bool), // number of backticks, preceded by backslash
76    MaybeHtml(bool),        // preceded by backslash
77    MaybeLinkOpen,
78    // bool indicates whether or not the preceding section could be a reference
79    MaybeLinkClose(bool),
80    MaybeImage,
81    /// Zero-width marker where a GFM autolink literal would start. Firing is
82    /// decided in `handle_inline_pass1`, where bracket-resolution state is
83    /// known; it never survives into `arena_build`.
84    MaybeAutolink(AutolinkCandidateIndex),
85
86    // These are inline items after resolution.
87    Emphasis,
88    Strong,
89    Strikethrough,
90    Superscript,
91    Subscript,
92    Math(CowIndex, bool), // true for display math
93    Code(CowIndex),
94    Link(LinkIndex),
95    Image(LinkIndex),
96    FootnoteReference(CowIndex),
97    TaskListMarker(bool), // true for checked
98
99    // These are also inline items.
100    InlineHtml,
101    OwnedInlineHtml(CowIndex),
102    SynthesizeText(CowIndex),
103    SynthesizeChar(char),
104    Html,
105    Text {
106        backslash_escaped: bool,
107    },
108    SoftBreak,
109    // true = is backlash
110    HardBreak(bool),
111
112    // Dummy node at the top of the tree - should not be used otherwise!
113    #[default]
114    Root,
115
116    // These are block items.
117    Paragraph,
118    TightParagraph,
119    Rule,
120    Heading(HeadingLevel, Option<HeadingIndex>), // heading level
121    // u32: byte length of the `lang` part within the decoded info string. The
122    // split is taken on raw source, so a character reference decoding to
123    // whitespace does not move it.
124    FencedCodeBlock(CowIndex, u32),
125    MathBlock(CowIndex), // meta string (info after $$)
126    // bool: true = lazy/no-extend (block was opened as a single-line
127    // synthetic split, e.g. after an empty list item closed via blank
128    // line); arena_build's trailing-indent extension must skip it.
129    IndentCodeBlock(bool),
130    HtmlBlock(bool), // true = trim trailing newline from value (type 6/7
131    // always; type 1-5 only when their closer pattern was found, not when
132    // the block ran out of input at EOF)
133    BlockQuote(Option<BlockQuoteKind>),
134    ContainerDirective(u8, DirectiveIndex), // (fence length, directive data)
135    LeafDirective(DirectiveIndex),
136    TextDirective(DirectiveIndex),
137    // A container directive's `[label]`, holding inline content. Emitted as a
138    // `paragraph` with `data.directiveLabel = true`. Its children are tokenized
139    // by the normal inline pass, so emphasis/strong/links resolve naturally.
140    DirectiveLabel,
141    List(bool, u8, u64),   // is_tight, list character, list start index
142    ListItem(usize, bool), // indent level, spread (loose item)
143    FootnoteDefinition(CowIndex),
144    MetadataBlock(MetadataBlockKind),
145
146    // Definition lists
147    DefinitionList(bool), // is_tight
148    // gets turned into either a paragraph or a definition list title,
149    // depending on whether there's a definition after it
150    MaybeDefinitionListTitle,
151    DefinitionListTitle,
152    DefinitionListDefinition(usize, bool), // indent level, spread (loose definition)
153
154    // Tables
155    Table(AlignmentIndex),
156    TableHead,
157    TableRow,
158    TableCell,
159
160    // MDX
161    #[cfg(feature = "mdx")]
162    MdxJsxFlowElement(JsxElementIndex),
163    #[cfg(feature = "mdx")]
164    MdxJsxTextElement(JsxElementIndex),
165    #[cfg(feature = "mdx")]
166    MdxFlowExpression(CowIndex),
167    #[cfg(feature = "mdx")]
168    MdxTextExpression(CowIndex),
169    #[cfg(feature = "mdx")]
170    MdxEsm(CowIndex),
171}
172
173impl ItemBody {
174    pub(crate) fn is_maybe_inline(&self) -> bool {
175        use ItemBody::*;
176        matches!(
177            *self,
178            MaybeEmphasis(..)
179                | MaybeEmphasisEscaped(..)
180                | MaybeMath(..)
181                | MaybeSmartQuote(..)
182                | MaybeCode(..)
183                | MaybeHtml(..)
184                | MaybeLinkOpen
185                | MaybeLinkClose(..)
186                | MaybeImage
187                | MaybeAutolink(..)
188        )
189    }
190    pub(crate) fn is_block_level(&self) -> bool {
191        !self.is_inline() && !matches!(self, ItemBody::Root)
192    }
193    fn is_inline(&self) -> bool {
194        use ItemBody::*;
195        matches!(
196            *self,
197            MaybeEmphasis(..)
198                | MaybeEmphasisEscaped(..)
199                | MaybeMath(..)
200                | MaybeSmartQuote(..)
201                | MaybeCode(..)
202                | MaybeHtml(..)
203                | MaybeLinkOpen
204                | MaybeLinkClose(..)
205                | MaybeImage
206                | MaybeAutolink(..)
207                | Emphasis
208                | Strong
209                | Strikethrough
210                | Math(..)
211                | Code(..)
212                | Link(..)
213                | Image(..)
214                | FootnoteReference(..)
215                | TaskListMarker(..)
216                | InlineHtml
217                | OwnedInlineHtml(..)
218                | SynthesizeText(..)
219                | SynthesizeChar(..)
220                | Html
221                | Text { .. }
222                | SoftBreak
223                | HardBreak(..)
224        )
225    }
226}
227
228#[derive(Debug)]
229pub struct BrokenLink<'a> {
230    pub span: core::ops::Range<usize>,
231    pub link_type: LinkType,
232    pub reference: CowStr<'a>,
233}
234
235/// Markdown event iterator.
236pub struct Parser<'input, CB = DefaultParserCallbacks> {
237    callbacks: CB,
238    inner: ParserInner<'input>,
239}
240
241// Inner state for `Parser`, extracted so that it can remain generic over the callback without
242// re-compiling complex logic for each instantiation of the generic type.
243pub(crate) struct ParserInner<'input> {
244    pub(crate) text: &'input str,
245    pub(crate) options: Options,
246    pub(crate) tree: Tree<Item>,
247    pub(crate) allocs: Allocations<'input>,
248    html_scan_guard: HtmlScanGuard,
249
250    // https://github.com/pulldown-cmark/pulldown-cmark/issues/844
251    // Consider this example:
252    //
253    //     [x]: xxx...
254    //     [x]
255    //     [x]
256    //     [x]
257    //
258    // Which expands to this HTML:
259    //
260    //     <a href="xxx...">x</a>
261    //     <a href="xxx...">x</a>
262    //     <a href="xxx...">x</a>
263    //
264    // This is quadratic growth, because it's filling in the area of a square.
265    // To prevent this, track how much it's expanded and limit it.
266    link_ref_expansion_limit: usize,
267
268    /// Earliest `(`-title start whose scan hit the block end with no `)`: later starts share that tail.
269    unclosed_paren_title_floor: Cell<usize>,
270
271    /// MDX validation errors collected during inline parsing.
272    pub(crate) mdx_errors: Vec<(usize, String)>,
273
274    // used by inline passes. store them here for reuse
275    inline_stack: InlineStack,
276    link_stack: LinkStack,
277    wikilink_stack: LinkStack,
278    code_delims: CodeDelims,
279    math_delims: MathDelims,
280}
281
282impl<'input, CB> core::fmt::Debug for Parser<'input, CB> {
283    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
284        // Only print the fields that have public types.
285        f.debug_struct("Parser")
286            .field("text", &self.inner.text)
287            .field("options", &self.inner.options)
288            .field("callbacks", &..)
289            .finish()
290    }
291}
292
293impl<'a> BrokenLink<'a> {
294    /// Moves the link into version with a static lifetime.
295    ///
296    /// The `reference` member is cloned to a Boxed or Inline version.
297    pub fn into_static(self) -> BrokenLink<'static> {
298        BrokenLink {
299            span: self.span.clone(),
300            link_type: self.link_type,
301            reference: self.reference.into_string().into(),
302        }
303    }
304}
305
306impl<'input> Parser<'input, DefaultParserCallbacks> {
307    /// Creates a new event iterator for a markdown string without any options enabled.
308    pub fn new(text: &'input str) -> Self {
309        Self::new_ext(text, Options::empty())
310    }
311
312    /// Creates a new event iterator for a markdown string with given options.
313    pub fn new_ext(text: &'input str, options: Options) -> Self {
314        Self::new_with_callbacks(text, options, DefaultParserCallbacks)
315    }
316}
317
318impl<'input, CB: ParserCallbacks<'input>> Parser<'input, CB> {
319    /// Creates a new event iterator for markdown text with given options and callbacks.
320    ///
321    /// ```
322    /// # use satteri_pulldown_cmark::{BrokenLink, CowStr, Event, Options, Parser, ParserCallbacks, Tag};
323    /// struct CustomCallbacks;
324    /// impl<'input> ParserCallbacks<'input> for CustomCallbacks {
325    ///     fn handle_broken_link(
326    ///         &mut self,
327    ///         link: BrokenLink<'input>,
328    ///     ) -> Option<(CowStr<'input>, CowStr<'input>)> {
329    ///         Some(("https://target".into(), link.reference))
330    ///     }
331    /// }
332    ///
333    /// let mut parser =
334    ///     Parser::new_with_callbacks("[broken]", Options::empty(), CustomCallbacks);
335    ///
336    /// assert!(matches!(
337    ///     parser.nth(1),
338    ///     Some(Event::Start(Tag::Link { .. }))
339    /// ));
340    /// ```
341    ///
342    /// See the [`ParserCallbacks`] trait for a list of callbacks that can be overridden.
343    pub fn new_with_callbacks(text: &'input str, options: Options, callbacks: CB) -> Self {
344        let text = crate::strip_leading_bom(text);
345        let (mut tree, allocs, _firstpass_mdx_errors) = run_first_pass(text, options);
346        tree.reset();
347        let inline_stack = Default::default();
348        let link_stack = Default::default();
349        let wikilink_stack = Default::default();
350        let html_scan_guard = Default::default();
351        Parser {
352            callbacks,
353
354            inner: ParserInner {
355                text,
356                options,
357                tree,
358                allocs,
359                inline_stack,
360                link_stack,
361                wikilink_stack,
362                html_scan_guard,
363                // always allow 100KiB
364                link_ref_expansion_limit: text.len().max(100_000),
365                unclosed_paren_title_floor: Cell::new(usize::MAX),
366                mdx_errors: Vec::new(),
367                code_delims: CodeDelims::new(),
368                math_delims: MathDelims::new(),
369            },
370        }
371    }
372
373    /// Returns a reference to the internal `RefDefs` object, which provides access
374    /// to the internal map of reference definitions.
375    pub fn reference_definitions(&self) -> &RefDefs<'_> {
376        &self.inner.allocs.refdefs
377    }
378
379    /// Returns MDX validation errors collected during parsing.
380    /// Only populated when [`Options::ENABLE_MDX`] is active.
381    pub fn mdx_errors(&self) -> &[(usize, String)] {
382        &self.inner.mdx_errors
383    }
384
385    /// Consumes the event iterator and produces an iterator that produces
386    /// `(Event, Range)` pairs, where the `Range` value maps to the corresponding
387    /// range in the markdown source.
388    pub fn into_offset_iter(self) -> OffsetIter<'input, CB> {
389        OffsetIter { parser: self }
390    }
391}
392
393impl<'input, F> Parser<'input, BrokenLinkCallback<F>> {
394    /// In case the parser encounters any potential links that have a broken
395    /// reference (e.g `[foo]` when there is no `[foo]: ` entry at the bottom)
396    /// the provided callback will be called with the reference name,
397    /// and the returned pair will be used as the link URL and title if it is not
398    /// `None`.
399    ///
400    /// This constructor is provided for backwards compatibility.
401    /// This and other callbacks can also be customized with [`Parser::new_with_callbacks`].
402    pub fn new_with_broken_link_callback(
403        text: &'input str,
404        options: Options,
405        broken_link_callback: Option<F>,
406    ) -> Self
407    where
408        F: FnMut(BrokenLink<'input>) -> Option<(CowStr<'input>, CowStr<'input>)>,
409    {
410        Self::new_with_callbacks(text, options, BrokenLinkCallback(broken_link_callback))
411    }
412}
413
414impl<'input> ParserInner<'input> {
415    pub(crate) fn new(text: &'input str, options: Options) -> Self {
416        let (mut tree, allocs, firstpass_mdx_errors) = run_first_pass(text, options);
417        tree.reset();
418        ParserInner {
419            text,
420            options,
421            tree,
422            allocs,
423            inline_stack: Default::default(),
424            link_stack: Default::default(),
425            wikilink_stack: Default::default(),
426            html_scan_guard: Default::default(),
427            link_ref_expansion_limit: text.len().max(100_000),
428            unclosed_paren_title_floor: Cell::new(usize::MAX),
429            mdx_errors: firstpass_mdx_errors,
430            code_delims: CodeDelims::new(),
431            math_delims: MathDelims::new(),
432        }
433    }
434
435    /// Use a link label to fetch a type, url, and title.
436    ///
437    /// This function enforces the [`link_ref_expansion_limit`].
438    /// If it returns Some, it also consumes some of the fuel.
439    /// If we're out of fuel, it immediately returns None.
440    ///
441    /// The URL and title are found in the [`RefDefs`] map.
442    /// If they're not there, and a callback was provided by the user,
443    /// `handle_broken_link` will be invoked and given the opportunity
444    /// to provide a fallback.
445    ///
446    /// The link type (that's "link" or "image") depends on the usage site, and
447    /// is provided by the caller of this function.
448    /// This function returns a new one because, if it has to invoke a callback
449    /// to find the information, the link type is [mapped to an unknown type].
450    ///
451    /// [mapped to an unknown type]: crate::LinkType::to_unknown
452    /// [`link_ref_expansion_limit`]: Self::link_ref_expansion_limit
453    fn fetch_link_type_url_title(
454        &mut self,
455        link_label: CowStr<'input>,
456        span: Range<usize>,
457        link_type: LinkType,
458        callbacks: &mut dyn ParserCallbacks<'input>,
459    ) -> Option<(LinkType, CowStr<'input>, CowStr<'input>)> {
460        if self.link_ref_expansion_limit == 0 {
461            return None;
462        }
463
464        let (link_type, url, title) = self
465            .allocs
466            .refdefs
467            .get(link_label.as_ref())
468            .map(|matching_def| {
469                // found a matching definition!
470                let title = matching_def
471                    .title
472                    .as_ref()
473                    .cloned()
474                    .unwrap_or_else(|| "".into());
475                let url = matching_def.dest.clone();
476                (link_type, url, title)
477            })
478            .or_else(|| {
479                // Construct a BrokenLink struct, which will be passed to the callback
480                let broken_link = BrokenLink {
481                    span,
482                    link_type,
483                    reference: link_label,
484                };
485
486                callbacks
487                    .handle_broken_link(broken_link)
488                    .map(|(url, title)| (link_type.to_unknown(), url, title))
489            })?;
490
491        // Limit expansion from link references.
492        // This isn't a problem for footnotes, because multiple references to the same one
493        // reuse the same node, but links/images get their HREF/SRC copied.
494        self.link_ref_expansion_limit = self
495            .link_ref_expansion_limit
496            .saturating_sub(url.len() + title.len());
497
498        Some((link_type, url, title))
499    }
500
501    /// Handle inline markup.
502    ///
503    /// When the parser encounters any item indicating potential inline markup, all
504    /// inline markup passes are run on the remainder of the chain.
505    ///
506    /// Note: there's some potential for optimization here, but that's future work.
507    pub(crate) fn handle_inline(&mut self, callbacks: &mut dyn ParserCallbacks<'input>) {
508        self.handle_inline_pass1(callbacks);
509        // Resolve attention (emphasis/strong) and strikethrough/sub/sup. Two
510        // delimiter families that can cross, so the resolve order matters,
511        // mirroring micromark:
512        //   * At the top level, the family whose marker is *tokenized first*
513        //     resolves first (`resolveAllConstructs` registration order):
514        //     `*~bar~*` → emphasis first; `~_~:_<` → strikethrough first.
515        //     An inert leading marker still counts (it still tokenizes), so
516        //     `_ ~a*b~*` resolves emphasis first even though `_` can't pair.
517        //   * Inside any already-formed span (a link/image label, or the
518        //     content between matched delimiters), micromark uses a fixed
519        //     `insideSpan.null = [strikethrough, attention]`, i.e. *always
520        //     strikethrough first*, independent of the top-level order. So
521        //     `[_~a*b~*](/x)` resolves strikethrough first in the label even
522        //     though `_ ~a*b~*` resolves emphasis first at the top level.
523        let st_enabled = self.options.contains(Options::ENABLE_STRIKETHROUGH)
524            || self.options.contains(Options::ENABLE_SUBSCRIPT)
525            || self.options.contains(Options::ENABLE_SUPERSCRIPT);
526        if !st_enabled {
527            self.handle_emphasis_pass();
528            return;
529        }
530        // The top-level order decision must see the whole inline scope from its
531        // first child (which includes leading inert markers), not from `cur()`:
532        // pass1 leaves `cur` at the first unresolved marker, past leading text
533        // an inert marker might hide behind.
534        let scope_first = self
535            .tree
536            .peek_up()
537            .and_then(|p| self.tree[p].child)
538            .or_else(|| self.tree.cur());
539        let strikethrough_first = matches!(
540            self.first_inline_marker_char(scope_first),
541            Some(b'~') | Some(b'^')
542        );
543        self.resolve_inline_scope(self.tree.cur(), strikethrough_first);
544    }
545
546    /// Resolve emphasis and strikethrough/sub/sup at this scope in the given
547    /// order, then descend into each formed span. Nested content is always
548    /// resolved strikethrough-first (micromark's `insideSpan.null`).
549    fn resolve_inline_scope(&mut self, start: Option<TreeIndex>, strikethrough_first: bool) {
550        if strikethrough_first {
551            self.resolve_tildes_carets_in_scope(start, false);
552            self.resolve_emphasis_at_scope(start);
553        } else {
554            self.resolve_emphasis_at_scope(start);
555            self.resolve_tildes_carets_in_scope(start, false);
556        }
557        let mut cur = start;
558        while let Some(cur_ix) = cur {
559            let next = self.tree[cur_ix].next;
560            if matches!(
561                self.tree[cur_ix].item.body,
562                ItemBody::Emphasis
563                    | ItemBody::Strong
564                    | ItemBody::Strikethrough
565                    | ItemBody::Subscript
566                    | ItemBody::Superscript
567                    | ItemBody::Link(_)
568                    | ItemBody::Image(_)
569            ) {
570                let child = self.tree[cur_ix].child;
571                self.resolve_inline_scope(child, true);
572            }
573            cur = next;
574        }
575    }
576
577    /// Resolve `*`/`_` at this scope only (no descent), with a fresh
578    /// `inline_stack`.
579    fn resolve_emphasis_at_scope(&mut self, start: Option<TreeIndex>) {
580        let saved = core::mem::take(&mut self.inline_stack);
581        self.handle_emphasis_in_scope(start);
582        self.inline_stack = saved;
583    }
584
585    /// Find the first emphasis/strikethrough marker (`*` `_` `~` `^`) in
586    /// `start..`, in source order. Used to pick the resolve order between the
587    /// two families: micromark registers a family's `resolveAll` when its
588    /// marker is *tokenized*, so an inert marker (one that can neither open
589    /// nor close, e.g. `_` before a space) still counts even though satteri
590    /// leaves it as plain text rather than a `MaybeEmphasis`. Scan text nodes
591    /// too, not just `MaybeEmphasis`, or `_ ~a*b~*` would wrongly resolve the
592    /// `~` first (the inert leading `_` is the real first marker).
593    fn first_inline_marker_char(&self, start: Option<TreeIndex>) -> Option<u8> {
594        // Only count markers for *enabled* families, matching which delimiters
595        // the reference actually tokenizes. `~` counts only with strikethrough
596        // or subscript on; `^` only with superscript on. Otherwise a literal
597        // `^` (or `~`) would wrongly skew the emphasis-vs-strikethrough order.
598        let tilde = self.options.contains(Options::ENABLE_STRIKETHROUGH)
599            || self.options.contains(Options::ENABLE_SUBSCRIPT);
600        let caret = self.options.contains(Options::ENABLE_SUPERSCRIPT);
601        let is_marker =
602            |c: u8| matches!(c, b'*' | b'_') || (c == b'~' && tilde) || (c == b'^' && caret);
603        let bytes = self.text.as_bytes();
604        let mut cur = start;
605        while let Some(cur_ix) = cur {
606            match self.tree[cur_ix].item.body {
607                ItemBody::MaybeEmphasis(..) => {
608                    let c = bytes[self.tree[cur_ix].item.start];
609                    if is_marker(c) {
610                        return Some(c);
611                    }
612                }
613                ItemBody::Text { backslash_escaped } => {
614                    let item = &self.tree[cur_ix].item;
615                    // A backslash-escaped leading byte (the `\` was stripped)
616                    // is not a marker micromark would tokenize.
617                    let from = item.start + usize::from(backslash_escaped);
618                    if let Some(off) = bytes[from..item.end].iter().position(|&c| is_marker(c)) {
619                        return Some(bytes[from + off]);
620                    }
621                }
622                _ => {}
623            }
624            cur = self.tree[cur_ix].next;
625        }
626        None
627    }
628
629    /// Recursive emphasis pass. Processes `*`/`_` MaybeEmphasis at this
630    /// scope, then descends into any inline containers (Emphasis,
631    /// Strong, Strikethrough, Link, Image, etc.) to do the same in
632    /// their children.
633    fn handle_emphasis_pass(&mut self) {
634        let start = self.tree.cur();
635        self.resolve_emphasis_recursive(start);
636    }
637
638    fn resolve_emphasis_recursive(&mut self, start: Option<TreeIndex>) {
639        // Save and reset the shared inline_stack so each scope works
640        // with a fresh one. Smart-quote state is local to
641        // `handle_emphasis_in_scope`, no save needed.
642        let saved = core::mem::take(&mut self.inline_stack);
643        self.handle_emphasis_in_scope(start);
644        self.inline_stack = saved;
645
646        let mut cur = start;
647        while let Some(cur_ix) = cur {
648            let next = self.tree[cur_ix].next;
649            match self.tree[cur_ix].item.body {
650                ItemBody::Emphasis
651                | ItemBody::Strong
652                | ItemBody::Strikethrough
653                | ItemBody::Subscript
654                | ItemBody::Superscript
655                | ItemBody::Link(_)
656                | ItemBody::Image(_) => {
657                    let child = self.tree[cur_ix].child;
658                    self.resolve_emphasis_recursive(child);
659                }
660                _ => {}
661            }
662            cur = next;
663        }
664    }
665
666    /// Handle inline HTML, code spans, and links.
667    ///
668    /// This function handles both inline HTML and code spans, because they have
669    /// the same precedence. It also handles links, even though they have lower
670    /// precedence, because the URL of links must not be processed.
671    fn handle_inline_pass1(&mut self, callbacks: &mut dyn ParserCallbacks<'input>) {
672        let mut cur = self.tree.cur();
673        let mut prev = None;
674
675        let block_end = self.tree[self.tree.peek_up().unwrap()].item.end;
676        let block_text = &self.text[..block_end];
677        self.unclosed_paren_title_floor.set(usize::MAX);
678
679        while let Some(mut cur_ix) = cur {
680            match self.tree[cur_ix].item.body {
681                ItemBody::MaybeHtml(preceded_by_backslash) => {
682                    if preceded_by_backslash {
683                        // No autolink claimed the `\`, so the `<` is literal.
684                        self.tree[cur_ix].item.body = ItemBody::Text {
685                            backslash_escaped: true,
686                        };
687                        prev = cur;
688                        cur = self.tree[cur_ix].next;
689                        continue;
690                    }
691                    // MDX inline JSX: check before HTML
692                    #[cfg(feature = "mdx")]
693                    if self.options.contains(Options::ENABLE_MDX) {
694                        let start = self.tree[cur_ix].item.start;
695                        let next_byte = block_text.as_bytes().get(start + 1).copied();
696
697                        // In MDX, `<!` is not valid (no HTML comments).
698                        if next_byte == Some(b'!') {
699                            self.mdx_errors.push((
700                                start,
701                                "Unexpected character `!` (U+0021) before name, expected a \
702                                 character that can start a name, such as a letter, `$`, or `_` \
703                                 (note: to create a comment in MDX, use `{/* text */}`)"
704                                    .to_string(),
705                            ));
706                            self.tree[cur_ix].item.body = ItemBody::Text {
707                                backslash_escaped: false,
708                            };
709                            prev = cur;
710                            cur = self.tree[cur_ix].next;
711                            continue;
712                        }
713
714                        if let Some(total_len) =
715                            scan_mdx_inline_jsx(&block_text.as_bytes()[start..])
716                        {
717                            let end = start + total_len;
718                            let node = scan_nodes_to_ix(&self.tree, self.tree[cur_ix].next, end);
719                            let raw = &block_text[start..end];
720                            let col = crate::mdx::column_at(block_text.as_bytes(), start);
721                            let jsx_data = crate::mdx::parse_jsx_tag_with_column(raw, col, 0);
722                            let mut allocator = oxc_allocator::Allocator::default();
723                            crate::mdx::validate_jsx_expressions(
724                                raw,
725                                &jsx_data.attrs,
726                                |rel| start + rel,
727                                &mut allocator,
728                                &mut self.mdx_errors,
729                            );
730                            let jsx_ix = self.allocs.allocate_jsx_element(jsx_data);
731                            self.tree[cur_ix].item.body = ItemBody::MdxJsxTextElement(jsx_ix);
732                            self.tree[cur_ix].item.end = end;
733                            self.tree[cur_ix].next = node;
734                            prev = cur;
735                            cur = node;
736                            if let Some(node_ix) = cur {
737                                self.tree[node_ix].item.start =
738                                    max(self.tree[node_ix].item.start, end);
739                            }
740                            continue;
741                        }
742
743                        // mdx-js fallback rule:
744                        //   `<` + space/tab → always literal `<` (text).
745                        //   `<` + newline   → JSX tag may span lines; treat
746                        //                      as text only if the next
747                        //                      non-whitespace byte is benign
748                        //                      (not `>`, not EOF/blank-line)
749                        //                      AND the line containing it
750                        //                      isn't a setext underline
751                        //                      (`-`+ or `=`+), which would
752                        //                      promote the `<` into a heading
753                        //                      whose JSX validation fails.
754                        //   `<` + anything else (incl. EOF) → parse error
755                        //                      (`<\`, `<,`, `<{`, `<<`, `<.`,
756                        //                       …).
757                        let bytes_block = block_text.as_bytes();
758                        let is_text_fallback = match next_byte {
759                            Some(b' ' | b'\t') => true,
760                            Some(b'\n' | b'\r') => {
761                                // Skip whitespace + container prefixes when
762                                // probing for the first significant byte
763                                // after `\n`. A `>` at line start inside a
764                                // blockquote is the container marker, not a
765                                // JSX-like delimiter.
766                                let bq_depth = self
767                                    .tree
768                                    .walk_spine()
769                                    .filter(|&&ix| {
770                                        matches!(self.tree[ix].item.body, ItemBody::BlockQuote(..))
771                                    })
772                                    .count();
773                                let mut probe = start + 1;
774                                loop {
775                                    while probe < bytes_block.len()
776                                        && matches!(
777                                            bytes_block[probe],
778                                            b' ' | b'\t' | b'\n' | b'\r'
779                                        )
780                                    {
781                                        probe += 1;
782                                    }
783                                    if bq_depth == 0
784                                        || probe >= bytes_block.len()
785                                        || bytes_block[probe] != b'>'
786                                    {
787                                        break;
788                                    }
789                                    let mut consumed = 0;
790                                    while consumed < bq_depth
791                                        && probe < bytes_block.len()
792                                        && bytes_block[probe] == b'>'
793                                    {
794                                        probe += 1;
795                                        if probe < bytes_block.len() && bytes_block[probe] == b' ' {
796                                            probe += 1;
797                                        }
798                                        consumed += 1;
799                                    }
800                                }
801                                if probe >= bytes_block.len() || bytes_block[probe] == b'>' {
802                                    false
803                                } else {
804                                    // Reject if `probe`'s line is a setext
805                                    // underline (only `-` or only `=`, then
806                                    // optional whitespace to EOL/EOF) AND
807                                    // would actually promote the `<`-line
808                                    // to a heading. Inside a blockquote
809                                    // container the underline line is
810                                    // typically a lazy continuation (no
811                                    // `>` prefix) and doesn't promote, so
812                                    // skip the rejection.
813                                    let underline_char = bytes_block[probe];
814                                    if !matches!(underline_char, b'-' | b'=') {
815                                        true
816                                    } else {
817                                        let mut q = probe;
818                                        while q < bytes_block.len()
819                                            && bytes_block[q] == underline_char
820                                        {
821                                            q += 1;
822                                        }
823                                        while q < bytes_block.len()
824                                            && matches!(bytes_block[q], b' ' | b'\t')
825                                        {
826                                            q += 1;
827                                        }
828                                        let at_eol = q >= bytes_block.len()
829                                            || matches!(bytes_block[q], b'\n' | b'\r');
830                                        if !at_eol {
831                                            true
832                                        } else {
833                                            // Container check: a blockquote
834                                            // `>` (possibly after up to 3
835                                            // spaces) on the line opening
836                                            // the `<` means the underline
837                                            // line would need the same
838                                            // prefix to actually promote a
839                                            // setext heading. Without it,
840                                            // the underline is lazy
841                                            // paragraph continuation, so
842                                            // accept as text.
843                                            //
844                                            // Same for listitems: if the
845                                            // spine has a ListItem and the
846                                            // underline line starts at a
847                                            // column less than the listitem
848                                            // content column, it's lazy
849                                            // continuation and doesn't
850                                            // promote — accept as text.
851                                            let mut ls = start;
852                                            while ls > 0
853                                                && !matches!(bytes_block[ls - 1], b'\n' | b'\r')
854                                            {
855                                                ls -= 1;
856                                            }
857                                            let mut k = ls;
858                                            let mut sp = 0;
859                                            while k < start && bytes_block[k] == b' ' && sp < 3 {
860                                                k += 1;
861                                                sp += 1;
862                                            }
863                                            if k < start && bytes_block[k] == b'>' {
864                                                true
865                                            } else {
866                                                // Underline line start.
867                                                let mut us = probe;
868                                                while us > 0
869                                                    && !matches!(bytes_block[us - 1], b'\n' | b'\r')
870                                                {
871                                                    us -= 1;
872                                                }
873                                                let mut underline_col = 0;
874                                                let mut uk = us;
875                                                while uk < probe && bytes_block[uk] == b' ' {
876                                                    uk += 1;
877                                                    underline_col += 1;
878                                                }
879                                                let listitem_indent = self
880                                                    .tree
881                                                    .walk_spine()
882                                                    .filter_map(|&ix| {
883                                                        match self.tree[ix].item.body {
884                                                            ItemBody::ListItem(indent, _) => {
885                                                                Some(indent)
886                                                            }
887                                                            _ => None,
888                                                        }
889                                                    })
890                                                    .next();
891                                                let in_blockquote =
892                                                    self.tree.walk_spine().any(|&ix| {
893                                                        matches!(
894                                                            self.tree[ix].item.body,
895                                                            ItemBody::BlockQuote(..)
896                                                        )
897                                                    });
898                                                // BlockQuote container: an
899                                                // underline line missing the
900                                                // `>` prefix is lazy
901                                                // continuation and doesn't
902                                                // promote. Detect by checking
903                                                // the underline line's source
904                                                // (not block_text, which has
905                                                // already stripped the
906                                                // prefix).
907                                                let bq_lazy = if in_blockquote {
908                                                    underline_col < 1
909                                                        || !bytes_block[us..probe].contains(&b'>')
910                                                } else {
911                                                    false
912                                                };
913                                                matches!(listitem_indent, Some(i) if underline_col < i)
914                                                    || bq_lazy
915                                            }
916                                        }
917                                    }
918                                }
919                            }
920                            _ => false,
921                        };
922                        if !is_text_fallback {
923                            self.mdx_errors.push((
924                                start,
925                                "Unexpected character after `<`, expected a valid JSX tag \
926                                 (note: to create a link in MDX, use `[text](url)`)"
927                                    .to_string(),
928                            ));
929                        }
930
931                        self.tree[cur_ix].item.body = ItemBody::Text {
932                            backslash_escaped: false,
933                        };
934                        prev = cur;
935                        cur = self.tree[cur_ix].next;
936                        continue;
937                    }
938
939                    let next = self.tree[cur_ix].next;
940                    let autolink = if let Some(next_ix) = next {
941                        scan_autolink(block_text, self.tree[next_ix].item.start)
942                    } else {
943                        None
944                    };
945
946                    if let Some((ix, uri, link_type)) = autolink {
947                        let node = scan_nodes_to_ix(&self.tree, next, ix);
948                        let text_node = self.tree.create_node(Item {
949                            start: self.tree[cur_ix].item.start + 1,
950                            end: ix - 1,
951                            body: ItemBody::Text {
952                                backslash_escaped: false,
953                            },
954                        });
955                        let link_ix =
956                            self.allocs
957                                .allocate_link(link_type, uri, "".into(), "".into());
958                        self.tree[cur_ix].item.body = ItemBody::Link(link_ix);
959                        self.tree[cur_ix].item.end = ix;
960                        self.tree[cur_ix].next = node;
961                        self.tree[cur_ix].child = Some(text_node);
962                        prev = cur;
963                        cur = node;
964                        if let Some(node_ix) = cur {
965                            let orig_start = self.tree[node_ix].item.start;
966                            let new_start = max(orig_start, ix);
967                            self.tree[node_ix].item.start = new_start;
968                            // When the autolink's closing `>` consumed the byte
969                            // that was the target of a preceding `\` escape,
970                            // the trailing text's `backslash_escaped` flag is
971                            // stale — clear it so arena_build doesn't extend
972                            // the text node's source span back over bytes the
973                            // link now owns. Mirrors the inline-link fix.
974                            if new_start > orig_start
975                                && let ItemBody::Text { backslash_escaped } =
976                                    &mut self.tree[node_ix].item.body
977                            {
978                                *backslash_escaped = false;
979                            }
980                        }
981                        continue;
982                    } else {
983                        let inline_html = next.and_then(|next_ix| {
984                            self.scan_inline_html(
985                                block_text.as_bytes(),
986                                self.tree[next_ix].item.start,
987                            )
988                        });
989                        if let Some((span, ix)) = inline_html {
990                            let node = scan_nodes_to_ix(&self.tree, next, ix);
991                            self.tree[cur_ix].item.body = if !span.is_empty() {
992                                let converted_string =
993                                    String::from_utf8(span).expect("invalid utf8");
994                                ItemBody::OwnedInlineHtml(
995                                    self.allocs.allocate_cow(converted_string.into()),
996                                )
997                            } else {
998                                ItemBody::InlineHtml
999                            };
1000                            self.tree[cur_ix].item.end = ix;
1001                            self.tree[cur_ix].next = node;
1002                            prev = cur;
1003                            cur = node;
1004                            if let Some(node_ix) = cur {
1005                                let orig_start = self.tree[node_ix].item.start;
1006                                let new_start = max(orig_start, ix);
1007                                self.tree[node_ix].item.start = new_start;
1008                                // Inline HTML may consume bytes that a `\X`
1009                                // escape was attached to (e.g. `\*` inside
1010                                // an attribute value). Clear the stale flag
1011                                // so arena_build doesn't extend the trail
1012                                // back over bytes the HTML now owns.
1013                                if new_start > orig_start
1014                                    && let ItemBody::Text { backslash_escaped } =
1015                                        &mut self.tree[node_ix].item.body
1016                                {
1017                                    *backslash_escaped = false;
1018                                }
1019                            }
1020                            continue;
1021                        }
1022                    }
1023                    self.tree[cur_ix].item.body = ItemBody::Text {
1024                        backslash_escaped: false,
1025                    };
1026                }
1027                ItemBody::MaybeMath(preceded_by_backslash, _brace_context) => {
1028                    if preceded_by_backslash {
1029                        self.tree[cur_ix].item.body = ItemBody::Text {
1030                            backslash_escaped: true,
1031                        };
1032                        prev = cur;
1033                        cur = self.tree[cur_ix].next;
1034                        continue;
1035                    }
1036                    // Count consecutive $ from the opening position
1037                    let mut open_count = 1usize;
1038                    let mut open_end = cur_ix;
1039                    {
1040                        let mut peek = self.tree[cur_ix].next;
1041                        while let Some(peek_ix) = peek {
1042                            if matches!(self.tree[peek_ix].item.body, ItemBody::MaybeMath(..))
1043                                && self.tree[peek_ix].item.start == self.tree[open_end].item.end
1044                            {
1045                                open_count += 1;
1046                                open_end = peek_ix;
1047                                peek = self.tree[peek_ix].next;
1048                            } else {
1049                                break;
1050                            }
1051                        }
1052                    }
1053
1054                    // Single- and multi-dollar math can be toggled
1055                    // independently (mirroring remark-math's
1056                    // `singleDollarTextMath`). When this run's length isn't
1057                    // an enabled delimiter, the `$` is literal text — so
1058                    // prose like `$50 to $100` never becomes a math span.
1059                    let count_enabled = if open_count == 1 {
1060                        self.options.contains(Options::ENABLE_MATH_SINGLE_DOLLAR)
1061                    } else {
1062                        self.options.contains(Options::ENABLE_MATH_MULTI_DOLLAR)
1063                    };
1064                    if !count_enabled {
1065                        let mut text_ix = cur_ix;
1066                        loop {
1067                            self.tree[text_ix].item.body = ItemBody::Text {
1068                                backslash_escaped: false,
1069                            };
1070                            if text_ix == open_end {
1071                                break;
1072                            }
1073                            match self.tree[text_ix].next {
1074                                Some(next) => text_ix = next,
1075                                None => break,
1076                            }
1077                        }
1078                        prev = cur;
1079                        cur = self.tree[cur_ix].next;
1080                        continue;
1081                    }
1082
1083                    // Scan forward for a matching run of the same count
1084                    let mut scan = self.tree[open_end].next;
1085                    let mut close_ix = None;
1086                    while let Some(scan_ix) = scan {
1087                        if matches!(self.tree[scan_ix].item.body, ItemBody::MaybeMath(..)) {
1088                            let mut run = 1usize;
1089                            let mut run_end = scan_ix;
1090                            let mut peek = self.tree[scan_ix].next;
1091                            while let Some(peek_ix) = peek {
1092                                if matches!(self.tree[peek_ix].item.body, ItemBody::MaybeMath(..))
1093                                    && self.tree[peek_ix].item.start == self.tree[run_end].item.end
1094                                {
1095                                    run += 1;
1096                                    run_end = peek_ix;
1097                                    peek = self.tree[peek_ix].next;
1098                                } else {
1099                                    break;
1100                                }
1101                            }
1102                            if run == open_count {
1103                                close_ix = Some(scan_ix);
1104                                break;
1105                            }
1106                            // Skip past this non-matching run
1107                            scan = self.tree[run_end].next;
1108                            continue;
1109                        }
1110                        scan = self.tree[scan_ix].next;
1111                    }
1112
1113                    if let Some(scan_ix) = close_ix {
1114                        self.make_math_span(cur_ix, scan_ix);
1115                    } else {
1116                        let mut fail_ix = cur_ix;
1117                        loop {
1118                            self.tree[fail_ix].item.body = ItemBody::Text {
1119                                backslash_escaped: false,
1120                            };
1121                            if fail_ix == open_end {
1122                                break;
1123                            }
1124                            if let Some(next) = self.tree[fail_ix].next {
1125                                fail_ix = next;
1126                            } else {
1127                                break;
1128                            }
1129                        }
1130                    }
1131                }
1132                ItemBody::MaybeCode(mut search_count, preceded_by_backslash) => {
1133                    if preceded_by_backslash {
1134                        search_count -= 1;
1135                        if search_count == 0 {
1136                            self.tree[cur_ix].item.body = ItemBody::Text {
1137                                backslash_escaped: true,
1138                            };
1139                            prev = cur;
1140                            cur = self.tree[cur_ix].next;
1141                            continue;
1142                        }
1143                    }
1144
1145                    if self.code_delims.is_populated() {
1146                        // we have previously scanned all codeblock delimiters,
1147                        // so we can reuse that work
1148                        if let Some(scan_ix) = self.code_delims.find(cur_ix, search_count) {
1149                            self.make_code_span(cur_ix, scan_ix, preceded_by_backslash);
1150                        } else {
1151                            self.tree[cur_ix].item.body = ItemBody::Text {
1152                                backslash_escaped: preceded_by_backslash,
1153                            };
1154                        }
1155                    } else {
1156                        // we haven't previously scanned all codeblock delimiters,
1157                        // so walk the AST
1158                        let mut scan = if search_count > 0 {
1159                            self.tree[cur_ix].next
1160                        } else {
1161                            None
1162                        };
1163                        while let Some(scan_ix) = scan {
1164                            if let ItemBody::MaybeCode(delim_count, _) =
1165                                self.tree[scan_ix].item.body
1166                            {
1167                                if search_count == delim_count {
1168                                    self.make_code_span(cur_ix, scan_ix, preceded_by_backslash);
1169                                    self.code_delims.clear();
1170                                    break;
1171                                } else {
1172                                    self.code_delims.insert(delim_count, scan_ix);
1173                                }
1174                            }
1175                            scan = self.tree[scan_ix].next;
1176                        }
1177                        if scan.is_none() {
1178                            self.tree[cur_ix].item.body = ItemBody::Text {
1179                                backslash_escaped: preceded_by_backslash,
1180                            };
1181                        }
1182                    }
1183                }
1184                ItemBody::MaybeAutolink(cand_ix) => {
1185                    // An unresolved bracket opener blocks the construct, and
1186                    // the stack holds exactly those.
1187                    let next = self.tree[cur_ix].next;
1188                    if !self.link_stack.is_empty() {
1189                        // Zero-width `Text` rather than an unlink: the emphasis
1190                        // resolver addresses nodes by arena index, so dropping
1191                        // one from the chain would hand it whatever came next.
1192                        self.tree[cur_ix].item.body = ItemBody::Text {
1193                            backslash_escaped: false,
1194                        };
1195                        prev = cur;
1196                        cur = next;
1197                        continue;
1198                    }
1199                    // Reusing the marker node as the `Link` keeps the preceding
1200                    // sibling's `next` pointer valid.
1201                    let cand = self.allocs[cand_ix];
1202                    let node_after = scan_nodes_to_ix(&self.tree, next, cand.end);
1203                    let text_child = self.tree.create_node(Item {
1204                        start: cand.start,
1205                        end: cand.end,
1206                        body: ItemBody::Text {
1207                            backslash_escaped: false,
1208                        },
1209                    });
1210                    self.tree[cur_ix].item = Item {
1211                        start: cand.start,
1212                        end: cand.end,
1213                        body: ItemBody::Link(cand.link),
1214                    };
1215                    self.tree[cur_ix].child = Some(text_child);
1216                    self.tree[cur_ix].next = node_after;
1217                    if let Some(node_after_ix) = node_after {
1218                        let orig_start = self.tree[node_after_ix].item.start;
1219                        let new_start = max(orig_start, cand.end);
1220                        // A `\` the first pass read as a hard-break marker
1221                        // turns out to be the URL's last byte.
1222                        if orig_start < cand.end
1223                            && matches!(
1224                                self.tree[node_after_ix].item.body,
1225                                ItemBody::HardBreak(true)
1226                            )
1227                        {
1228                            self.tree[node_after_ix].item.body = ItemBody::SoftBreak;
1229                        }
1230                        // The clamp below can't trim an item that carries its
1231                        // own content: a character reference the URL ends
1232                        // inside would still emit its decoded value.
1233                        if orig_start < cand.end
1234                            && matches!(
1235                                self.tree[node_after_ix].item.body,
1236                                ItemBody::SynthesizeText(..)
1237                            )
1238                        {
1239                            self.tree[node_after_ix].item.body = ItemBody::Text {
1240                                backslash_escaped: false,
1241                            };
1242                        }
1243                        self.tree[node_after_ix].item.start = new_start;
1244                        // The `\` a flag refers to sits one byte before its
1245                        // item, so the link owns it here. A surviving flag
1246                        // would stretch the successor's span back over it, and
1247                        // on a `<` keep the inline HTML from opening.
1248                        if orig_start <= cand.end {
1249                            match &mut self.tree[node_after_ix].item.body {
1250                                ItemBody::Text { backslash_escaped }
1251                                | ItemBody::MaybeHtml(backslash_escaped) => {
1252                                    *backslash_escaped = false;
1253                                }
1254                                _ => {}
1255                            }
1256                        }
1257                        self.repair_construct_after_url_end(cand.end, node_after_ix);
1258                    }
1259                }
1260                ItemBody::MaybeEmphasisEscaped(count, ..) => {
1261                    // Nothing unblocked it, so the escape stands: the delimiter
1262                    // it hid is literal and the run after it is one shorter.
1263                    self.tree[cur_ix].item.body = ItemBody::Text {
1264                        backslash_escaped: true,
1265                    };
1266                    let c = self.text.as_bytes()[self.tree[cur_ix].item.start];
1267                    if !crate::firstpass::delim_run_is_valid(c, count - 1, self.options) {
1268                        let mut scan = self.tree[cur_ix].next;
1269                        for _ in 1..count {
1270                            let Some(next_ix) = scan else { break };
1271                            self.tree[next_ix].item.body = ItemBody::Text {
1272                                backslash_escaped: false,
1273                            };
1274                            scan = self.tree[next_ix].next;
1275                        }
1276                    }
1277                }
1278                ItemBody::MaybeLinkOpen => {
1279                    self.tree[cur_ix].item.body = ItemBody::Text {
1280                        backslash_escaped: false,
1281                    };
1282                    let link_open_doubled = self.tree[cur_ix]
1283                        .next
1284                        .map(|ix| self.tree[ix].item.body == ItemBody::MaybeLinkOpen)
1285                        .unwrap_or(false);
1286                    if self.options.contains(Options::ENABLE_WIKILINKS) && link_open_doubled {
1287                        self.wikilink_stack.push(LinkStackEl {
1288                            node: cur_ix,
1289                            ty: LinkStackTy::Link,
1290                        });
1291                    }
1292                    self.link_stack.push(LinkStackEl {
1293                        node: cur_ix,
1294                        ty: LinkStackTy::Link,
1295                    });
1296                }
1297                ItemBody::MaybeImage => {
1298                    self.tree[cur_ix].item.body = ItemBody::Text {
1299                        backslash_escaped: false,
1300                    };
1301                    let link_open_doubled = self.tree[cur_ix]
1302                        .next
1303                        .map(|ix| self.tree[ix].item.body == ItemBody::MaybeLinkOpen)
1304                        .unwrap_or(false);
1305                    if self.options.contains(Options::ENABLE_WIKILINKS) && link_open_doubled {
1306                        self.wikilink_stack.push(LinkStackEl {
1307                            node: cur_ix,
1308                            ty: LinkStackTy::Image,
1309                        });
1310                    }
1311                    self.link_stack.push(LinkStackEl {
1312                        node: cur_ix,
1313                        ty: LinkStackTy::Image,
1314                    });
1315                }
1316                ItemBody::MaybeLinkClose(could_be_ref) => {
1317                    self.tree[cur_ix].item.body = ItemBody::Text {
1318                        backslash_escaped: false,
1319                    };
1320                    let tos_link = self.link_stack.pop();
1321                    if self.options.contains(Options::ENABLE_WIKILINKS)
1322                        && self.tree[cur_ix]
1323                            .next
1324                            .map(|ix| {
1325                                matches!(self.tree[ix].item.body, ItemBody::MaybeLinkClose(..))
1326                            })
1327                            .unwrap_or(false)
1328                        && let Some(node) = self.handle_wikilink(block_text, cur_ix, prev)
1329                    {
1330                        cur = self.tree[node].next;
1331                        continue;
1332                    }
1333                    if let Some(tos) = tos_link {
1334                        // skip rendering if already in a link, unless its an
1335                        // image
1336                        if tos.ty != LinkStackTy::Image
1337                            && matches!(
1338                                self.tree[self.tree.peek_up().unwrap()].item.body,
1339                                ItemBody::Link(..)
1340                            )
1341                        {
1342                            continue;
1343                        }
1344                        if tos.ty == LinkStackTy::Disabled {
1345                            continue;
1346                        }
1347                        let next = self.tree[cur_ix].next;
1348                        // remark's image label start eats the `[`, so `![^x](y)` stays an image.
1349                        let footnote_first = tos.ty == LinkStackTy::Link
1350                            && self.defined_footnote_label(tos.node, cur_ix);
1351                        if !footnote_first
1352                            && let Some((next_ix, url, title)) =
1353                                self.scan_inline_link(block_text, self.tree[cur_ix].item.end, next)
1354                        {
1355                            let next_node = scan_nodes_to_ix(&self.tree, next, next_ix);
1356                            if let Some(prev_ix) = prev {
1357                                self.tree[prev_ix].next = None;
1358                            }
1359                            cur = Some(tos.node);
1360                            cur_ix = tos.node;
1361                            let link_ix =
1362                                self.allocs
1363                                    .allocate_link(LinkType::Inline, url, title, "".into());
1364                            self.tree[cur_ix].item.body = if tos.ty == LinkStackTy::Image {
1365                                ItemBody::Image(link_ix)
1366                            } else {
1367                                ItemBody::Link(link_ix)
1368                            };
1369                            self.tree[cur_ix].child = self.tree[cur_ix].next;
1370                            self.tree[cur_ix].next = next_node;
1371                            self.tree[cur_ix].item.end = next_ix;
1372                            if let Some(next_node_ix) = next_node {
1373                                let orig_start = self.tree[next_node_ix].item.start;
1374                                let new_start = max(orig_start, next_ix);
1375                                self.tree[next_node_ix].item.start = new_start;
1376                                // If the text node's start was advanced past
1377                                // its original position (the link's URL or
1378                                // title consumed the bytes the escape was
1379                                // attached to), the `backslash_escaped`
1380                                // flag no longer applies — clear it so the
1381                                // arena-build position fixup doesn't extend
1382                                // the text node's source span back over
1383                                // bytes already owned by the link.
1384                                if new_start > orig_start
1385                                    && let ItemBody::Text { backslash_escaped } =
1386                                        &mut self.tree[next_node_ix].item.body
1387                                {
1388                                    *backslash_escaped = false;
1389                                }
1390                            }
1391
1392                            if tos.ty == LinkStackTy::Link {
1393                                self.disable_all_links();
1394                            }
1395                        } else {
1396                            // Footnote-first check: if the first bracket content is
1397                            // `[^X]` where `X` has a matching footnote definition,
1398                            // emit a FootnoteReference regardless of what follows.
1399                            // Otherwise `[^X][Y]` would be resolved as a link whose
1400                            // text happens to start with `^`, which diverges from
1401                            // remark-gfm's two-node parse (footnote + trailing ref).
1402                            let first_bracket_start = self.tree[tos.node].item.start;
1403                            let first_bracket_end = self.tree[cur_ix].item.end;
1404                            let first_bracket_text =
1405                                &self.text[first_bracket_start..first_bracket_end];
1406                            if let Some((label_len, ReferenceLabel::Footnote(footlabel))) =
1407                                scan_link_label(&self.tree, first_bracket_text, self.options)
1408                                // A code span can swallow the label's `]`, leaving `cur_ix` on a later one.
1409                                && label_len == first_bracket_text.len()
1410                                && self.allocs.footdefs.contains(&footlabel)
1411                            {
1412                                let footref = self.allocs.allocate_cow(footlabel);
1413                                if let Some(def) = self
1414                                    .allocs
1415                                    .footdefs
1416                                    .get_mut(self.allocs.cows[footref.0].to_owned())
1417                                {
1418                                    def.use_count += 1;
1419                                }
1420                                let footnote_ix = if tos.ty == LinkStackTy::Image {
1421                                    self.tree[tos.node].next = Some(cur_ix);
1422                                    self.tree[tos.node].child = None;
1423                                    self.tree[tos.node].item.body = ItemBody::SynthesizeChar('!');
1424                                    self.tree[cur_ix].item.start =
1425                                        self.tree[tos.node].item.start + 1;
1426                                    self.tree[tos.node].item.end =
1427                                        self.tree[tos.node].item.start + 1;
1428                                    cur_ix
1429                                } else {
1430                                    tos.node
1431                                };
1432                                self.tree[footnote_ix].next = next;
1433                                self.tree[footnote_ix].child = None;
1434                                self.tree[footnote_ix].item.body =
1435                                    ItemBody::FootnoteReference(footref);
1436                                self.tree[footnote_ix].item.end = first_bracket_end;
1437                                prev = Some(footnote_ix);
1438                                cur = next;
1439                                self.link_stack.clear();
1440                                continue;
1441                            }
1442                            // ok, so its not an inline link. maybe it is a reference
1443                            // to a defined link?
1444                            let scan_result =
1445                                scan_reference(&self.tree, block_text, next, self.options);
1446                            let (node_after_link, link_type) = match scan_result {
1447                                // [label][reference]
1448                                RefScan::LinkLabel(_, end_ix) => {
1449                                    // Toggle reference viability of the last closing bracket,
1450                                    // so that we can skip it on future iterations in case
1451                                    // it fails in this one. In particular, we won't call
1452                                    // the broken link callback twice on one reference.
1453                                    let reference_close_node = if let Some(node) =
1454                                        scan_nodes_to_ix(&self.tree, next, end_ix - 1)
1455                                    {
1456                                        node
1457                                    } else {
1458                                        continue;
1459                                    };
1460                                    self.tree[reference_close_node].item.body =
1461                                        ItemBody::MaybeLinkClose(false);
1462                                    // The label scan walks raw source, so it can
1463                                    // stop inside a wider item, e.g. an MDX
1464                                    // expression or directive holding a `]`. The
1465                                    // label owns up to `end_ix`; the rest is
1466                                    // literal text, and without this it would
1467                                    // belong to no node.
1468                                    let close_end = self.tree[reference_close_node].item.end;
1469                                    let next_node = if close_end > end_ix {
1470                                        self.tree[reference_close_node].item.end = end_ix;
1471                                        let tail = self.tree.create_node(Item {
1472                                            start: end_ix,
1473                                            end: close_end,
1474                                            body: ItemBody::Text {
1475                                                backslash_escaped: false,
1476                                            },
1477                                        });
1478                                        self.tree[tail].next = self.tree[reference_close_node].next;
1479                                        self.tree[reference_close_node].next = Some(tail);
1480                                        Some(tail)
1481                                    } else {
1482                                        self.tree[reference_close_node].next
1483                                    };
1484
1485                                    (next_node, LinkType::Reference)
1486                                }
1487                                // [reference][]
1488                                RefScan::Collapsed(next_node) => {
1489                                    // This reference has already been tried, and it's not
1490                                    // valid. Skip it.
1491                                    if !could_be_ref {
1492                                        continue;
1493                                    }
1494                                    (next_node, LinkType::Collapsed)
1495                                }
1496                                // [X][^Y] — full-reference form with a footnote-shaped
1497                                // second label. Per CommonMark the full-ref has to
1498                                // resolve to a link definition, which `^Y` never will;
1499                                // shortcut fallback is NOT tried. Leave both brackets
1500                                // literal and let `[^Y]` be parsed as a footnote on
1501                                // its own MaybeLinkClose iteration.
1502                                RefScan::UnexpectedFootnote => continue,
1503                                // `[text][invalid_label]` — the `[` after `[text]`
1504                                // started a label slot but it wasn't a valid label
1505                                // (e.g. unescaped `[` inside). Spec: a shortcut link
1506                                // can't be followed by `[`, so don't fall back to
1507                                // shortcut. Leave both brackets literal.
1508                                RefScan::FailedInvalidLabel => continue,
1509                                // [shortcut]
1510                                //
1511                                // [shortcut]: /blah
1512                                RefScan::Failed => {
1513                                    if !could_be_ref {
1514                                        continue;
1515                                    }
1516                                    (next, LinkType::Shortcut)
1517                                }
1518                            };
1519
1520                            // FIXME: references and labels are mixed in the naming of variables
1521                            // below. Disambiguate!
1522
1523                            // (label, source_ix end)
1524                            let label: Option<(ReferenceLabel<'input>, usize)> = match scan_result {
1525                                RefScan::LinkLabel(l, end_ix) => {
1526                                    Some((ReferenceLabel::Link(l), end_ix))
1527                                }
1528                                RefScan::Collapsed(..)
1529                                | RefScan::Failed
1530                                | RefScan::FailedInvalidLabel
1531                                | RefScan::UnexpectedFootnote => {
1532                                    // No label? maybe it is a shortcut reference
1533                                    let label_start = self.tree[tos.node].item.end - 1;
1534                                    let label_end = self.tree[cur_ix].item.end;
1535                                    scan_link_label(
1536                                        &self.tree,
1537                                        &self.text[label_start..label_end],
1538                                        self.options,
1539                                    )
1540                                    .map(|(ix, label)| (label, label_start + ix))
1541                                    .filter(|(_, end)| *end == label_end)
1542                                }
1543                            };
1544
1545                            let id = match &label {
1546                                Some(
1547                                    (ReferenceLabel::Link(l), _) | (ReferenceLabel::Footnote(l), _),
1548                                ) => l.clone(),
1549                                None => "".into(),
1550                            };
1551
1552                            // see if it's a footnote reference
1553                            if let Some((ReferenceLabel::Footnote(l), end)) = label {
1554                                let footref = self.allocs.allocate_cow(l);
1555                                if let Some(def) = self
1556                                    .allocs
1557                                    .footdefs
1558                                    .get_mut(self.allocs.cows[footref.0].to_owned())
1559                                {
1560                                    def.use_count += 1;
1561                                }
1562                                if self.allocs.footdefs.contains(&self.allocs.cows[footref.0]) {
1563                                    // If this came from a MaybeImage, then the `!` prefix
1564                                    // isn't part of the footnote reference.
1565                                    let footnote_ix = if tos.ty == LinkStackTy::Image {
1566                                        self.tree[tos.node].next = Some(cur_ix);
1567                                        self.tree[tos.node].child = None;
1568                                        self.tree[tos.node].item.body =
1569                                            ItemBody::SynthesizeChar('!');
1570                                        self.tree[cur_ix].item.start =
1571                                            self.tree[tos.node].item.start + 1;
1572                                        self.tree[tos.node].item.end =
1573                                            self.tree[tos.node].item.start + 1;
1574                                        cur_ix
1575                                    } else {
1576                                        tos.node
1577                                    };
1578                                    // use `next` instead of `node_after_link` because
1579                                    // node_after_link is calculated for a [collapsed][] link,
1580                                    // which footnotes don't support.
1581                                    self.tree[footnote_ix].next = next;
1582                                    self.tree[footnote_ix].child = None;
1583                                    self.tree[footnote_ix].item.body =
1584                                        ItemBody::FootnoteReference(footref);
1585                                    self.tree[footnote_ix].item.end = end;
1586                                    prev = Some(footnote_ix);
1587                                    cur = next;
1588                                    self.link_stack.clear();
1589                                    continue;
1590                                }
1591                            } else if let Some((ReferenceLabel::Link(link_label), end)) = label
1592                                && let Some((def_link_type, url, title)) = self
1593                                    .fetch_link_type_url_title(
1594                                        link_label,
1595                                        (self.tree[tos.node].item.start)..end,
1596                                        link_type,
1597                                        callbacks,
1598                                    )
1599                            {
1600                                let link_ix =
1601                                    self.allocs.allocate_link(def_link_type, url, title, id);
1602                                self.tree[tos.node].item.body = if tos.ty == LinkStackTy::Image {
1603                                    ItemBody::Image(link_ix)
1604                                } else {
1605                                    ItemBody::Link(link_ix)
1606                                };
1607                                let label_node = self.tree[tos.node].next;
1608
1609                                // lets do some tree surgery to add the link to the tree
1610                                // 1st: skip the label node and close node
1611                                self.tree[tos.node].next = node_after_link;
1612
1613                                // then, if it exists, add the label node as a child to the link node
1614                                if label_node != cur {
1615                                    self.tree[tos.node].child = label_node;
1616
1617                                    // finally: disconnect list of children
1618                                    if let Some(prev_ix) = prev {
1619                                        self.tree[prev_ix].next = None;
1620                                    }
1621                                }
1622
1623                                self.tree[tos.node].item.end = end;
1624                                // No `max(orig_start, end)` clamp here,
1625                                // unlike the inline-link splice: the item at
1626                                // `end - 1` either ends the label or was
1627                                // truncated to it, and the zero-width items
1628                                // that could sit at `end` all sit on a URL
1629                                // or email byte, never a `]`.
1630                                debug_assert!(
1631                                    node_after_link.is_none_or(|node_after_ix| {
1632                                        self.tree[node_after_ix].item.start >= end
1633                                    }),
1634                                    "reference splice must not overrun its successor",
1635                                );
1636
1637                                // set up cur so next node will be node_after_link
1638                                cur = Some(tos.node);
1639                                cur_ix = tos.node;
1640
1641                                if tos.ty == LinkStackTy::Link {
1642                                    self.disable_all_links();
1643                                }
1644                            }
1645                        }
1646                    }
1647                }
1648                _ => {}
1649            }
1650            prev = cur;
1651            cur = self.tree[cur_ix].next;
1652        }
1653        self.link_stack.clear();
1654        self.wikilink_stack.clear();
1655        self.code_delims.clear();
1656        self.math_delims.clear();
1657    }
1658
1659    /// The construct opening on the byte after a URL-ending `\`. The first
1660    /// pass's escape handler consumed that byte, so the construct either never
1661    /// got an item or got a blocked one; now that the link owns the `\`, give
1662    /// it back.
1663    fn repair_construct_after_url_end(&mut self, cand_end: usize, node_ix: TreeIndex) {
1664        let item = self.tree[node_ix].item;
1665        if item.start != cand_end {
1666            return;
1667        }
1668        if let ItemBody::MaybeEmphasisEscaped(count, can_open, can_close) = item.body {
1669            self.tree[node_ix].item.body = ItemBody::MaybeEmphasis(count, can_open, can_close);
1670            // The rest of the run was emitted for the shorter run the escape
1671            // would have left, so only its flanking needs restating.
1672            let mut scan = self.tree[node_ix].next;
1673            for _ in 1..count {
1674                let Some(next_ix) = scan else { break };
1675                if let ItemBody::MaybeEmphasis(_, open, close) = &mut self.tree[next_ix].item.body {
1676                    *open = can_open;
1677                    *close = can_close;
1678                }
1679                scan = self.tree[next_ix].next;
1680            }
1681            return;
1682        }
1683        if !matches!(item.body, ItemBody::Text { .. })
1684            || self.text.as_bytes().get(cand_end) != Some(&b'&')
1685        {
1686            return;
1687        }
1688        let (n, Some(value)) = scan_entity(&self.text.as_bytes()[cand_end..]) else {
1689            return;
1690        };
1691        if cand_end + n > item.end {
1692            return;
1693        }
1694        let cow_ix = self.allocs.allocate_cow(value);
1695        if cand_end + n < item.end {
1696            let tail = self.tree.create_node(Item {
1697                start: cand_end + n,
1698                end: item.end,
1699                body: ItemBody::Text {
1700                    backslash_escaped: false,
1701                },
1702            });
1703            self.tree[tail].next = self.tree[node_ix].next;
1704            self.tree[node_ix].next = Some(tail);
1705        }
1706        self.tree[node_ix].item.end = cand_end + n;
1707        self.tree[node_ix].item.body = ItemBody::SynthesizeText(cow_ix);
1708    }
1709
1710    /// Handles a wikilink.
1711    ///
1712    /// This function may bail early in case the link is malformed, so this
1713    /// acts as a control flow guard. Returns the link node if a wikilink was
1714    /// found and created.
1715    fn handle_wikilink(
1716        &mut self,
1717        block_text: &'input str,
1718        cur_ix: TreeIndex,
1719        prev: Option<TreeIndex>,
1720    ) -> Option<TreeIndex> {
1721        let next_ix = self.tree[cur_ix].next.unwrap();
1722        // this is a wikilink closing delim, try popping from
1723        // the wikilink stack
1724        if let Some(tos) = self.wikilink_stack.pop() {
1725            if tos.ty == LinkStackTy::Disabled {
1726                return None;
1727            }
1728            // fetches the beginning of the wikilink body
1729            let Some(body_node) = self.tree[tos.node].next.and_then(|ix| self.tree[ix].next) else {
1730                // skip if no next node exists, like at end of input
1731                return None;
1732            };
1733            let start_ix = self.tree[body_node].item.start;
1734            let end_ix = self.tree[cur_ix].item.start;
1735            let wikilink = match scan_wikilink_pipe(
1736                block_text,
1737                start_ix, // bounded by closing tag
1738                end_ix - start_ix,
1739            ) {
1740                Some((rest, wikitext)) => {
1741                    // bail early if the wikiname would be empty
1742                    if wikitext.is_empty() {
1743                        return None;
1744                    }
1745                    // [[WikiName|rest]]
1746                    let body_node = scan_nodes_to_ix(&self.tree, Some(body_node), rest);
1747                    if let Some(body_node) = body_node {
1748                        // break node so passes can actually format
1749                        // the display text
1750                        self.tree[body_node].item.start = rest;
1751                        Some((true, body_node, wikitext))
1752                    } else {
1753                        None
1754                    }
1755                }
1756                None => {
1757                    let wikitext = &block_text[start_ix..end_ix];
1758                    // bail early if the wikiname would be empty
1759                    if wikitext.is_empty() {
1760                        return None;
1761                    }
1762                    let body_node = self.tree.create_node(Item {
1763                        start: start_ix,
1764                        end: end_ix,
1765                        body: ItemBody::Text {
1766                            backslash_escaped: false,
1767                        },
1768                    });
1769                    Some((false, body_node, wikitext))
1770                }
1771            };
1772
1773            if let Some((has_pothole, body_node, wikiname)) = wikilink {
1774                let link_ix = self.allocs.allocate_link(
1775                    LinkType::WikiLink { has_pothole },
1776                    wikiname.into(),
1777                    "".into(),
1778                    "".into(),
1779                );
1780                if let Some(prev_ix) = prev {
1781                    self.tree[prev_ix].next = None;
1782                }
1783                if tos.ty == LinkStackTy::Image {
1784                    self.tree[tos.node].item.body = ItemBody::Image(link_ix);
1785                } else {
1786                    self.tree[tos.node].item.body = ItemBody::Link(link_ix);
1787                }
1788                self.tree[tos.node].child = Some(body_node);
1789                self.tree[tos.node].next = self.tree[next_ix].next;
1790                self.tree[tos.node].item.end = end_ix + 2;
1791                self.disable_all_links();
1792                return Some(tos.node);
1793            }
1794        }
1795
1796        None
1797    }
1798
1799    fn handle_emphasis_in_scope(&mut self, start: Option<TreeIndex>) {
1800        let mut prev = None;
1801        let mut prev_ix: TreeIndex;
1802        let mut cur = start;
1803
1804        let mut single_quote_open: Option<TreeIndex> = None;
1805        let mut double_quote_open: bool = false;
1806
1807        while let Some(mut cur_ix) = cur {
1808            match self.tree[cur_ix].item.body {
1809                ItemBody::MaybeEmphasis(mut count, can_open, can_close) => {
1810                    let run_length = count;
1811                    let c = self.text.as_bytes()[self.tree[cur_ix].item.start];
1812                    let both = can_open && can_close;
1813                    // Defer `~`/`^` resolution to the post-pass.
1814                    // Without lookahead, the single-pass can't tell whether an
1815                    // earlier `*`/`_` opener will pair (in which case the
1816                    // `~`/`^` should match inside the future emphasis) or
1817                    // remain unmatched (in which case `~`/`^` would cross the
1818                    // boundary). micromark handles this with a separate
1819                    // strikethrough resolve phase that runs after emphasis.
1820                    if c == b'~' || c == b'^' {
1821                        prev_ix = cur_ix + count - 1;
1822                        prev = Some(prev_ix);
1823                        cur = self.tree[prev_ix].next;
1824                        continue;
1825                    }
1826                    if can_close {
1827                        while let Some(el) =
1828                            self.inline_stack
1829                                .find_match(&mut self.tree, c, run_length, count, both)
1830                        {
1831                            // have a match!
1832                            if let Some(prev_ix) = prev {
1833                                self.tree[prev_ix].next = None;
1834                            }
1835                            // Consume at most two markers per inner-loop pass
1836                            // (one `<strong>`/`<em>` per match), matching
1837                            // micromark's `use = open>1 && close>1 ? 2 : 1`.
1838                            // The outer `while let` then drives nesting by
1839                            // re-running `find_match` with the leftover
1840                            // counts, which is how `***foo***` becomes
1841                            // `<em><strong>foo</strong></em>` instead of one
1842                            // flat match.
1843                            let match_count = min(2, min(count, el.count));
1844                            // start, end are tree node indices
1845                            let mut end = cur_ix - 1;
1846                            let mut start = el.start + el.count;
1847
1848                            // work from the inside out
1849                            while start > el.start + el.count - match_count {
1850                                let inc = if start > el.start + el.count - match_count + 1 {
1851                                    2
1852                                } else {
1853                                    1
1854                                };
1855                                let ty = if c == b'~' {
1856                                    if inc == 2 {
1857                                        if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
1858                                            ItemBody::Strikethrough
1859                                        } else {
1860                                            ItemBody::Text {
1861                                                backslash_escaped: false,
1862                                            }
1863                                        }
1864                                    } else if self.options.contains(Options::ENABLE_SUBSCRIPT) {
1865                                        ItemBody::Subscript
1866                                    } else if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
1867                                        ItemBody::Strikethrough
1868                                    } else {
1869                                        ItemBody::Text {
1870                                            backslash_escaped: false,
1871                                        }
1872                                    }
1873                                } else if c == b'^' {
1874                                    if self.options.contains(Options::ENABLE_SUPERSCRIPT) {
1875                                        ItemBody::Superscript
1876                                    } else {
1877                                        ItemBody::Text {
1878                                            backslash_escaped: false,
1879                                        }
1880                                    }
1881                                } else if inc == 2 {
1882                                    ItemBody::Strong
1883                                } else {
1884                                    ItemBody::Emphasis
1885                                };
1886
1887                                let root = start - inc;
1888                                end = end + inc;
1889                                self.tree[root].item.body = ty;
1890                                self.tree[root].item.end = self.tree[end].item.end;
1891                                self.tree[root].child = Some(start);
1892                                self.tree[root].next = None;
1893                                start = root;
1894                            }
1895
1896                            // set next for top most emph level
1897                            prev_ix = el.start + el.count - match_count;
1898                            prev = Some(prev_ix);
1899                            cur = self.tree[cur_ix + match_count - 1].next;
1900                            self.tree[prev_ix].next = cur;
1901
1902                            if el.count > match_count {
1903                                self.inline_stack.push(InlineEl {
1904                                    start: el.start,
1905                                    count: el.count - match_count,
1906                                    run_length: el.run_length,
1907                                    c: el.c,
1908                                    both: el.both,
1909                                })
1910                            }
1911                            count -= match_count;
1912                            if count > 0 {
1913                                cur_ix = cur.unwrap();
1914                            } else {
1915                                break;
1916                            }
1917                        }
1918                    }
1919                    if count > 0 {
1920                        if can_open {
1921                            self.inline_stack.push(InlineEl {
1922                                start: cur_ix,
1923                                run_length,
1924                                count,
1925                                c,
1926                                both,
1927                            });
1928                        } else {
1929                            for i in 0..count {
1930                                self.tree[cur_ix + i].item.body = ItemBody::Text {
1931                                    backslash_escaped: false,
1932                                };
1933                            }
1934                        }
1935                        prev_ix = cur_ix + count - 1;
1936                        prev = Some(prev_ix);
1937                        cur = self.tree[prev_ix].next;
1938                    }
1939                }
1940                ItemBody::MaybeSmartQuote(c, can_open, can_close) => {
1941                    self.tree[cur_ix].item.body = match c {
1942                        b'\'' => {
1943                            if let (Some(open_ix), true) = (single_quote_open, can_close) {
1944                                self.tree[open_ix].item.body = ItemBody::SynthesizeChar('‘');
1945                                single_quote_open = None;
1946                            } else if can_open {
1947                                single_quote_open = Some(cur_ix);
1948                            }
1949                            ItemBody::SynthesizeChar('’')
1950                        }
1951                        _ /* double quote */ => {
1952                            if can_close && double_quote_open {
1953                                double_quote_open = false;
1954                                ItemBody::SynthesizeChar('”')
1955                            } else if can_open {
1956                                double_quote_open = true;
1957                                ItemBody::SynthesizeChar('“')
1958                            } else if can_close {
1959                                // Unmatched close-flanking quote (e.g. the inch
1960                                // mark in `24"`): render a closing curly quote.
1961                                ItemBody::SynthesizeChar('”')
1962                            } else {
1963                                // Non-flanking quote: opening curly quote.
1964                                ItemBody::SynthesizeChar('“')
1965                            }
1966                        }
1967                    };
1968                    prev = cur;
1969                    cur = self.tree[cur_ix].next;
1970                }
1971                ItemBody::HardBreak(true) => {
1972                    if self.tree[cur_ix].next.is_none() {
1973                        self.tree[cur_ix].item.body = ItemBody::SynthesizeChar('\\');
1974                    }
1975                    prev = cur;
1976                    cur = self.tree[cur_ix].next;
1977                }
1978                _ => {
1979                    prev = cur;
1980                    cur = self.tree[cur_ix].next;
1981                }
1982            }
1983        }
1984        self.inline_stack.pop_all(&mut self.tree);
1985    }
1986
1987    /// Second-pass strikethrough/sub/sup resolution. Walks the tree
1988    /// hierarchically and resolves `~`/`^` MaybeEmphasis tokens within
1989    /// each inline scope independently. This matches micromark's
1990    /// post-emphasis resolve phase: a `~..~` pair only forms when both
1991    /// ends lie within the same enclosing scope (root, emphasis, link,
1992    /// etc.). Multi-char `~~` strikethrough was already resolved in
1993    /// the main pass.
1994    /// Resolve `~`/`^` runs at this scope. With `descend`, also recurse into
1995    /// already-formed spans; the per-scope driver passes `false` and handles
1996    /// descent itself so it can flip the resolve order for nested content.
1997    fn resolve_tildes_carets_in_scope(&mut self, start: Option<TreeIndex>, descend: bool) {
1998        let mut stack: Vec<InlineEl> = Vec::new();
1999        let mut cur = start;
2000        let mut prev: Option<TreeIndex> = None;
2001        while let Some(mut cur_ix) = cur {
2002            match self.tree[cur_ix].item.body {
2003                ItemBody::MaybeEmphasis(count, can_open, can_close) => {
2004                    let c = self.text.as_bytes()[self.tree[cur_ix].item.start];
2005                    if c != b'~' && c != b'^' {
2006                        prev = Some(cur_ix);
2007                        cur = self.tree[cur_ix].next;
2008                        continue;
2009                    }
2010                    let run_length = count;
2011                    let mut remaining = count;
2012                    if can_close {
2013                        while remaining > 0 {
2014                            let res = stack
2015                                .iter()
2016                                .enumerate()
2017                                .rfind(|(_, el)| el.c == c && el.run_length == run_length);
2018                            let Some((matching_ix, matching_el)) = res else {
2019                                break;
2020                            };
2021                            let matching_el = *matching_el;
2022                            if let Some(prev_ix) = prev {
2023                                self.tree[prev_ix].next = None;
2024                            }
2025                            // Convert intermediate `~`/`^` openers above the
2026                            // match to text — they failed to find a pair.
2027                            for el in &stack[(matching_ix + 1)..] {
2028                                for i in 0..el.count {
2029                                    self.tree[el.start + i].item.body = ItemBody::Text {
2030                                        backslash_escaped: false,
2031                                    };
2032                                }
2033                            }
2034                            stack.truncate(matching_ix);
2035                            let match_count =
2036                                core::cmp::min(2, core::cmp::min(remaining, matching_el.count));
2037                            let mut end = cur_ix - 1;
2038                            let mut sub_start = matching_el.start + matching_el.count;
2039                            while sub_start > matching_el.start + matching_el.count - match_count {
2040                                let inc = if sub_start
2041                                    > matching_el.start + matching_el.count - match_count + 1
2042                                {
2043                                    2
2044                                } else {
2045                                    1
2046                                };
2047                                let ty = if c == b'~' {
2048                                    if inc == 2 {
2049                                        if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
2050                                            ItemBody::Strikethrough
2051                                        } else {
2052                                            ItemBody::Text {
2053                                                backslash_escaped: false,
2054                                            }
2055                                        }
2056                                    } else if self.options.contains(Options::ENABLE_SUBSCRIPT) {
2057                                        ItemBody::Subscript
2058                                    } else if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
2059                                        ItemBody::Strikethrough
2060                                    } else {
2061                                        ItemBody::Text {
2062                                            backslash_escaped: false,
2063                                        }
2064                                    }
2065                                } else if self.options.contains(Options::ENABLE_SUPERSCRIPT) {
2066                                    ItemBody::Superscript
2067                                } else {
2068                                    ItemBody::Text {
2069                                        backslash_escaped: false,
2070                                    }
2071                                };
2072                                let root = sub_start - inc;
2073                                end = end + inc;
2074                                self.tree[root].item.body = ty;
2075                                self.tree[root].item.end = self.tree[end].item.end;
2076                                self.tree[root].child = Some(sub_start);
2077                                self.tree[root].next = None;
2078                                sub_start = root;
2079                            }
2080                            let new_prev_ix = matching_el.start + matching_el.count - match_count;
2081                            let new_cur = self.tree[cur_ix + match_count - 1].next;
2082                            self.tree[new_prev_ix].next = new_cur;
2083                            prev = Some(new_prev_ix);
2084                            if matching_el.count > match_count {
2085                                stack.push(InlineEl {
2086                                    start: matching_el.start,
2087                                    count: matching_el.count - match_count,
2088                                    run_length: matching_el.run_length,
2089                                    c: matching_el.c,
2090                                    both: matching_el.both,
2091                                });
2092                            }
2093                            remaining -= match_count;
2094                            if remaining > 0 {
2095                                let Some(next_cur) = new_cur else { break };
2096                                cur_ix = next_cur;
2097                            } else {
2098                                break;
2099                            }
2100                        }
2101                    }
2102                    if remaining > 0 {
2103                        if can_open {
2104                            stack.push(InlineEl {
2105                                start: cur_ix,
2106                                count: remaining,
2107                                run_length,
2108                                c,
2109                                both: can_open && can_close,
2110                            });
2111                        } else {
2112                            for i in 0..remaining {
2113                                self.tree[cur_ix + i].item.body = ItemBody::Text {
2114                                    backslash_escaped: false,
2115                                };
2116                            }
2117                        }
2118                        let prev_ix = cur_ix + remaining - 1;
2119                        prev = Some(prev_ix);
2120                        cur = self.tree[prev_ix].next;
2121                    } else {
2122                        cur = self.tree[prev.unwrap()].next;
2123                    }
2124                    continue;
2125                }
2126                ItemBody::Emphasis
2127                | ItemBody::Strong
2128                | ItemBody::Strikethrough
2129                | ItemBody::Subscript
2130                | ItemBody::Superscript
2131                | ItemBody::Link(_)
2132                | ItemBody::Image(_)
2133                    if descend =>
2134                {
2135                    let child = self.tree[cur_ix].child;
2136                    self.resolve_tildes_carets_in_scope(child, true);
2137                }
2138                _ => {}
2139            }
2140            prev = Some(cur_ix);
2141            cur = self.tree[cur_ix].next;
2142        }
2143        // End of scope: any remaining openers couldn't find a closer.
2144        for el in stack {
2145            for i in 0..el.count {
2146                self.tree[el.start + i].item.body = ItemBody::Text {
2147                    backslash_escaped: false,
2148                };
2149            }
2150        }
2151    }
2152
2153    fn disable_all_links(&mut self) {
2154        self.link_stack.disable_all_links();
2155        self.wikilink_stack.disable_all_links();
2156    }
2157
2158    fn defined_footnote_label(&self, open_ix: TreeIndex, close_ix: TreeIndex) -> bool {
2159        let start = self.tree[open_ix].item.start;
2160        if !self.options.contains(Options::ENABLE_FOOTNOTES)
2161            || self.text.as_bytes().get(start + 1) != Some(&b'^')
2162        {
2163            return false;
2164        }
2165        let label_text = &self.text[start..self.tree[close_ix].item.end];
2166        let Some((len, ReferenceLabel::Footnote(label))) =
2167            scan_link_label(&self.tree, label_text, self.options)
2168        else {
2169            return false;
2170        };
2171        // micromark ends a footnote call at whitespace, where the shared label scan collapses it.
2172        len == label_text.len()
2173            && !label_text.as_bytes()[..len]
2174                .iter()
2175                .any(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
2176            && self.allocs.footdefs.contains(&label)
2177    }
2178
2179    /// Returns next byte index, url and title.
2180    fn scan_inline_link(
2181        &self,
2182        underlying: &'input str,
2183        mut ix: usize,
2184        node: Option<TreeIndex>,
2185    ) -> Option<(usize, CowStr<'input>, CowStr<'input>)> {
2186        if underlying.as_bytes().get(ix) != Some(&b'(') {
2187            return None;
2188        }
2189        ix += 1;
2190
2191        let scan_separator = |ix: &mut usize| {
2192            *ix += scan_while(&underlying.as_bytes()[*ix..], is_space_or_tab);
2193            if let Some(bl) = scan_eol(&underlying.as_bytes()[*ix..]) {
2194                *ix += bl;
2195                *ix += skip_container_prefixes(
2196                    &self.tree,
2197                    &underlying.as_bytes()[*ix..],
2198                    self.options,
2199                );
2200            }
2201            *ix += scan_while(&underlying.as_bytes()[*ix..], is_space_or_tab);
2202        };
2203
2204        scan_separator(&mut ix);
2205
2206        let (dest_length, dest) = scan_link_dest(underlying, ix, LINK_MAX_NESTED_PARENS)?;
2207        let dest = unescape(dest, self.tree.is_in_table());
2208        ix += dest_length;
2209
2210        let dest_end = ix;
2211        scan_separator(&mut ix);
2212
2213        // A title is only reachable through whitespace after the destination.
2214        let title = if ix > dest_end
2215            && let Some((bytes_scanned, t)) = self.scan_link_title(underlying, ix, node)
2216        {
2217            ix += bytes_scanned;
2218            scan_separator(&mut ix);
2219            t
2220        } else {
2221            "".into()
2222        };
2223        if underlying.as_bytes().get(ix) != Some(&b')') {
2224            return None;
2225        }
2226        ix += 1;
2227
2228        Some((ix, dest, title))
2229    }
2230
2231    // returns (bytes scanned, title cow)
2232    fn scan_link_title(
2233        &self,
2234        text: &'input str,
2235        start_ix: usize,
2236        node: Option<TreeIndex>,
2237    ) -> Option<(usize, CowStr<'input>)> {
2238        let bytes = text.as_bytes();
2239        let open = match bytes.get(start_ix) {
2240            Some(b @ b'\'') | Some(b @ b'\"') | Some(b @ b'(') => *b,
2241            _ => return None,
2242        };
2243        if open == b'(' && start_ix >= self.unclosed_paren_title_floor.get() {
2244            return None;
2245        }
2246        // Unlike CommonMark, remark keeps an unescaped `(` inside a paren title as content.
2247        let close = if open == b'(' { b')' } else { open };
2248
2249        let mut title = String::new();
2250        let mut mark = start_ix + 1;
2251        let mut i = start_ix + 1;
2252
2253        while i < bytes.len() {
2254            let c = bytes[i];
2255
2256            if c == close {
2257                let cow = if title.is_empty() {
2258                    (i - start_ix + 1, text[mark..i].into())
2259                } else {
2260                    title.push_str(&text[mark..i]);
2261                    (i - start_ix + 1, title.into())
2262                };
2263
2264                return Some(cow);
2265            }
2266
2267            if (c == b'\n' || c == b'\r')
2268                && let Some(node_ix) = scan_nodes_to_ix(&self.tree, node, i + 1)
2269                && self.tree[node_ix].item.start > i
2270            {
2271                title.push_str(&text[mark..i]);
2272                // The title's line endings are content, kept byte for byte.
2273                title.push(c as char);
2274                if c == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
2275                    title.push('\n');
2276                }
2277                i = self.tree[node_ix].item.start;
2278                mark = i;
2279                continue;
2280            }
2281            if c == b'&'
2282                && let (n, Some(value)) = scan_entity(&bytes[i..])
2283            {
2284                title.push_str(&text[mark..i]);
2285                title.push_str(&value);
2286                i += n;
2287                mark = i;
2288                continue;
2289            }
2290            if self.tree.is_in_table()
2291                && c == b'\\'
2292                && i + 2 < bytes.len()
2293                && bytes[i + 1] == b'\\'
2294                && bytes[i + 2] == b'|'
2295            {
2296                // this runs if there are an even number of pipes in a table
2297                // if it's odd, then it gets parsed as normal
2298                title.push_str(&text[mark..i]);
2299                i += 2;
2300                mark = i;
2301            }
2302            if c == b'\\' && i + 1 < bytes.len() && is_ascii_punctuation(bytes[i + 1]) {
2303                title.push_str(&text[mark..i]);
2304                i += 1;
2305                mark = i;
2306            }
2307
2308            i += 1;
2309        }
2310
2311        if open == b'(' {
2312            let floor = self.unclosed_paren_title_floor.get();
2313            self.unclosed_paren_title_floor.set(floor.min(start_ix));
2314        }
2315        None
2316    }
2317
2318    fn make_math_span(&mut self, open: TreeIndex, close: TreeIndex) {
2319        // Find the end of the opening run of consecutive $ tokens
2320        let mut open_end = open;
2321        {
2322            let mut peek = self.tree[open].next;
2323            while let Some(peek_ix) = peek {
2324                if matches!(self.tree[peek_ix].item.body, ItemBody::MaybeMath(..))
2325                    && self.tree[peek_ix].item.start == self.tree[open_end].item.end
2326                    && peek_ix != close
2327                {
2328                    open_end = peek_ix;
2329                    peek = self.tree[peek_ix].next;
2330                } else {
2331                    break;
2332                }
2333            }
2334        }
2335        // Find the end of the closing run
2336        let mut close_end = close;
2337        {
2338            let mut peek = self.tree[close].next;
2339            while let Some(peek_ix) = peek {
2340                if matches!(self.tree[peek_ix].item.body, ItemBody::MaybeMath(..))
2341                    && self.tree[peek_ix].item.start == self.tree[close_end].item.end
2342                {
2343                    close_end = peek_ix;
2344                    peek = self.tree[peek_ix].next;
2345                } else {
2346                    break;
2347                }
2348            }
2349        }
2350
2351        let span_start = self.tree[open_end].item.end;
2352        let span_end = self.tree[close].item.start;
2353
2354        if span_start > span_end {
2355            self.tree[open].item.body = ItemBody::Text {
2356                backslash_escaped: false,
2357            };
2358            return;
2359        }
2360
2361        let spanned_text = &self.text[span_start..span_end];
2362        let spanned_bytes = spanned_text.as_bytes();
2363        let mut buf: Option<String> = None;
2364
2365        let mut start_ix = 0;
2366        let mut ix = 0;
2367        while ix < spanned_bytes.len() {
2368            let c = spanned_bytes[ix];
2369            if c == b'\r' || c == b'\n' {
2370                ix += 1;
2371                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2372                buf.push_str(&spanned_text[start_ix..ix]);
2373                // Use the full source bytes from this position (not just
2374                // the span slice) so scan_containers can see the real
2375                // line content past the closing backtick. With only the
2376                // span slice, a partial-indent line followed by buffer
2377                // end (e.g. `    ` + closing) was misread as EOL by
2378                // is_at_eol — letting the ListItem container "match" the
2379                // 4 spaces of a 5-indent item and over-strip the code
2380                // span's trailing whitespace.
2381                let from = span_start + ix;
2382                let (scanned, leftover) = skip_container_prefixes_with_remaining(
2383                    &self.tree,
2384                    &self.text.as_bytes()[from..],
2385                    self.options,
2386                );
2387                let scanned = scanned.min(spanned_bytes.len() - ix);
2388                ix += scanned;
2389                start_ix = ix;
2390                // Preserve leftover virtual columns from a tab the
2391                // container only partially consumed (e.g. `\t` in a 2-col
2392                // listitem leaves 2 spaces of content).
2393                for _ in 0..leftover {
2394                    buf.push(' ');
2395                }
2396            } else if c == b'\\'
2397                && spanned_bytes.get(ix + 1) == Some(&b'|')
2398                && self.tree.is_in_table()
2399            {
2400                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2401                buf.push_str(&spanned_text[start_ix..ix]);
2402                buf.push('|');
2403                ix += 2;
2404                start_ix = ix;
2405            } else {
2406                ix += 1;
2407            }
2408        }
2409
2410        if let Some(buf) = &mut buf {
2411            buf.push_str(&spanned_text[start_ix..]);
2412        }
2413        let cow: CowStr<'input> = strip_span_padding(buf, spanned_text);
2414
2415        self.tree[open].item.body = ItemBody::Math(self.allocs.allocate_cow(cow), false);
2416        self.tree[open].item.end = self.tree[close_end].item.end;
2417        self.tree[open].next = self.tree[close_end].next;
2418    }
2419
2420    /// Make a code span.
2421    ///
2422    /// Both `open` and `close` are matching MaybeCode items.
2423    fn make_code_span(&mut self, open: TreeIndex, close: TreeIndex, preceding_backslash: bool) {
2424        let span_start = self.tree[open].item.end;
2425        let span_end = self.tree[close].item.start;
2426        let mut buf: Option<String> = None;
2427
2428        let spanned_text = &self.text[span_start..span_end];
2429        let spanned_bytes = spanned_text.as_bytes();
2430        let mut start_ix = 0;
2431        let mut ix = 0;
2432        while ix < spanned_bytes.len() {
2433            let c = spanned_bytes[ix];
2434            if c == b'\r' || c == b'\n' {
2435                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2436                // The span's own line endings are content and are kept byte for
2437                // byte; only the container prefix below is stripped.
2438                buf.push_str(&spanned_text[start_ix..ix]);
2439                buf.push(c as char);
2440                ix += 1;
2441                if c == b'\r' && spanned_bytes.get(ix) == Some(&b'\n') {
2442                    buf.push('\n');
2443                    ix += 1;
2444                }
2445                // Use the full source bytes from this position (not just
2446                // the span slice) so scan_containers can see the real
2447                // line content past the closing backtick. With only the
2448                // span slice, a partial-indent line followed by buffer
2449                // end (e.g. `    ` + closing) was misread as EOL by
2450                // is_at_eol — letting the ListItem container "match" the
2451                // 4 spaces of a 5-indent item and over-strip the code
2452                // span's trailing whitespace.
2453                let from = span_start + ix;
2454                let (scanned, leftover) = skip_container_prefixes_with_remaining(
2455                    &self.tree,
2456                    &self.text.as_bytes()[from..],
2457                    self.options,
2458                );
2459                let scanned = scanned.min(spanned_bytes.len() - ix);
2460                ix += scanned;
2461                start_ix = ix;
2462                // Preserve leftover virtual columns from a tab the
2463                // container only partially consumed (e.g. `\t` in a 2-col
2464                // listitem leaves 2 spaces of content).
2465                for _ in 0..leftover {
2466                    buf.push(' ');
2467                }
2468            } else if c == b'\\'
2469                && spanned_bytes.get(ix + 1) == Some(&b'|')
2470                && self.tree.is_in_table()
2471            {
2472                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2473                buf.push_str(&spanned_text[start_ix..ix]);
2474                buf.push('|');
2475                ix += 2;
2476                start_ix = ix;
2477            } else {
2478                ix += 1;
2479            }
2480        }
2481
2482        if let Some(buf) = &mut buf {
2483            buf.push_str(&spanned_text[start_ix..]);
2484        }
2485        let cow: CowStr<'input> = strip_span_padding(buf, spanned_text);
2486
2487        if preceding_backslash {
2488            self.tree[open].item.body = ItemBody::Text {
2489                backslash_escaped: true,
2490            };
2491            self.tree[open].item.end = self.tree[open].item.start + 1;
2492            self.tree[open].next = Some(close);
2493            self.tree[close].item.body = ItemBody::Code(self.allocs.allocate_cow(cow));
2494            self.tree[close].item.start = self.tree[open].item.start + 1;
2495        } else {
2496            self.tree[open].item.body = ItemBody::Code(self.allocs.allocate_cow(cow));
2497            self.tree[open].item.end = self.tree[close].item.end;
2498            self.tree[open].next = self.tree[close].next;
2499        }
2500
2501        // MDX: errors recorded in pass 1 for `{` inside what turned out to be a
2502        // code span are false positives — the `{` is literal text.
2503        if !self.mdx_errors.is_empty() {
2504            self.mdx_errors
2505                .retain(|(offset, _)| *offset < span_start || *offset >= span_end);
2506        }
2507    }
2508
2509    /// On success, returns a buffer containing the inline html and byte offset.
2510    /// When no bytes were skipped, the buffer will be empty and the html can be
2511    /// represented as a subslice of the input string.
2512    fn scan_inline_html(&mut self, bytes: &[u8], ix: usize) -> Option<(Vec<u8>, usize)> {
2513        let c = *bytes.get(ix)?;
2514        if c == b'!' {
2515            Some((
2516                vec![],
2517                scan_inline_html_comment(bytes, ix + 1, &mut self.html_scan_guard)?,
2518            ))
2519        } else if c == b'?' {
2520            Some((
2521                vec![],
2522                scan_inline_html_processing(bytes, ix + 1, &mut self.html_scan_guard)?,
2523            ))
2524        } else {
2525            let (span, i) = scan_html_block_inner(
2526                // Subtract 1 to include the < character
2527                &bytes[(ix - 1)..],
2528                Some(&|bytes| skip_container_prefixes(&self.tree, bytes, self.options)),
2529            )?;
2530            Some((span, i + ix - 1))
2531        }
2532    }
2533}
2534
2535/// Returns number of containers scanned.
2536pub(crate) fn scan_containers(
2537    tree: &Tree<Item>,
2538    line_start: &mut LineStart<'_>,
2539    options: Options,
2540) -> usize {
2541    let mut i = 0;
2542    for &node_ix in tree.walk_spine() {
2543        match tree[node_ix].item.body {
2544            ItemBody::BlockQuote(..) => {
2545                let save = line_start.clone();
2546                // In MDX mode indented code blocks are disabled, so the
2547                // ≤3-space cap on blockquote prefix indent doesn't apply —
2548                // tab- or 4+-space-indented `>` should still continue the
2549                // blockquote (matches micromark + remark-mdx).
2550                if options.contains(Options::ENABLE_MDX) {
2551                    line_start.scan_all_space();
2552                } else {
2553                    let _ = line_start.scan_space(3);
2554                }
2555                if !line_start.scan_blockquote_marker() {
2556                    *line_start = save;
2557                    break;
2558                }
2559            }
2560            ItemBody::ListItem(indent, _) => {
2561                let save = line_start.clone();
2562                if !line_start.scan_space(indent) && !line_start.is_at_eol() {
2563                    *line_start = save;
2564                    break;
2565                }
2566            }
2567            ItemBody::DefinitionListDefinition(indent, _) => {
2568                let save = line_start.clone();
2569                if !line_start.scan_space(indent) && !line_start.is_at_eol() {
2570                    *line_start = save;
2571                    break;
2572                }
2573            }
2574            ItemBody::FootnoteDefinition(..) if options.contains(Options::ENABLE_FOOTNOTES) => {
2575                let save = line_start.clone();
2576                if !line_start.scan_space(4) && !line_start.is_at_eol() {
2577                    *line_start = save;
2578                    break;
2579                }
2580            }
2581            _ => (),
2582        }
2583        i += 1;
2584    }
2585    i
2586}
2587
2588/// Strip one leading and one trailing space or line ending from a code or math
2589/// span, but only when both are present and the span isn't all whitespace. A
2590/// CRLF is a single line ending, so it goes as a pair.
2591///
2592/// `buf` is the span's rewritten content when a container prefix or `\|` escape
2593/// forced an allocation; otherwise the span is `spanned_text` verbatim.
2594fn strip_span_padding<'input>(buf: Option<String>, spanned_text: &'input str) -> CowStr<'input> {
2595    let s = buf.as_deref().unwrap_or(spanned_text);
2596    let lead = if s.starts_with("\r\n") {
2597        2
2598    } else {
2599        usize::from(matches!(s.as_bytes().first(), Some(b' ' | b'\n' | b'\r')))
2600    };
2601    let trail = if s.ends_with("\r\n") {
2602        2
2603    } else {
2604        usize::from(matches!(s.as_bytes().last(), Some(b' ' | b'\n' | b'\r')))
2605    };
2606    let all_spaces = s.bytes().all(|b| matches!(b, b' ' | b'\n' | b'\r'));
2607
2608    if !all_spaces && lead > 0 && trail > 0 {
2609        if let Some(mut buf) = buf {
2610            if !buf.is_empty() {
2611                buf.truncate(buf.len() - trail);
2612                buf.replace_range(..lead, "");
2613            }
2614            buf.into()
2615        } else {
2616            spanned_text[lead..(spanned_text.len() - trail).max(lead)].into()
2617        }
2618    } else if let Some(buf) = buf {
2619        buf.into()
2620    } else {
2621        spanned_text.into()
2622    }
2623}
2624
2625pub(crate) fn skip_container_prefixes(tree: &Tree<Item>, bytes: &[u8], options: Options) -> usize {
2626    let mut line_start = LineStart::new(bytes);
2627    let _ = scan_containers(tree, &mut line_start, options);
2628    line_start.bytes_scanned()
2629}
2630
2631/// Like `skip_container_prefixes`, but also returns the leftover virtual
2632/// space columns from tab-stop expansion past the last consumed container
2633/// prefix. Used by math-span content extraction to faithfully reproduce
2634/// indentation that the container "ate" only partially — e.g. a single
2635/// `\t` (4 cols) in a list item with 2-col content indent leaves 2
2636/// trailing spaces of content.
2637fn skip_container_prefixes_with_remaining(
2638    tree: &Tree<Item>,
2639    bytes: &[u8],
2640    options: Options,
2641) -> (usize, usize) {
2642    let mut line_start = LineStart::new(bytes);
2643    let _ = scan_containers(tree, &mut line_start, options);
2644    (line_start.bytes_scanned(), line_start.remaining_space())
2645}
2646
2647impl Tree<Item> {
2648    pub(crate) fn append_text(&mut self, start: usize, end: usize, backslash_escaped: bool) {
2649        if end > start {
2650            if let Some(ix) = self.cur()
2651                && matches!(self[ix].item.body, ItemBody::Text { .. })
2652                && self[ix].item.end == start
2653            {
2654                self[ix].item.end = end;
2655                return;
2656            }
2657            self.append(Item {
2658                start,
2659                end,
2660                body: ItemBody::Text { backslash_escaped },
2661            });
2662        }
2663    }
2664    /// Returns true if the current node is inside a table.
2665    ///
2666    /// If `cur` is an ItemBody::Table, it would return false,
2667    /// but since the `TableRow` and `TableHead` and `TableCell`
2668    /// are children of the table, anything doing inline parsing
2669    /// doesn't need to care about that.
2670    pub(crate) fn is_in_table(&self) -> bool {
2671        fn might_be_in_table(item: &Item) -> bool {
2672            item.body.is_inline()
2673                || matches!(item.body, |ItemBody::TableHead| ItemBody::TableRow
2674                    | ItemBody::TableCell)
2675        }
2676        for &ix in self.walk_spine().rev() {
2677            if matches!(self[ix].item.body, ItemBody::Table(_)) {
2678                return true;
2679            }
2680            if !might_be_in_table(&self[ix].item) {
2681                return false;
2682            }
2683        }
2684        false
2685    }
2686}
2687
2688#[derive(Copy, Clone, Debug)]
2689struct InlineEl {
2690    /// offset of tree node
2691    start: TreeIndex,
2692    /// number of delimiters available for matching
2693    count: usize,
2694    /// length of the run that these delimiters came from
2695    run_length: usize,
2696    /// b'*', b'_', or b'~'
2697    c: u8,
2698    /// can both open and close
2699    both: bool,
2700}
2701
2702#[derive(Debug, Clone, Default)]
2703struct InlineStack {
2704    stack: Vec<InlineEl>,
2705    // Lower bounds for matching indices in the stack. For example
2706    // a strikethrough delimiter will never match with any element
2707    // in the stack with index smaller than
2708    // `lower_bounds[InlineStack::TILDES]`.
2709    lower_bounds: [usize; 10],
2710}
2711
2712impl InlineStack {
2713    /// These are indices into the lower bounds array.
2714    /// Not both refers to the property that the delimiter can not both
2715    /// be opener as a closer.
2716    const UNDERSCORE_NOT_BOTH: usize = 0;
2717    const ASTERISK_NOT_BOTH: usize = 1;
2718    const ASTERISK_BASE: usize = 2;
2719    const TILDES: usize = 5;
2720    const UNDERSCORE_BASE: usize = 6;
2721    const CIRCUMFLEXES: usize = 9;
2722
2723    fn pop_all(&mut self, tree: &mut Tree<Item>) {
2724        for el in self.stack.drain(..) {
2725            for i in 0..el.count {
2726                tree[el.start + i].item.body = ItemBody::Text {
2727                    backslash_escaped: false,
2728                };
2729            }
2730        }
2731        self.lower_bounds = [0; 10];
2732    }
2733
2734    fn get_lowerbound(&self, c: u8, count: usize, both: bool) -> usize {
2735        if c == b'_' {
2736            let mod3_lower = self.lower_bounds[InlineStack::UNDERSCORE_BASE + count % 3];
2737            if both {
2738                mod3_lower
2739            } else {
2740                min(
2741                    mod3_lower,
2742                    self.lower_bounds[InlineStack::UNDERSCORE_NOT_BOTH],
2743                )
2744            }
2745        } else if c == b'*' {
2746            let mod3_lower = self.lower_bounds[InlineStack::ASTERISK_BASE + count % 3];
2747            if both {
2748                mod3_lower
2749            } else {
2750                min(
2751                    mod3_lower,
2752                    self.lower_bounds[InlineStack::ASTERISK_NOT_BOTH],
2753                )
2754            }
2755        } else if c == b'^' {
2756            self.lower_bounds[InlineStack::CIRCUMFLEXES]
2757        } else {
2758            self.lower_bounds[InlineStack::TILDES]
2759        }
2760    }
2761
2762    fn set_lowerbound(&mut self, c: u8, count: usize, both: bool, new_bound: usize) {
2763        if c == b'_' {
2764            if both {
2765                self.lower_bounds[InlineStack::UNDERSCORE_BASE + count % 3] = new_bound;
2766            } else {
2767                self.lower_bounds[InlineStack::UNDERSCORE_NOT_BOTH] = new_bound;
2768            }
2769        } else if c == b'*' {
2770            self.lower_bounds[InlineStack::ASTERISK_BASE + count % 3] = new_bound;
2771            if !both {
2772                self.lower_bounds[InlineStack::ASTERISK_NOT_BOTH] = new_bound;
2773            }
2774        } else if c == b'^' {
2775            self.lower_bounds[InlineStack::CIRCUMFLEXES] = new_bound;
2776        } else {
2777            self.lower_bounds[InlineStack::TILDES] = new_bound;
2778        }
2779    }
2780
2781    fn truncate(&mut self, new_bound: usize) {
2782        self.stack.truncate(new_bound);
2783        for lower_bound in &mut self.lower_bounds {
2784            if *lower_bound > new_bound {
2785                *lower_bound = new_bound;
2786            }
2787        }
2788    }
2789
2790    /// Find an opener that can match `c` of original `run_length`.
2791    ///
2792    /// `current_count` is the **remaining** length of the closer being
2793    /// processed (chars not yet consumed by earlier inner-loop matches).
2794    /// We use it for CommonMark rule 9 (the "mod 3" both-side rule) so
2795    /// that after a partial consumption like `3*foo *bar**` the outer `*`
2796    /// can pair with what's left of the `**` — micromark re-evaluates the
2797    /// rule using only the *current* run lengths on each side.
2798    ///
2799    /// `run_length` is the original closer length; it stays stable across
2800    /// inner-loop iterations and is what the lower-bounds optimisation and
2801    /// the strict tilde/caret length check key off.
2802    fn find_match(
2803        &mut self,
2804        tree: &mut Tree<Item>,
2805        c: u8,
2806        run_length: usize,
2807        current_count: usize,
2808        both: bool,
2809    ) -> Option<InlineEl> {
2810        // Use current_count (the post-partial-consumption remaining length)
2811        // for the rule-9 mod-3 lowerbound key, not run_length. After an
2812        // inner-loop pass consumes part of the closer, the remaining
2813        // length sits in a different mod-3 bucket and may now satisfy
2814        // rule 9 with openers the earlier (longer) attempt failed
2815        // against. Keying on run_length would carry over the earlier
2816        // failure into the new bucket and block valid matches like the
2817        // outer `*` in `cz*x` `*foo***bar***baz` (closer `***` partial
2818        // remainder 1 should still reach the opener at offset 2).
2819        let lowerbound = min(
2820            self.stack.len(),
2821            self.get_lowerbound(c, current_count, both),
2822        );
2823        let res = self.stack[lowerbound..]
2824            .iter()
2825            .cloned()
2826            .enumerate()
2827            .rfind(|(_, el)| {
2828                if (c == b'~' || c == b'^') && run_length != el.run_length {
2829                    return false;
2830                }
2831                // Rule 9 (mod-3): for `*`/`_`, the openers on the stack are
2832                // checked against the *current* lengths — `el.count` reflects
2833                // remaining-after-partial-consumption when an opener has been
2834                // re-pushed, and `current_count` is the remaining closer.
2835                el.c == c
2836                    && (!both && !el.both
2837                        || !(current_count + el.count).is_multiple_of(3)
2838                        || current_count.is_multiple_of(3))
2839            });
2840
2841        if let Some((matching_ix, matching_el)) = res {
2842            let matching_ix = matching_ix + lowerbound;
2843            for el in &self.stack[(matching_ix + 1)..] {
2844                for i in 0..el.count {
2845                    tree[el.start + i].item.body = ItemBody::Text {
2846                        backslash_escaped: false,
2847                    };
2848                }
2849            }
2850            self.truncate(matching_ix);
2851            Some(matching_el)
2852        } else {
2853            // For `*`/`_`, the lower-bound optimisation is safe because their
2854            // matching rule (CM "rule of three") is monotonic across future
2855            // closers with the same count. Tildes/carets match strictly by
2856            // equal run-length, so a failure at run-length 2 must not close
2857            // the door on a later run-length 1 closer matching an earlier
2858            // run-length 1 opener still on the stack. Key the bound by
2859            // `current_count` (the post-partial-consumption length) so it
2860            // applies only to closers whose remaining bucket actually
2861            // shares this failure mode.
2862            if c != b'~' && c != b'^' {
2863                self.set_lowerbound(c, current_count, both, self.stack.len());
2864            }
2865            None
2866        }
2867    }
2868
2869    fn trim_lower_bound(&mut self, ix: usize) {
2870        self.lower_bounds[ix] = self.lower_bounds[ix].min(self.stack.len());
2871    }
2872
2873    fn push(&mut self, el: InlineEl) {
2874        if el.c == b'~' {
2875            self.trim_lower_bound(InlineStack::TILDES);
2876        } else if el.c == b'^' {
2877            self.trim_lower_bound(InlineStack::CIRCUMFLEXES);
2878        }
2879        self.stack.push(el)
2880    }
2881}
2882
2883#[derive(Debug, Clone)]
2884enum RefScan<'a> {
2885    // label, source ix of label end
2886    LinkLabel(CowStr<'a>, usize),
2887    // contains next node index
2888    Collapsed(Option<TreeIndex>),
2889    UnexpectedFootnote,
2890    Failed,
2891    // `[text][...]` where `[...]` started but is an invalid label
2892    // (e.g. contains unescaped `[`). The shortcut form for `[text]` is
2893    // suppressed because the spec says a shortcut link must NOT be
2894    // followed by `[` — even if that `[` doesn't form a valid label.
2895    FailedInvalidLabel,
2896}
2897
2898/// Skips forward within a block to a node which spans (ends inclusive) the given
2899/// index into the source.
2900fn scan_nodes_to_ix(
2901    tree: &Tree<Item>,
2902    mut node: Option<TreeIndex>,
2903    ix: usize,
2904) -> Option<TreeIndex> {
2905    while let Some(node_ix) = node {
2906        let item = tree[node_ix].item;
2907        // A zero-width node *at* `ix` must not be skipped: callers splice the
2908        // result back in as the tail, so skipping drops it from the tree.
2909        if item.end <= ix && item.start < ix {
2910            node = tree[node_ix].next;
2911        } else {
2912            break;
2913        }
2914    }
2915    node
2916}
2917
2918/// Scans an inline link label, which cannot be interrupted.
2919/// Returns number of bytes (including brackets) and label on success.
2920fn scan_link_label<'text>(
2921    tree: &Tree<Item>,
2922    text: &'text str,
2923    options: Options,
2924) -> Option<(usize, ReferenceLabel<'text>)> {
2925    let bytes = text.as_bytes();
2926    if bytes.len() < 2 || bytes[0] != b'[' {
2927        return None;
2928    }
2929    let linebreak_handler = |bytes: &[u8]| Some(skip_container_prefixes(tree, bytes, options));
2930    if options.contains(Options::ENABLE_FOOTNOTES)
2931        && b'^' == bytes[1]
2932        && bytes.get(2) != Some(&b']')
2933    {
2934        // GFM footnote labels don't wrap across line breaks.
2935        let linebreak_handler: &dyn Fn(&[u8]) -> Option<usize> = &|_| None;
2936        if let Some((byte_index, cow)) =
2937            scan_link_label_rest(&text[2..], linebreak_handler, tree.is_in_table())
2938        {
2939            return Some((byte_index + 2, ReferenceLabel::Footnote(cow)));
2940        }
2941    }
2942    let (byte_index, cow) =
2943        scan_link_label_rest(&text[1..], &linebreak_handler, tree.is_in_table())?;
2944    Some((byte_index + 1, ReferenceLabel::Link(cow)))
2945}
2946
2947fn scan_reference<'b>(
2948    tree: &Tree<Item>,
2949    text: &'b str,
2950    cur: Option<TreeIndex>,
2951    options: Options,
2952) -> RefScan<'b> {
2953    let cur_ix = match cur {
2954        None => return RefScan::Failed,
2955        Some(cur_ix) => cur_ix,
2956    };
2957    let start = tree[cur_ix].item.start;
2958    let tail = &text.as_bytes()[start..];
2959
2960    // If the `[` opening the candidate label was escaped in source
2961    // (preceded by an odd run of backslashes), it's a literal `[` and
2962    // can't start a reference label. Without this check the label
2963    // scanner walks raw source, which doesn't know that pulldown-cmark
2964    // already absorbed the `\` into a backslash-escape token, and it
2965    // would falsely consume `\[foo]` as `[foo]`.
2966    if tail.first() == Some(&b'[') && start > 0 {
2967        let src = text.as_bytes();
2968        let mut backslashes = 0usize;
2969        let mut j = start;
2970        while j > 0 && src[j - 1] == b'\\' {
2971            backslashes += 1;
2972            j -= 1;
2973        }
2974        if backslashes % 2 == 1 {
2975            return RefScan::Failed;
2976        }
2977    }
2978
2979    if tail.starts_with(b"[]") {
2980        // The trailing `]` of the collapsed reference must already exist as a
2981        // tree node — pulldown-cmark emits each bracket as its own item, and
2982        // we only reach here when `tail` already contains `]`. Defensive
2983        // fallback to `Failed` if that invariant is somehow broken.
2984        let Some(closing_node) = tree[cur_ix].next else {
2985            return RefScan::Failed;
2986        };
2987        RefScan::Collapsed(tree[closing_node].next)
2988    } else {
2989        let label = scan_link_label(tree, &text[start..], options);
2990        match label {
2991            Some((ix, ReferenceLabel::Link(label))) => RefScan::LinkLabel(label, start + ix),
2992            Some((_ix, ReferenceLabel::Footnote(_label))) => RefScan::UnexpectedFootnote,
2993            None => {
2994                // If `[text]` is followed by `[` that looked like a label
2995                // opener, the shortcut form is suppressed even though the
2996                // label parse failed (CommonMark requires shortcut links
2997                // not be followed by `[`).
2998                if tail.starts_with(b"[") {
2999                    RefScan::FailedInvalidLabel
3000                } else {
3001                    RefScan::Failed
3002                }
3003            }
3004        }
3005    }
3006}
3007
3008#[derive(Clone, Default)]
3009struct LinkStack {
3010    inner: Vec<LinkStackEl>,
3011    disabled_ix: usize,
3012}
3013
3014impl LinkStack {
3015    fn is_empty(&self) -> bool {
3016        self.inner.is_empty()
3017    }
3018
3019    fn push(&mut self, el: LinkStackEl) {
3020        self.inner.push(el);
3021    }
3022
3023    fn pop(&mut self) -> Option<LinkStackEl> {
3024        let el = self.inner.pop();
3025        self.disabled_ix = core::cmp::min(self.disabled_ix, self.inner.len());
3026        el
3027    }
3028
3029    fn clear(&mut self) {
3030        self.inner.clear();
3031        self.disabled_ix = 0;
3032    }
3033
3034    fn disable_all_links(&mut self) {
3035        for el in &mut self.inner[self.disabled_ix..] {
3036            if el.ty == LinkStackTy::Link {
3037                el.ty = LinkStackTy::Disabled;
3038            }
3039        }
3040        self.disabled_ix = self.inner.len();
3041    }
3042}
3043
3044#[derive(Clone, Debug)]
3045struct LinkStackEl {
3046    node: TreeIndex,
3047    ty: LinkStackTy,
3048}
3049
3050#[derive(PartialEq, Clone, Debug)]
3051enum LinkStackTy {
3052    Link,
3053    Image,
3054    Disabled,
3055}
3056
3057/// Contains the destination URL, title and source span of a reference definition.
3058#[derive(Clone, Debug)]
3059pub struct LinkDef<'a> {
3060    pub dest: CowStr<'a>,
3061    pub title: Option<CowStr<'a>>,
3062    pub span: Range<usize>,
3063}
3064
3065impl<'a> LinkDef<'a> {
3066    pub fn into_static(self) -> LinkDef<'static> {
3067        LinkDef {
3068            dest: self.dest.into_static(),
3069            title: self.title.map(|s| s.into_static()),
3070            span: self.span,
3071        }
3072    }
3073}
3074
3075/// Contains the destination URL, title and source span of a reference definition.
3076#[derive(Clone, Debug)]
3077pub struct FootnoteDef {
3078    pub use_count: usize,
3079}
3080
3081/// Tracks tree indices of code span delimiters of each length. It should prevent
3082/// quadratic scanning behaviours by providing (amortized) constant time lookups.
3083struct CodeDelims {
3084    inner: FxHashMap<usize, VecDeque<TreeIndex>>,
3085    seen_first: bool,
3086}
3087
3088impl CodeDelims {
3089    fn new() -> Self {
3090        Self {
3091            inner: Default::default(),
3092            seen_first: false,
3093        }
3094    }
3095
3096    fn insert(&mut self, count: usize, ix: TreeIndex) {
3097        if self.seen_first {
3098            self.inner.entry(count).or_default().push_back(ix);
3099        } else {
3100            // Skip the first insert, since that delimiter will always
3101            // be an opener and not a closer.
3102            self.seen_first = true;
3103        }
3104    }
3105
3106    fn is_populated(&self) -> bool {
3107        !self.inner.is_empty()
3108    }
3109
3110    fn find(&mut self, open_ix: TreeIndex, count: usize) -> Option<TreeIndex> {
3111        while let Some(ix) = self.inner.get_mut(&count)?.pop_front() {
3112            if ix > open_ix {
3113                return Some(ix);
3114            }
3115        }
3116        None
3117    }
3118
3119    fn clear(&mut self) {
3120        self.inner.clear();
3121        self.seen_first = false;
3122    }
3123}
3124
3125/// Tracks brace contexts and delimiter length for math delimiters.
3126/// Provides amortized constant-time lookups.
3127struct MathDelims {
3128    inner: FxHashMap<u8, VecDeque<(TreeIndex, bool, bool)>>,
3129}
3130
3131impl MathDelims {
3132    fn new() -> Self {
3133        Self {
3134            inner: Default::default(),
3135        }
3136    }
3137
3138    fn clear(&mut self) {
3139        self.inner.clear();
3140    }
3141}
3142
3143#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3144pub(crate) struct LinkIndex(usize);
3145
3146#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3147pub(crate) struct CowIndex(usize);
3148
3149#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3150pub(crate) struct AlignmentIndex(usize);
3151
3152#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3153pub(crate) struct HeadingIndex(NonZeroUsize);
3154
3155#[cfg(feature = "mdx")]
3156#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3157pub(crate) struct JsxElementIndex(usize);
3158
3159#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3160pub(crate) struct DirectiveIndex(usize);
3161
3162#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3163pub(crate) struct AutolinkCandidateIndex(usize);
3164
3165/// A GFM autolink literal the first pass found but did not commit to. The
3166/// `Link` is allocated up front so firing is only a body swap.
3167#[derive(Copy, Clone, Debug)]
3168pub(crate) struct AutolinkCandidate {
3169    /// Precedes the trigger byte when the email scan walks back over the local
3170    /// part.
3171    pub start: usize,
3172    pub end: usize,
3173    pub link: LinkIndex,
3174}
3175
3176/// A parsed JSX attribute.
3177#[cfg(feature = "mdx")]
3178#[derive(Debug, Clone)]
3179pub(crate) enum JsxAttr<'a> {
3180    Boolean(CowStr<'a>),
3181    Literal(CowStr<'a>, CowStr<'a>),
3182    /// `name={value}`. The two `usize`s are the byte range of `value` within
3183    /// the opening tag, so a parse error can be validated against the verbatim
3184    /// source slice and resolved to an exact source position.
3185    Expression(CowStr<'a>, CowStr<'a>, usize, usize),
3186    /// `{...value}`. The two `usize`s are the byte range of `value` (including
3187    /// the leading `...`) within the opening tag.
3188    Spread(CowStr<'a>, usize, usize),
3189}
3190
3191#[cfg(feature = "mdx")]
3192impl<'a> JsxAttr<'a> {
3193    pub fn into_static(self) -> JsxAttr<'static> {
3194        match self {
3195            JsxAttr::Boolean(n) => JsxAttr::Boolean(n.into_static()),
3196            JsxAttr::Literal(n, v) => JsxAttr::Literal(n.into_static(), v.into_static()),
3197            JsxAttr::Expression(n, v, start, end) => {
3198                JsxAttr::Expression(n.into_static(), v.into_static(), start, end)
3199            }
3200            JsxAttr::Spread(v, start, end) => JsxAttr::Spread(v.into_static(), start, end),
3201        }
3202    }
3203}
3204
3205/// Pre-parsed JSX element data (name + attributes + tag classification).
3206#[cfg(feature = "mdx")]
3207#[derive(Debug, Clone)]
3208pub(crate) struct JsxElementData<'a> {
3209    pub name: CowStr<'a>,
3210    pub attrs: Vec<JsxAttr<'a>>,
3211    pub raw: CowStr<'a>,
3212    pub is_closing: bool,
3213    pub is_self_closing: bool,
3214}
3215
3216#[cfg(feature = "mdx")]
3217impl<'a> JsxElementData<'a> {
3218    pub fn into_static(self) -> JsxElementData<'static> {
3219        JsxElementData {
3220            name: self.name.into_static(),
3221            attrs: self.attrs.into_iter().map(|a| a.into_static()).collect(),
3222            raw: self.raw.into_static(),
3223            is_closing: self.is_closing,
3224            is_self_closing: self.is_self_closing,
3225        }
3226    }
3227}
3228
3229#[derive(Debug, Clone)]
3230pub(crate) struct DirectiveAttrData<'a> {
3231    pub name: CowStr<'a>,
3232    pub attributes: Vec<(CowStr<'a>, CowStr<'a>)>,
3233    pub label_start: usize,
3234    pub label_end: usize,
3235    /// Cols of leading whitespace before `:::` on the opening line, after
3236    /// outer-container prefix stripping. Mirrors micromark-extension-directive's
3237    /// `initialSize`, which controls how much the directive body's per-line
3238    /// linePrefix is stripped (up to `initialSize + 1` cols). Only meaningful
3239    /// for container directives — leaf/text directives leave this 0.
3240    pub initial_size: u8,
3241}
3242
3243#[derive(Clone)]
3244pub(crate) struct Allocations<'a> {
3245    pub refdefs: RefDefs<'a>,
3246    /// Every refdef occurrence in source order, including duplicates that
3247    /// `refdefs` drops (it's a map and only keeps the first per label, since
3248    /// resolution picks the first match per CommonMark). Used to emit every
3249    /// definition as its own mdast `definition` node.
3250    pub refdefs_all: Vec<(LinkLabel<'a>, LinkDef<'a>)>,
3251    pub footdefs: FootnoteDefs<'a>,
3252    links: Vec<(LinkType, CowStr<'a>, CowStr<'a>, CowStr<'a>)>,
3253    cows: Vec<CowStr<'a>>,
3254    alignments: Vec<Vec<Alignment>>,
3255    headings: Vec<HeadingAttributes<'a>>,
3256    #[cfg(feature = "mdx")]
3257    jsx_elements: Vec<JsxElementData<'a>>,
3258    directives: Vec<DirectiveAttrData<'a>>,
3259    autolink_candidates: Vec<AutolinkCandidate>,
3260}
3261
3262/// Used by the heading attributes extension.
3263#[derive(Clone)]
3264pub(crate) struct HeadingAttributes<'a> {
3265    pub id: Option<CowStr<'a>>,
3266    pub classes: Vec<CowStr<'a>>,
3267    pub attrs: Vec<(CowStr<'a>, Option<CowStr<'a>>)>,
3268}
3269
3270/// Keeps track of the reference definitions defined in the document.
3271#[derive(Clone, Default, Debug)]
3272pub struct RefDefs<'input>(pub(crate) FxHashMap<LinkLabel<'input>, LinkDef<'input>>);
3273
3274/// Keeps track of the footnote definitions defined in the document.
3275#[derive(Clone, Default, Debug)]
3276pub struct FootnoteDefs<'input>(pub(crate) FxHashMap<FootnoteLabel<'input>, FootnoteDef>);
3277
3278impl<'input, 'b, 's> RefDefs<'input>
3279where
3280    's: 'b,
3281{
3282    /// Performs a lookup on reference label using unicode case folding.
3283    pub fn get(&'s self, key: &'b str) -> Option<&'b LinkDef<'input>> {
3284        self.0.get(&UniCase::new(key.into()))
3285    }
3286
3287    /// Provides an iterator over all the document's reference definitions.
3288    pub fn iter(
3289        &'s self,
3290    ) -> impl Iterator<Item = (&'s str, &'s LinkDef<'input>)> + use<'s, 'input> {
3291        self.0.iter().map(|(k, v)| (k.as_ref(), v))
3292    }
3293}
3294
3295impl<'input, 'b, 's> FootnoteDefs<'input>
3296where
3297    's: 'b,
3298{
3299    /// Performs a lookup on reference label using unicode case folding.
3300    pub fn contains(&'s self, key: &'b str) -> bool {
3301        self.0.contains_key(&UniCase::new(key.into()))
3302    }
3303    /// Performs a lookup on reference label using unicode case folding.
3304    pub fn get_mut(&'s mut self, key: CowStr<'input>) -> Option<&'s mut FootnoteDef> {
3305        self.0.get_mut(&UniCase::new(key))
3306    }
3307}
3308
3309impl<'a> Allocations<'a> {
3310    pub fn new() -> Self {
3311        Self {
3312            refdefs: RefDefs::default(),
3313            refdefs_all: Vec::new(),
3314            footdefs: FootnoteDefs::default(),
3315            links: Vec::with_capacity(128),
3316            cows: Vec::new(),
3317            alignments: Vec::new(),
3318            headings: Vec::new(),
3319            #[cfg(feature = "mdx")]
3320            jsx_elements: Vec::new(),
3321            directives: Vec::new(),
3322            autolink_candidates: Vec::new(),
3323        }
3324    }
3325
3326    pub fn allocate_autolink_candidate(
3327        &mut self,
3328        candidate: AutolinkCandidate,
3329    ) -> AutolinkCandidateIndex {
3330        let ix = self.autolink_candidates.len();
3331        self.autolink_candidates.push(candidate);
3332        AutolinkCandidateIndex(ix)
3333    }
3334
3335    pub fn allocate_cow(&mut self, cow: CowStr<'a>) -> CowIndex {
3336        let ix = self.cows.len();
3337        self.cows.push(cow);
3338        CowIndex(ix)
3339    }
3340
3341    pub fn allocate_link(
3342        &mut self,
3343        ty: LinkType,
3344        url: CowStr<'a>,
3345        title: CowStr<'a>,
3346        id: CowStr<'a>,
3347    ) -> LinkIndex {
3348        let ix = self.links.len();
3349        self.links.push((ty, url, title, id));
3350        LinkIndex(ix)
3351    }
3352
3353    pub fn allocate_alignment(&mut self, alignment: Vec<Alignment>) -> AlignmentIndex {
3354        let ix = self.alignments.len();
3355        self.alignments.push(alignment);
3356        AlignmentIndex(ix)
3357    }
3358
3359    pub fn allocate_heading(&mut self, attrs: HeadingAttributes<'a>) -> HeadingIndex {
3360        let ix = self.headings.len();
3361        self.headings.push(attrs);
3362        // This won't panic. `self.headings.len()` can't be `usize::MAX` since
3363        // such a long Vec cannot fit in memory.
3364        let ix_nonzero = NonZeroUsize::new(ix.wrapping_add(1)).expect("too many headings");
3365        HeadingIndex(ix_nonzero)
3366    }
3367
3368    pub fn take_cow(&mut self, ix: CowIndex) -> CowStr<'a> {
3369        core::mem::replace(&mut self.cows[ix.0], "".into())
3370    }
3371
3372    pub fn take_link(&mut self, ix: LinkIndex) -> (LinkType, CowStr<'a>, CowStr<'a>, CowStr<'a>) {
3373        let default_link = (LinkType::ShortcutUnknown, "".into(), "".into(), "".into());
3374        core::mem::replace(&mut self.links[ix.0], default_link)
3375    }
3376
3377    pub fn take_alignment(&mut self, ix: AlignmentIndex) -> Vec<Alignment> {
3378        core::mem::take(&mut self.alignments[ix.0])
3379    }
3380
3381    #[cfg(feature = "mdx")]
3382    pub fn allocate_jsx_element(&mut self, data: JsxElementData<'a>) -> JsxElementIndex {
3383        let ix = self.jsx_elements.len();
3384        self.jsx_elements.push(data);
3385        JsxElementIndex(ix)
3386    }
3387
3388    pub fn allocate_directive(&mut self, data: DirectiveAttrData<'a>) -> DirectiveIndex {
3389        let ix = self.directives.len();
3390        self.directives.push(data);
3391        DirectiveIndex(ix)
3392    }
3393
3394    pub fn take_directive(&mut self, ix: DirectiveIndex) -> DirectiveAttrData<'a> {
3395        core::mem::replace(
3396            &mut self.directives[ix.0],
3397            DirectiveAttrData {
3398                name: "".into(),
3399                attributes: Vec::new(),
3400                label_start: 0,
3401                label_end: 0,
3402                initial_size: 0,
3403            },
3404        )
3405    }
3406
3407    pub fn directive_ref(&self, ix: DirectiveIndex) -> &DirectiveAttrData<'a> {
3408        &self.directives[ix.0]
3409    }
3410
3411    #[cfg(feature = "mdx")]
3412    pub fn take_jsx_element(&mut self, ix: JsxElementIndex) -> JsxElementData<'a> {
3413        core::mem::replace(
3414            &mut self.jsx_elements[ix.0],
3415            JsxElementData {
3416                name: "".into(),
3417                attrs: Vec::new(),
3418                raw: "".into(),
3419                is_closing: false,
3420                is_self_closing: false,
3421            },
3422        )
3423    }
3424}
3425
3426impl<'a> Index<CowIndex> for Allocations<'a> {
3427    type Output = CowStr<'a>;
3428
3429    fn index(&self, ix: CowIndex) -> &Self::Output {
3430        self.cows.index(ix.0)
3431    }
3432}
3433
3434impl<'a> Index<LinkIndex> for Allocations<'a> {
3435    type Output = (LinkType, CowStr<'a>, CowStr<'a>, CowStr<'a>);
3436
3437    fn index(&self, ix: LinkIndex) -> &Self::Output {
3438        self.links.index(ix.0)
3439    }
3440}
3441
3442impl<'a> Index<AutolinkCandidateIndex> for Allocations<'a> {
3443    type Output = AutolinkCandidate;
3444
3445    fn index(&self, ix: AutolinkCandidateIndex) -> &Self::Output {
3446        self.autolink_candidates.index(ix.0)
3447    }
3448}
3449
3450impl<'a> Index<AlignmentIndex> for Allocations<'a> {
3451    type Output = Vec<Alignment>;
3452
3453    fn index(&self, ix: AlignmentIndex) -> &Self::Output {
3454        self.alignments.index(ix.0)
3455    }
3456}
3457
3458impl<'a> Index<HeadingIndex> for Allocations<'a> {
3459    type Output = HeadingAttributes<'a>;
3460
3461    fn index(&self, ix: HeadingIndex) -> &Self::Output {
3462        self.headings.index(ix.0.get() - 1)
3463    }
3464}
3465
3466/// A struct containing information on the reachability of certain inline HTML
3467/// elements. In particular, for cdata elements (`<![CDATA[`), processing
3468/// elements (`<?`) and declarations (`<!DECLARATION`). The respectives usizes
3469/// represent the indices before which a scan will always fail and can hence
3470/// be skipped.
3471#[derive(Clone, Default)]
3472pub(crate) struct HtmlScanGuard {
3473    pub cdata: usize,
3474    pub processing: usize,
3475    pub declaration: usize,
3476    pub comment: usize,
3477}
3478
3479/// Trait to customize [`Parser`] behavior with callbacks. See [`Parser::new_with_callbacks`].
3480///
3481/// All methods have a default implementation, so you can choose which ones to override.
3482pub trait ParserCallbacks<'input> {
3483    /// Potentially provide a custom definition for a broken link.
3484    ///
3485    /// In case the parser encounters any potential links that have a broken
3486    /// reference (e.g `[foo]` when there is no `[foo]: ` entry at the bottom)
3487    /// this callback will be called with information about the reference,
3488    /// and the returned pair will be used as the link URL and title if it is not
3489    /// `None`.
3490    fn handle_broken_link(
3491        &mut self,
3492        #[allow(unused_variables)] link: BrokenLink<'input>,
3493    ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3494        None
3495    }
3496}
3497
3498/// Wrapper to implement [`ParserCallbacks::handle_broken_link`] with a closure.
3499///
3500/// Used internally by [`Parser::new_with_broken_link_callback`].
3501#[allow(missing_debug_implementations)]
3502pub struct BrokenLinkCallback<F>(Option<F>);
3503
3504impl<'input, F> ParserCallbacks<'input> for BrokenLinkCallback<F>
3505where
3506    F: FnMut(BrokenLink<'input>) -> Option<(CowStr<'input>, CowStr<'input>)>,
3507{
3508    fn handle_broken_link(
3509        &mut self,
3510        link: BrokenLink<'input>,
3511    ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3512        self.0.as_mut().and_then(|cb| cb(link))
3513    }
3514}
3515
3516impl<'input> ParserCallbacks<'input> for Box<dyn ParserCallbacks<'input>> {
3517    fn handle_broken_link(
3518        &mut self,
3519        link: BrokenLink<'input>,
3520    ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3521        (**self).handle_broken_link(link)
3522    }
3523}
3524
3525/// [Parser] callbacks that do nothing.
3526///
3527/// Used when no custom callbacks are provided.
3528#[allow(missing_debug_implementations)]
3529pub struct DefaultParserCallbacks;
3530
3531impl<'input> ParserCallbacks<'input> for DefaultParserCallbacks {}
3532
3533/// Markdown event and source range iterator.
3534///
3535/// Generates tuples where the first element is the markdown event and the second
3536/// is a the corresponding range in the source string.
3537///
3538/// Constructed from a `Parser` using its
3539/// [`into_offset_iter`](struct.Parser.html#method.into_offset_iter) method.
3540#[derive(Debug)]
3541pub struct OffsetIter<'a, CB> {
3542    parser: Parser<'a, CB>,
3543}
3544
3545impl<'a, CB: ParserCallbacks<'a>> OffsetIter<'a, CB> {
3546    /// Returns a reference to the internal reference definition tracker.
3547    pub fn reference_definitions(&self) -> &RefDefs<'_> {
3548        self.parser.reference_definitions()
3549    }
3550
3551    /// Returns MDX validation errors collected during parsing.
3552    pub fn mdx_errors(&self) -> &[(usize, String)] {
3553        self.parser.mdx_errors()
3554    }
3555}
3556
3557impl<'a, CB: ParserCallbacks<'a>> Iterator for OffsetIter<'a, CB> {
3558    type Item = (Event<'a>, Range<usize>);
3559
3560    fn next(&mut self) -> Option<Self::Item> {
3561        self.parser
3562            .inner
3563            .next_event_range(&mut self.parser.callbacks)
3564    }
3565}
3566
3567impl<'a, CB: ParserCallbacks<'a>> Iterator for Parser<'a, CB> {
3568    type Item = Event<'a>;
3569
3570    fn next(&mut self) -> Option<Event<'a>> {
3571        self.inner
3572            .next_event_range(&mut self.callbacks)
3573            .map(|(event, _range)| event)
3574    }
3575}
3576
3577impl<'a, CB: ParserCallbacks<'a>> FusedIterator for Parser<'a, CB> {}
3578
3579impl<'input> ParserInner<'input> {
3580    fn next_event_range(
3581        &mut self,
3582        callbacks: &mut dyn ParserCallbacks<'input>,
3583    ) -> Option<(Event<'input>, Range<usize>)> {
3584        match self.tree.cur() {
3585            None => {
3586                let ix = self.tree.pop()?;
3587                let ix = if matches!(self.tree[ix].item.body, ItemBody::TightParagraph) {
3588                    // tight paragraphs emit nothing
3589                    self.tree.next_sibling(ix);
3590                    return self.next_event_range(callbacks);
3591                } else {
3592                    ix
3593                };
3594                let tag_end = body_to_tag_end(&self.tree[ix].item.body);
3595                self.tree.next_sibling(ix);
3596                let span = self.tree[ix].item.start..self.tree[ix].item.end;
3597                debug_assert!(span.start <= span.end);
3598                Some((Event::End(tag_end), span))
3599            }
3600            Some(cur_ix) => {
3601                let cur_ix = if matches!(self.tree[cur_ix].item.body, ItemBody::TightParagraph) {
3602                    // tight paragraphs emit nothing
3603                    self.tree.push();
3604                    self.tree.cur().unwrap()
3605                } else {
3606                    cur_ix
3607                };
3608                if self.tree[cur_ix].item.body.is_maybe_inline() {
3609                    self.handle_inline(callbacks);
3610                }
3611
3612                let node = self.tree[cur_ix];
3613                let item = node.item;
3614                let event = item_to_event(item, self.text, &mut self.allocs);
3615                if let Event::Start(..) = event {
3616                    self.tree.push();
3617                } else {
3618                    self.tree.next_sibling(cur_ix);
3619                }
3620                debug_assert!(item.start <= item.end);
3621                Some((event, item.start..item.end))
3622            }
3623        }
3624    }
3625}
3626
3627fn body_to_tag_end(body: &ItemBody) -> TagEnd {
3628    match *body {
3629        ItemBody::Paragraph => TagEnd::Paragraph,
3630        ItemBody::Emphasis => TagEnd::Emphasis,
3631        ItemBody::Superscript => TagEnd::Superscript,
3632        ItemBody::Subscript => TagEnd::Subscript,
3633        ItemBody::Strong => TagEnd::Strong,
3634        ItemBody::Strikethrough => TagEnd::Strikethrough,
3635        ItemBody::Link(..) => TagEnd::Link,
3636        ItemBody::Image(..) => TagEnd::Image,
3637        ItemBody::Heading(level, _) => TagEnd::Heading(level),
3638        ItemBody::IndentCodeBlock(..) | ItemBody::FencedCodeBlock(..) | ItemBody::MathBlock(..) => {
3639            TagEnd::CodeBlock
3640        }
3641        ItemBody::ContainerDirective(..) => TagEnd::Directive(DirectiveKind::Container),
3642        ItemBody::LeafDirective(..) => TagEnd::Directive(DirectiveKind::Leaf),
3643        ItemBody::TextDirective(..) => TagEnd::Directive(DirectiveKind::Text),
3644        ItemBody::BlockQuote(kind) => TagEnd::BlockQuote(kind),
3645        ItemBody::HtmlBlock(_) => TagEnd::HtmlBlock,
3646        ItemBody::List(_, c, _) => {
3647            let is_ordered = c == b'.' || c == b')';
3648            TagEnd::List(is_ordered)
3649        }
3650        ItemBody::ListItem(_, _) => TagEnd::Item,
3651        ItemBody::TableHead => TagEnd::TableHead,
3652        ItemBody::TableCell => TagEnd::TableCell,
3653        ItemBody::TableRow => TagEnd::TableRow,
3654        ItemBody::Table(..) => TagEnd::Table,
3655        ItemBody::FootnoteDefinition(..) => TagEnd::FootnoteDefinition,
3656        ItemBody::MetadataBlock(kind) => TagEnd::MetadataBlock(kind),
3657        ItemBody::DefinitionList(_) => TagEnd::DefinitionList,
3658        ItemBody::DefinitionListTitle => TagEnd::DefinitionListTitle,
3659        ItemBody::DefinitionListDefinition(..) => TagEnd::DefinitionListDefinition,
3660        #[cfg(feature = "mdx")]
3661        ItemBody::MdxJsxFlowElement(..) => TagEnd::MdxJsxFlowElement,
3662        #[cfg(feature = "mdx")]
3663        ItemBody::MdxJsxTextElement(..) => TagEnd::MdxJsxTextElement,
3664        _ => panic!("unexpected item body {:?}", body),
3665    }
3666}
3667
3668fn item_to_event<'a>(item: Item, text: &'a str, allocs: &mut Allocations<'a>) -> Event<'a> {
3669    let tag = match item.body {
3670        ItemBody::Text { .. } => return Event::Text(text[item.start..item.end].into()),
3671        ItemBody::Code(cow_ix) => return Event::Code(allocs.take_cow(cow_ix)),
3672        ItemBody::SynthesizeText(cow_ix) => return Event::Text(allocs.take_cow(cow_ix)),
3673        ItemBody::SynthesizeChar(c) => return Event::Text(c.into()),
3674        ItemBody::HtmlBlock(_) => Tag::HtmlBlock,
3675        ItemBody::Html => return Event::Html(text[item.start..item.end].into()),
3676        ItemBody::InlineHtml => return Event::InlineHtml(text[item.start..item.end].into()),
3677        ItemBody::OwnedInlineHtml(cow_ix) => return Event::InlineHtml(allocs.take_cow(cow_ix)),
3678        ItemBody::SoftBreak => return Event::SoftBreak,
3679        ItemBody::HardBreak(_) => return Event::HardBreak,
3680        ItemBody::FootnoteReference(cow_ix) => {
3681            return Event::FootnoteReference(allocs.take_cow(cow_ix));
3682        }
3683        ItemBody::TaskListMarker(checked) => return Event::TaskListMarker(checked),
3684        ItemBody::Rule => return Event::Rule,
3685        ItemBody::Paragraph => Tag::Paragraph,
3686        ItemBody::Emphasis => Tag::Emphasis,
3687        ItemBody::Superscript => Tag::Superscript,
3688        ItemBody::Subscript => Tag::Subscript,
3689        ItemBody::Strong => Tag::Strong,
3690        ItemBody::Strikethrough => Tag::Strikethrough,
3691        ItemBody::Link(link_ix) => {
3692            let (link_type, dest_url, title, id) = allocs.take_link(link_ix);
3693            Tag::Link {
3694                link_type,
3695                dest_url,
3696                title,
3697                id,
3698            }
3699        }
3700        ItemBody::Image(link_ix) => {
3701            let (link_type, dest_url, title, id) = allocs.take_link(link_ix);
3702            Tag::Image {
3703                link_type,
3704                dest_url,
3705                title,
3706                id,
3707            }
3708        }
3709        ItemBody::Heading(level, Some(heading_ix)) => {
3710            let HeadingAttributes { id, classes, attrs } = allocs.index(heading_ix);
3711            Tag::Heading {
3712                level,
3713                id: id.clone(),
3714                classes: classes.clone(),
3715                attrs: attrs.clone(),
3716            }
3717        }
3718        ItemBody::Heading(level, None) => Tag::Heading {
3719            level,
3720            id: None,
3721            classes: Vec::new(),
3722            attrs: Vec::new(),
3723        },
3724        ItemBody::MathBlock(cow_ix) => {
3725            Tag::CodeBlock(CodeBlockKind::Fenced(allocs.take_cow(cow_ix)))
3726        }
3727        ItemBody::FencedCodeBlock(cow_ix, _) => {
3728            Tag::CodeBlock(CodeBlockKind::Fenced(allocs.take_cow(cow_ix)))
3729        }
3730        ItemBody::IndentCodeBlock(..) => Tag::CodeBlock(CodeBlockKind::Indented),
3731        ItemBody::ContainerDirective(_, dir_ix)
3732        | ItemBody::LeafDirective(dir_ix)
3733        | ItemBody::TextDirective(dir_ix) => {
3734            let kind = match item.body {
3735                ItemBody::ContainerDirective(..) => DirectiveKind::Container,
3736                ItemBody::LeafDirective(..) => DirectiveKind::Leaf,
3737                _ => DirectiveKind::Text,
3738            };
3739            let dir = allocs.take_directive(dir_ix);
3740            Tag::Directive {
3741                kind,
3742                name: dir.name,
3743                attributes: dir.attributes,
3744            }
3745        }
3746        ItemBody::BlockQuote(kind) => Tag::BlockQuote(kind),
3747        ItemBody::List(is_tight, c, listitem_start) => {
3748            if c == b'.' || c == b')' {
3749                Tag::List(Some(listitem_start), is_tight)
3750            } else {
3751                Tag::List(None, is_tight)
3752            }
3753        }
3754        ItemBody::ListItem(_, _) => Tag::Item,
3755        ItemBody::TableHead => Tag::TableHead,
3756        ItemBody::TableCell => Tag::TableCell,
3757        ItemBody::TableRow => Tag::TableRow,
3758        ItemBody::Table(alignment_ix) => Tag::Table(allocs.take_alignment(alignment_ix)),
3759        ItemBody::FootnoteDefinition(cow_ix) => Tag::FootnoteDefinition(allocs.take_cow(cow_ix)),
3760        ItemBody::MetadataBlock(kind) => Tag::MetadataBlock(kind),
3761        ItemBody::Math(cow_ix, is_display) => {
3762            return if is_display {
3763                Event::DisplayMath(allocs.take_cow(cow_ix))
3764            } else {
3765                Event::InlineMath(allocs.take_cow(cow_ix))
3766            };
3767        }
3768        ItemBody::DefinitionList(_) => Tag::DefinitionList,
3769        ItemBody::DefinitionListTitle => Tag::DefinitionListTitle,
3770        ItemBody::DefinitionListDefinition(..) => Tag::DefinitionListDefinition,
3771        #[cfg(feature = "mdx")]
3772        ItemBody::MdxJsxFlowElement(jsx_ix) => {
3773            let jsx = allocs.take_jsx_element(jsx_ix);
3774            Tag::MdxJsxFlowElement(jsx.raw)
3775        }
3776        #[cfg(feature = "mdx")]
3777        ItemBody::MdxJsxTextElement(jsx_ix) => {
3778            let jsx = allocs.take_jsx_element(jsx_ix);
3779            Tag::MdxJsxTextElement(jsx.raw)
3780        }
3781        #[cfg(feature = "mdx")]
3782        ItemBody::MdxFlowExpression(cow_ix) => {
3783            return Event::MdxFlowExpression(allocs.take_cow(cow_ix));
3784        }
3785        #[cfg(feature = "mdx")]
3786        ItemBody::MdxTextExpression(cow_ix) => {
3787            return Event::MdxTextExpression(allocs.take_cow(cow_ix));
3788        }
3789        #[cfg(feature = "mdx")]
3790        ItemBody::MdxEsm(cow_ix) => return Event::MdxEsm(allocs.take_cow(cow_ix)),
3791        _ => panic!("unexpected item body {:?}", item.body),
3792    };
3793
3794    Event::Start(tag)
3795}
3796
3797#[cfg(test)]
3798mod test {
3799    use alloc::{borrow::ToOwned, string::ToString, vec::Vec};
3800
3801    use super::*;
3802    use crate::tree::Node;
3803
3804    // TODO: move these tests to tests/html.rs?
3805
3806    fn parser_with_extensions(text: &str) -> Parser<'_> {
3807        let mut opts = Options::empty();
3808        opts.insert(Options::ENABLE_TABLES);
3809        opts.insert(Options::ENABLE_FOOTNOTES);
3810        opts.insert(Options::ENABLE_STRIKETHROUGH);
3811        opts.insert(Options::ENABLE_SUPERSCRIPT);
3812        opts.insert(Options::ENABLE_SUBSCRIPT);
3813        opts.insert(Options::ENABLE_TASKLISTS);
3814
3815        Parser::new_ext(text, opts)
3816    }
3817
3818    #[test]
3819    #[cfg(target_pointer_width = "64")]
3820    fn node_size() {
3821        let node_size = core::mem::size_of::<Node<Item>>();
3822        assert_eq!(48, node_size);
3823    }
3824
3825    #[test]
3826    #[cfg(target_pointer_width = "64")]
3827    fn body_size() {
3828        let body_size = core::mem::size_of::<ItemBody>();
3829        assert_eq!(16, body_size);
3830    }
3831
3832    #[test]
3833    fn single_open_fish_bracket() {
3834        // dont crash
3835        assert_eq!(3, Parser::new("<").count());
3836    }
3837
3838    #[test]
3839    fn lone_hashtag() {
3840        // dont crash
3841        assert_eq!(2, Parser::new("#").count());
3842    }
3843
3844    #[test]
3845    fn lots_of_backslashes() {
3846        // dont crash
3847        Parser::new("\\\\\r\r").count();
3848        Parser::new("\\\r\r\\.\\\\\r\r\\.\\").count();
3849    }
3850
3851    #[test]
3852    fn issue_1030() {
3853        let mut opts = Options::empty();
3854        opts.insert(Options::ENABLE_WIKILINKS);
3855
3856        let parser = Parser::new_ext("For a new ferrari, [[Wikientry|click here]]!", opts);
3857
3858        let offsets = parser
3859            .into_offset_iter()
3860            .map(|(_ev, range)| range)
3861            .collect::<Vec<_>>();
3862        let expected_offsets = vec![
3863            (0..44),  // Paragraph START
3864            (0..19),  // `For a new ferrari, `
3865            (19..43), // Wikilink START
3866            (31..41), // `click here`
3867            (19..43), // Wikilink END
3868            (43..44), // `!`
3869            (0..44),  // Paragraph END
3870        ];
3871        assert_eq!(offsets, expected_offsets);
3872    }
3873
3874    #[test]
3875    fn issue_320() {
3876        // dont crash
3877        parser_with_extensions(":\r\t> |\r:\r\t> |\r").count();
3878    }
3879
3880    #[test]
3881    fn issue_319() {
3882        // dont crash
3883        parser_with_extensions("|\r-]([^|\r-]([^").count();
3884        parser_with_extensions("|\r\r=][^|\r\r=][^car").count();
3885    }
3886
3887    #[test]
3888    fn issue_303() {
3889        // dont crash
3890        parser_with_extensions("[^\r\ra]").count();
3891        parser_with_extensions("\r\r]Z[^\x00\r\r]Z[^\x00").count();
3892    }
3893
3894    #[test]
3895    fn issue_313() {
3896        // dont crash
3897        parser_with_extensions("*]0[^\r\r*]0[^").count();
3898        parser_with_extensions("[^\r> `][^\r> `][^\r> `][").count();
3899    }
3900
3901    #[test]
3902    fn issue_311() {
3903        // dont crash
3904        parser_with_extensions("\\\u{0d}-\u{09}\\\u{0d}-\u{09}").count();
3905    }
3906
3907    #[test]
3908    fn issue_283() {
3909        let input = core::str::from_utf8(b"\xf0\x9b\xb2\x9f<td:^\xf0\x9b\xb2\x9f").unwrap();
3910        // dont crash
3911        parser_with_extensions(input).count();
3912    }
3913
3914    #[test]
3915    fn issue_289() {
3916        // dont crash
3917        parser_with_extensions("> - \\\n> - ").count();
3918        parser_with_extensions("- \n\n").count();
3919    }
3920
3921    #[test]
3922    fn issue_306() {
3923        // dont crash
3924        parser_with_extensions("*\r_<__*\r_<__*\r_<__*\r_<__").count();
3925    }
3926
3927    #[test]
3928    fn issue_305() {
3929        // dont crash
3930        parser_with_extensions("_6**6*_*").count();
3931    }
3932
3933    #[test]
3934    fn another_emphasis_panic() {
3935        parser_with_extensions("*__#_#__*").count();
3936    }
3937
3938    #[test]
3939    fn offset_iter() {
3940        let event_offsets: Vec<_> = Parser::new("*hello* world")
3941            .into_offset_iter()
3942            .map(|(_ev, range)| range)
3943            .collect();
3944        let expected_offsets = vec![(0..13), (0..7), (1..6), (0..7), (7..13), (0..13)];
3945        assert_eq!(expected_offsets, event_offsets);
3946    }
3947
3948    #[test]
3949    fn reference_link_offsets() {
3950        let range =
3951            Parser::new("# H1\n[testing][Some reference]\n\n[Some reference]: https://github.com")
3952                .into_offset_iter()
3953                .filter_map(|(ev, range)| match ev {
3954                    Event::Start(
3955                        Tag::Link {
3956                            link_type: LinkType::Reference,
3957                            ..
3958                        },
3959                        ..,
3960                    ) => Some(range),
3961                    _ => None,
3962                })
3963                .next()
3964                .unwrap();
3965        assert_eq!(5..30, range);
3966    }
3967
3968    #[test]
3969    fn footnote_offsets() {
3970        let range = parser_with_extensions("Testing this[^1] out.\n\n[^1]: Footnote.")
3971            .into_offset_iter()
3972            .filter_map(|(ev, range)| match ev {
3973                Event::FootnoteReference(..) => Some(range),
3974                _ => None,
3975            })
3976            .next()
3977            .unwrap();
3978        assert_eq!(12..16, range);
3979    }
3980
3981    #[test]
3982    fn footnote_offsets_exclamation() {
3983        let mut immediately_before_footnote = None;
3984        let range = parser_with_extensions("Testing this![^1] out.\n\n[^1]: Footnote.")
3985            .into_offset_iter()
3986            .filter_map(|(ev, range)| match ev {
3987                Event::FootnoteReference(..) => Some(range),
3988                _ => {
3989                    immediately_before_footnote = Some((ev, range));
3990                    None
3991                }
3992            })
3993            .next()
3994            .unwrap();
3995        assert_eq!(13..17, range);
3996        if let (Event::Text(exclamation), range_exclamation) =
3997            immediately_before_footnote.as_ref().unwrap()
3998        {
3999            assert_eq!("!", &exclamation[..]);
4000            assert_eq!(&(12..13), range_exclamation);
4001        } else {
4002            panic!("what came first, then? {immediately_before_footnote:?}");
4003        }
4004    }
4005
4006    #[test]
4007    fn table_offset() {
4008        let markdown = "a\n\nTesting|This|Outtt\n--|:--:|--:\nSome Data|Other data|asdf";
4009        let event_offset = parser_with_extensions(markdown)
4010            .into_offset_iter()
4011            .map(|(_ev, range)| range)
4012            .nth(3)
4013            .unwrap();
4014        let expected_offset = 3..59;
4015        assert_eq!(expected_offset, event_offset);
4016    }
4017
4018    #[test]
4019    fn table_cell_span() {
4020        let markdown = "a|b|c\n--|--|--\na|  |c";
4021        let event_offset = parser_with_extensions(markdown)
4022            .into_offset_iter()
4023            .filter_map(|(ev, span)| match ev {
4024                Event::Start(Tag::TableCell) => Some(span),
4025                _ => None,
4026            })
4027            .nth(4)
4028            .unwrap();
4029        // Cell span includes the leading `|` delimiter (matching remark).
4030        let expected_offset_start = "a|b|c\n--|--|--\na".len();
4031        assert_eq!(
4032            expected_offset_start..(expected_offset_start + 3),
4033            event_offset
4034        );
4035    }
4036
4037    #[test]
4038    fn offset_iter_issue_378() {
4039        let event_offsets: Vec<_> = Parser::new("a [b](c) d")
4040            .into_offset_iter()
4041            .map(|(_ev, range)| range)
4042            .collect();
4043        let expected_offsets = vec![(0..10), (0..2), (2..8), (3..4), (2..8), (8..10), (0..10)];
4044        assert_eq!(expected_offsets, event_offsets);
4045    }
4046
4047    #[test]
4048    fn offset_iter_issue_404() {
4049        let event_offsets: Vec<_> = Parser::new("###\n")
4050            .into_offset_iter()
4051            .map(|(_ev, range)| range)
4052            .collect();
4053        let expected_offsets = vec![(0..4), (0..4)];
4054        assert_eq!(expected_offsets, event_offsets);
4055    }
4056
4057    #[test]
4058    fn broken_links_called_only_once() {
4059        for &(markdown, expected) in &[
4060            ("See also [`g()`][crate::g].", 1),
4061            ("See also [`g()`][crate::g][].", 1),
4062            ("[brokenlink1] some other node [brokenlink2]", 2),
4063        ] {
4064            let mut times_called = 0;
4065            let callback = &mut |_broken_link: BrokenLink| {
4066                times_called += 1;
4067                None
4068            };
4069            let parser =
4070                Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(callback));
4071            for _ in parser {}
4072            assert_eq!(times_called, expected);
4073        }
4074    }
4075
4076    #[test]
4077    fn simple_broken_link_callback() {
4078        let test_str = "This is a link w/o def: [hello][world]";
4079        let mut callback = |broken_link: BrokenLink| {
4080            assert_eq!("world", broken_link.reference.as_ref());
4081            assert_eq!(&test_str[broken_link.span], "[hello][world]");
4082            let url = "YOLO".into();
4083            let title = "SWAG".to_owned().into();
4084            Some((url, title))
4085        };
4086        let parser =
4087            Parser::new_with_broken_link_callback(test_str, Options::empty(), Some(&mut callback));
4088        let mut link_tag_count = 0;
4089        for (typ, url, title, id) in parser.filter_map(|event| match event {
4090            Event::Start(Tag::Link {
4091                link_type,
4092                dest_url,
4093                title,
4094                id,
4095            }) => Some((link_type, dest_url, title, id)),
4096            _ => None,
4097        }) {
4098            link_tag_count += 1;
4099            assert_eq!(typ, LinkType::ReferenceUnknown);
4100            assert_eq!(url.as_ref(), "YOLO");
4101            assert_eq!(title.as_ref(), "SWAG");
4102            assert_eq!(id.as_ref(), "world");
4103        }
4104        assert!(link_tag_count > 0);
4105    }
4106
4107    #[test]
4108    fn code_block_kind_check_fenced() {
4109        let parser = Parser::new("hello\n```test\ntadam\n```");
4110        let mut found = 0;
4111        for (ev, _range) in parser.into_offset_iter() {
4112            if let Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(syntax))) = ev {
4113                assert_eq!(syntax.as_ref(), "test");
4114                found += 1;
4115            }
4116        }
4117        assert_eq!(found, 1);
4118    }
4119
4120    #[test]
4121    fn code_block_kind_check_indented() {
4122        let parser = Parser::new("hello\n\n    ```test\n    tadam\nhello");
4123        let mut found = 0;
4124        for (ev, _range) in parser.into_offset_iter() {
4125            if let Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) = ev {
4126                found += 1;
4127            }
4128        }
4129        assert_eq!(found, 1);
4130    }
4131
4132    #[test]
4133    fn ref_defs() {
4134        let input = r###"[a B c]: http://example.com
4135[another]: https://google.com
4136
4137text
4138
4139[final ONE]: http://wikipedia.org
4140"###;
4141        let mut parser = Parser::new(input);
4142
4143        assert!(parser.reference_definitions().get("a b c").is_some());
4144        assert!(parser.reference_definitions().get("nope").is_none());
4145
4146        if let Some(_event) = parser.next() {
4147            // testing keys with shorter lifetimes than parser and its input
4148            let s = "final one".to_owned();
4149            let link_def = parser.reference_definitions().get(&s).unwrap();
4150            let span = &input[link_def.span.clone()];
4151            assert_eq!(span, "[final ONE]: http://wikipedia.org");
4152        }
4153    }
4154
4155    #[test]
4156    #[allow(clippy::extra_unused_lifetimes)]
4157    fn common_lifetime_patterns_allowed<'b>() {
4158        let temporary_str = String::from("xyz");
4159
4160        // NOTE: this is a limitation of Rust, it doesn't allow putting lifetime parameters on the closure itself.
4161        // Hack it by attaching the lifetime to the test function instead.
4162        // TODO: why is the `'b` lifetime required at all? Changing it to `'_` breaks things :(
4163        let mut closure = |link: BrokenLink<'b>| Some(("#".into(), link.reference));
4164
4165        fn function(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)> {
4166            Some(("#".into(), link.reference))
4167        }
4168
4169        for _ in Parser::new_with_broken_link_callback(
4170            "static lifetime",
4171            Options::empty(),
4172            Some(&mut closure),
4173        ) {}
4174        /* This fails to compile. Because the closure can't say `for <'a> fn(BrokenLink<'a>) ->
4175         * CowStr<'a>` and has to use the enclosing `'b` lifetime parameter, `temporary_str` lives
4176         * shorter than `'b`. I think this is unlikely to occur in real life, and if it does, the
4177         * fix is simple: move it out to a function that allows annotating the lifetimes.
4178         */
4179        //for _ in Parser::new_with_broken_link_callback(&temporary_str, Options::empty(), Some(&mut callback)) {
4180        //}
4181
4182        for _ in Parser::new_with_broken_link_callback(
4183            "static lifetime",
4184            Options::empty(),
4185            Some(&mut function),
4186        ) {}
4187        for _ in Parser::new_with_broken_link_callback(
4188            &temporary_str,
4189            Options::empty(),
4190            Some(&mut function),
4191        ) {}
4192    }
4193
4194    #[test]
4195    fn inline_html_inside_blockquote() {
4196        // Regression for #960
4197        let input = "> <foo\n> bar>";
4198        let events: Vec<_> = Parser::new(input).collect();
4199        let expected = [
4200            Event::Start(Tag::BlockQuote(None)),
4201            Event::Start(Tag::Paragraph),
4202            Event::InlineHtml(CowStr::Boxed("<foo\nbar>".to_string().into())),
4203            Event::End(TagEnd::Paragraph),
4204            Event::End(TagEnd::BlockQuote(None)),
4205        ];
4206        assert_eq!(&events, &expected);
4207    }
4208
4209    #[test]
4210    fn wikilink_has_pothole() {
4211        let input = "[[foo]] [[bar|baz]]";
4212        let events: Vec<_> = Parser::new_ext(input, Options::ENABLE_WIKILINKS).collect();
4213        let expected = [
4214            Event::Start(Tag::Paragraph),
4215            Event::Start(Tag::Link {
4216                link_type: LinkType::WikiLink { has_pothole: false },
4217                dest_url: CowStr::Borrowed("foo"),
4218                title: CowStr::Borrowed(""),
4219                id: CowStr::Borrowed(""),
4220            }),
4221            Event::Text(CowStr::Borrowed("foo")),
4222            Event::End(TagEnd::Link),
4223            Event::Text(CowStr::Borrowed(" ")),
4224            Event::Start(Tag::Link {
4225                link_type: LinkType::WikiLink { has_pothole: true },
4226                dest_url: CowStr::Borrowed("bar"),
4227                title: CowStr::Borrowed(""),
4228                id: CowStr::Borrowed(""),
4229            }),
4230            Event::Text(CowStr::Borrowed("baz")),
4231            Event::End(TagEnd::Link),
4232            Event::End(TagEnd::Paragraph),
4233        ];
4234        assert_eq!(&events, &expected);
4235    }
4236
4237    #[cfg(feature = "mdx")]
4238    fn mdx_parser(text: &str) -> Parser<'_> {
4239        Parser::new_ext(text, Options::ENABLE_MDX)
4240    }
4241
4242    #[cfg(feature = "mdx")]
4243    #[test]
4244    fn mdx_esm_import() {
4245        let events: Vec<_> = mdx_parser("import {Chart} from './chart.js'\n").collect();
4246        assert_eq!(events.len(), 1);
4247        assert!(matches!(&events[0], Event::MdxEsm(s) if s.contains("import")));
4248    }
4249
4250    #[cfg(feature = "mdx")]
4251    #[test]
4252    fn mdx_esm_export() {
4253        let events: Vec<_> = mdx_parser("export const meta = {}\n").collect();
4254        assert_eq!(events.len(), 1);
4255        assert!(matches!(&events[0], Event::MdxEsm(s) if s.contains("export")));
4256    }
4257
4258    #[cfg(feature = "mdx")]
4259    #[test]
4260    fn mdx_flow_expression() {
4261        let events: Vec<_> = mdx_parser("{1 + 1}\n").collect();
4262        assert_eq!(events.len(), 1);
4263        assert!(matches!(&events[0], Event::MdxFlowExpression(s) if s.as_ref() == "1 + 1"));
4264    }
4265
4266    #[cfg(feature = "mdx")]
4267    #[test]
4268    fn mdx_jsx_flow_self_closing() {
4269        let events: Vec<_> = mdx_parser("<Chart values={[1,2,3]} />\n").collect();
4270        assert!(!events.is_empty());
4271        assert!(
4272            matches!(&events[0], Event::Start(Tag::MdxJsxFlowElement(s)) if s.contains("Chart"))
4273        );
4274    }
4275
4276    #[cfg(feature = "mdx")]
4277    #[test]
4278    fn mdx_jsx_flow_fragment() {
4279        let events: Vec<_> = mdx_parser("<>\n").collect();
4280        assert!(!events.is_empty());
4281        assert!(matches!(
4282            &events[0],
4283            Event::Start(Tag::MdxJsxFlowElement(_))
4284        ));
4285    }
4286
4287    #[cfg(feature = "mdx")]
4288    #[test]
4289    fn mdx_inline_expression() {
4290        let events: Vec<_> = mdx_parser("hello {name} world\n").collect();
4291        let has_expr = events
4292            .iter()
4293            .any(|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "name"));
4294        assert!(
4295            has_expr,
4296            "Expected inline MDX expression, got: {:?}",
4297            events
4298        );
4299    }
4300
4301    #[cfg(feature = "mdx")]
4302    #[test]
4303    fn mdx_inline_jsx() {
4304        let events: Vec<_> = mdx_parser("hello <Badge /> world\n").collect();
4305        let has_jsx = events
4306            .iter()
4307            .any(|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("Badge")));
4308        assert!(has_jsx, "Expected inline MDX JSX, got: {:?}", events);
4309    }
4310
4311    #[cfg(feature = "mdx")]
4312    #[test]
4313    fn mdx_all_tags_are_jsx() {
4314        // In MDX mode, all tags (including lowercase) are JSX, not HTML.
4315        let events: Vec<_> = mdx_parser("hello <em>world</em>\n").collect();
4316        let has_jsx = events
4317            .iter()
4318            .any(|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(_))));
4319        assert!(has_jsx, "In MDX mode, <em> should be JSX: {:?}", events);
4320    }
4321
4322    #[test]
4323    fn mdx_does_not_interfere_without_flag() {
4324        // Without ENABLE_MDX, none of this should be parsed as MDX.
4325        let events: Vec<_> = Parser::new("import foo from 'bar'\n").collect();
4326        // Should be a regular paragraph.
4327        assert!(
4328            events
4329                .iter()
4330                .any(|e| matches!(e, Event::Start(Tag::Paragraph)))
4331        );
4332    }
4333
4334    #[cfg(feature = "mdx")]
4335    #[test]
4336    fn mdx_expression_in_heading() {
4337        let events: Vec<_> = mdx_parser("# {title}\n").collect();
4338        let has_heading = events
4339            .iter()
4340            .any(|e| matches!(e, Event::Start(Tag::Heading { .. })));
4341        assert!(has_heading, "Should have a heading");
4342        let has_expr = events
4343            .iter()
4344            .any(|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "title"));
4345        assert!(
4346            has_expr,
4347            "Heading should contain MdxTextExpression, got: {:?}",
4348            events
4349        );
4350    }
4351
4352    #[cfg(feature = "mdx")]
4353    #[test]
4354    fn mdx_expression_mixed_text_in_heading() {
4355        let events: Vec<_> = mdx_parser("## Hello {name}\n").collect();
4356        let has_text = events
4357            .iter()
4358            .any(|e| matches!(e, Event::Text(s) if s.contains("Hello")));
4359        let has_expr = events
4360            .iter()
4361            .any(|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "name"));
4362        assert!(has_text, "Should have text, got: {:?}", events);
4363        assert!(has_expr, "Should have expression, got: {:?}", events);
4364    }
4365}