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((_, ReferenceLabel::Footnote(footlabel))) =
1407                                scan_link_label(&self.tree, first_bracket_text, self.options)
1408                                && self.allocs.footdefs.contains(&footlabel)
1409                            {
1410                                let footref = self.allocs.allocate_cow(footlabel);
1411                                if let Some(def) = self
1412                                    .allocs
1413                                    .footdefs
1414                                    .get_mut(self.allocs.cows[footref.0].to_owned())
1415                                {
1416                                    def.use_count += 1;
1417                                }
1418                                let footnote_ix = if tos.ty == LinkStackTy::Image {
1419                                    self.tree[tos.node].next = Some(cur_ix);
1420                                    self.tree[tos.node].child = None;
1421                                    self.tree[tos.node].item.body = ItemBody::SynthesizeChar('!');
1422                                    self.tree[cur_ix].item.start =
1423                                        self.tree[tos.node].item.start + 1;
1424                                    self.tree[tos.node].item.end =
1425                                        self.tree[tos.node].item.start + 1;
1426                                    cur_ix
1427                                } else {
1428                                    tos.node
1429                                };
1430                                self.tree[footnote_ix].next = next;
1431                                self.tree[footnote_ix].child = None;
1432                                self.tree[footnote_ix].item.body =
1433                                    ItemBody::FootnoteReference(footref);
1434                                self.tree[footnote_ix].item.end = first_bracket_end;
1435                                prev = Some(footnote_ix);
1436                                cur = next;
1437                                self.link_stack.clear();
1438                                continue;
1439                            }
1440                            // ok, so its not an inline link. maybe it is a reference
1441                            // to a defined link?
1442                            let scan_result =
1443                                scan_reference(&self.tree, block_text, next, self.options);
1444                            let (node_after_link, link_type) = match scan_result {
1445                                // [label][reference]
1446                                RefScan::LinkLabel(_, end_ix) => {
1447                                    // Toggle reference viability of the last closing bracket,
1448                                    // so that we can skip it on future iterations in case
1449                                    // it fails in this one. In particular, we won't call
1450                                    // the broken link callback twice on one reference.
1451                                    let reference_close_node = if let Some(node) =
1452                                        scan_nodes_to_ix(&self.tree, next, end_ix - 1)
1453                                    {
1454                                        node
1455                                    } else {
1456                                        continue;
1457                                    };
1458                                    self.tree[reference_close_node].item.body =
1459                                        ItemBody::MaybeLinkClose(false);
1460                                    // The label scan walks raw source, so it can
1461                                    // stop inside a wider item, e.g. an MDX
1462                                    // expression or directive holding a `]`. The
1463                                    // label owns up to `end_ix`; the rest is
1464                                    // literal text, and without this it would
1465                                    // belong to no node.
1466                                    let close_end = self.tree[reference_close_node].item.end;
1467                                    let next_node = if close_end > end_ix {
1468                                        self.tree[reference_close_node].item.end = end_ix;
1469                                        let tail = self.tree.create_node(Item {
1470                                            start: end_ix,
1471                                            end: close_end,
1472                                            body: ItemBody::Text {
1473                                                backslash_escaped: false,
1474                                            },
1475                                        });
1476                                        self.tree[tail].next = self.tree[reference_close_node].next;
1477                                        self.tree[reference_close_node].next = Some(tail);
1478                                        Some(tail)
1479                                    } else {
1480                                        self.tree[reference_close_node].next
1481                                    };
1482
1483                                    (next_node, LinkType::Reference)
1484                                }
1485                                // [reference][]
1486                                RefScan::Collapsed(next_node) => {
1487                                    // This reference has already been tried, and it's not
1488                                    // valid. Skip it.
1489                                    if !could_be_ref {
1490                                        continue;
1491                                    }
1492                                    (next_node, LinkType::Collapsed)
1493                                }
1494                                // [X][^Y] — full-reference form with a footnote-shaped
1495                                // second label. Per CommonMark the full-ref has to
1496                                // resolve to a link definition, which `^Y` never will;
1497                                // shortcut fallback is NOT tried. Leave both brackets
1498                                // literal and let `[^Y]` be parsed as a footnote on
1499                                // its own MaybeLinkClose iteration.
1500                                RefScan::UnexpectedFootnote => continue,
1501                                // `[text][invalid_label]` — the `[` after `[text]`
1502                                // started a label slot but it wasn't a valid label
1503                                // (e.g. unescaped `[` inside). Spec: a shortcut link
1504                                // can't be followed by `[`, so don't fall back to
1505                                // shortcut. Leave both brackets literal.
1506                                RefScan::FailedInvalidLabel => continue,
1507                                // [shortcut]
1508                                //
1509                                // [shortcut]: /blah
1510                                RefScan::Failed => {
1511                                    if !could_be_ref {
1512                                        continue;
1513                                    }
1514                                    (next, LinkType::Shortcut)
1515                                }
1516                            };
1517
1518                            // FIXME: references and labels are mixed in the naming of variables
1519                            // below. Disambiguate!
1520
1521                            // (label, source_ix end)
1522                            let label: Option<(ReferenceLabel<'input>, usize)> = match scan_result {
1523                                RefScan::LinkLabel(l, end_ix) => {
1524                                    Some((ReferenceLabel::Link(l), end_ix))
1525                                }
1526                                RefScan::Collapsed(..)
1527                                | RefScan::Failed
1528                                | RefScan::FailedInvalidLabel
1529                                | RefScan::UnexpectedFootnote => {
1530                                    // No label? maybe it is a shortcut reference
1531                                    let label_start = self.tree[tos.node].item.end - 1;
1532                                    let label_end = self.tree[cur_ix].item.end;
1533                                    scan_link_label(
1534                                        &self.tree,
1535                                        &self.text[label_start..label_end],
1536                                        self.options,
1537                                    )
1538                                    .map(|(ix, label)| (label, label_start + ix))
1539                                    .filter(|(_, end)| *end == label_end)
1540                                }
1541                            };
1542
1543                            let id = match &label {
1544                                Some(
1545                                    (ReferenceLabel::Link(l), _) | (ReferenceLabel::Footnote(l), _),
1546                                ) => l.clone(),
1547                                None => "".into(),
1548                            };
1549
1550                            // see if it's a footnote reference
1551                            if let Some((ReferenceLabel::Footnote(l), end)) = label {
1552                                let footref = self.allocs.allocate_cow(l);
1553                                if let Some(def) = self
1554                                    .allocs
1555                                    .footdefs
1556                                    .get_mut(self.allocs.cows[footref.0].to_owned())
1557                                {
1558                                    def.use_count += 1;
1559                                }
1560                                if self.allocs.footdefs.contains(&self.allocs.cows[footref.0]) {
1561                                    // If this came from a MaybeImage, then the `!` prefix
1562                                    // isn't part of the footnote reference.
1563                                    let footnote_ix = if tos.ty == LinkStackTy::Image {
1564                                        self.tree[tos.node].next = Some(cur_ix);
1565                                        self.tree[tos.node].child = None;
1566                                        self.tree[tos.node].item.body =
1567                                            ItemBody::SynthesizeChar('!');
1568                                        self.tree[cur_ix].item.start =
1569                                            self.tree[tos.node].item.start + 1;
1570                                        self.tree[tos.node].item.end =
1571                                            self.tree[tos.node].item.start + 1;
1572                                        cur_ix
1573                                    } else {
1574                                        tos.node
1575                                    };
1576                                    // use `next` instead of `node_after_link` because
1577                                    // node_after_link is calculated for a [collapsed][] link,
1578                                    // which footnotes don't support.
1579                                    self.tree[footnote_ix].next = next;
1580                                    self.tree[footnote_ix].child = None;
1581                                    self.tree[footnote_ix].item.body =
1582                                        ItemBody::FootnoteReference(footref);
1583                                    self.tree[footnote_ix].item.end = end;
1584                                    prev = Some(footnote_ix);
1585                                    cur = next;
1586                                    self.link_stack.clear();
1587                                    continue;
1588                                }
1589                            } else if let Some((ReferenceLabel::Link(link_label), end)) = label
1590                                && let Some((def_link_type, url, title)) = self
1591                                    .fetch_link_type_url_title(
1592                                        link_label,
1593                                        (self.tree[tos.node].item.start)..end,
1594                                        link_type,
1595                                        callbacks,
1596                                    )
1597                            {
1598                                let link_ix =
1599                                    self.allocs.allocate_link(def_link_type, url, title, id);
1600                                self.tree[tos.node].item.body = if tos.ty == LinkStackTy::Image {
1601                                    ItemBody::Image(link_ix)
1602                                } else {
1603                                    ItemBody::Link(link_ix)
1604                                };
1605                                let label_node = self.tree[tos.node].next;
1606
1607                                // lets do some tree surgery to add the link to the tree
1608                                // 1st: skip the label node and close node
1609                                self.tree[tos.node].next = node_after_link;
1610
1611                                // then, if it exists, add the label node as a child to the link node
1612                                if label_node != cur {
1613                                    self.tree[tos.node].child = label_node;
1614
1615                                    // finally: disconnect list of children
1616                                    if let Some(prev_ix) = prev {
1617                                        self.tree[prev_ix].next = None;
1618                                    }
1619                                }
1620
1621                                self.tree[tos.node].item.end = end;
1622                                // No `max(orig_start, end)` clamp here,
1623                                // unlike the inline-link splice: the item at
1624                                // `end - 1` either ends the label or was
1625                                // truncated to it, and the zero-width items
1626                                // that could sit at `end` all sit on a URL
1627                                // or email byte, never a `]`.
1628                                debug_assert!(
1629                                    node_after_link.is_none_or(|node_after_ix| {
1630                                        self.tree[node_after_ix].item.start >= end
1631                                    }),
1632                                    "reference splice must not overrun its successor",
1633                                );
1634
1635                                // set up cur so next node will be node_after_link
1636                                cur = Some(tos.node);
1637                                cur_ix = tos.node;
1638
1639                                if tos.ty == LinkStackTy::Link {
1640                                    self.disable_all_links();
1641                                }
1642                            }
1643                        }
1644                    }
1645                }
1646                _ => {}
1647            }
1648            prev = cur;
1649            cur = self.tree[cur_ix].next;
1650        }
1651        self.link_stack.clear();
1652        self.wikilink_stack.clear();
1653        self.code_delims.clear();
1654        self.math_delims.clear();
1655    }
1656
1657    /// The construct opening on the byte after a URL-ending `\`. The first
1658    /// pass's escape handler consumed that byte, so the construct either never
1659    /// got an item or got a blocked one; now that the link owns the `\`, give
1660    /// it back.
1661    fn repair_construct_after_url_end(&mut self, cand_end: usize, node_ix: TreeIndex) {
1662        let item = self.tree[node_ix].item;
1663        if item.start != cand_end {
1664            return;
1665        }
1666        if let ItemBody::MaybeEmphasisEscaped(count, can_open, can_close) = item.body {
1667            self.tree[node_ix].item.body = ItemBody::MaybeEmphasis(count, can_open, can_close);
1668            // The rest of the run was emitted for the shorter run the escape
1669            // would have left, so only its flanking needs restating.
1670            let mut scan = self.tree[node_ix].next;
1671            for _ in 1..count {
1672                let Some(next_ix) = scan else { break };
1673                if let ItemBody::MaybeEmphasis(_, open, close) = &mut self.tree[next_ix].item.body {
1674                    *open = can_open;
1675                    *close = can_close;
1676                }
1677                scan = self.tree[next_ix].next;
1678            }
1679            return;
1680        }
1681        if !matches!(item.body, ItemBody::Text { .. })
1682            || self.text.as_bytes().get(cand_end) != Some(&b'&')
1683        {
1684            return;
1685        }
1686        let (n, Some(value)) = scan_entity(&self.text.as_bytes()[cand_end..]) else {
1687            return;
1688        };
1689        if cand_end + n > item.end {
1690            return;
1691        }
1692        let cow_ix = self.allocs.allocate_cow(value);
1693        if cand_end + n < item.end {
1694            let tail = self.tree.create_node(Item {
1695                start: cand_end + n,
1696                end: item.end,
1697                body: ItemBody::Text {
1698                    backslash_escaped: false,
1699                },
1700            });
1701            self.tree[tail].next = self.tree[node_ix].next;
1702            self.tree[node_ix].next = Some(tail);
1703        }
1704        self.tree[node_ix].item.end = cand_end + n;
1705        self.tree[node_ix].item.body = ItemBody::SynthesizeText(cow_ix);
1706    }
1707
1708    /// Handles a wikilink.
1709    ///
1710    /// This function may bail early in case the link is malformed, so this
1711    /// acts as a control flow guard. Returns the link node if a wikilink was
1712    /// found and created.
1713    fn handle_wikilink(
1714        &mut self,
1715        block_text: &'input str,
1716        cur_ix: TreeIndex,
1717        prev: Option<TreeIndex>,
1718    ) -> Option<TreeIndex> {
1719        let next_ix = self.tree[cur_ix].next.unwrap();
1720        // this is a wikilink closing delim, try popping from
1721        // the wikilink stack
1722        if let Some(tos) = self.wikilink_stack.pop() {
1723            if tos.ty == LinkStackTy::Disabled {
1724                return None;
1725            }
1726            // fetches the beginning of the wikilink body
1727            let Some(body_node) = self.tree[tos.node].next.and_then(|ix| self.tree[ix].next) else {
1728                // skip if no next node exists, like at end of input
1729                return None;
1730            };
1731            let start_ix = self.tree[body_node].item.start;
1732            let end_ix = self.tree[cur_ix].item.start;
1733            let wikilink = match scan_wikilink_pipe(
1734                block_text,
1735                start_ix, // bounded by closing tag
1736                end_ix - start_ix,
1737            ) {
1738                Some((rest, wikitext)) => {
1739                    // bail early if the wikiname would be empty
1740                    if wikitext.is_empty() {
1741                        return None;
1742                    }
1743                    // [[WikiName|rest]]
1744                    let body_node = scan_nodes_to_ix(&self.tree, Some(body_node), rest);
1745                    if let Some(body_node) = body_node {
1746                        // break node so passes can actually format
1747                        // the display text
1748                        self.tree[body_node].item.start = rest;
1749                        Some((true, body_node, wikitext))
1750                    } else {
1751                        None
1752                    }
1753                }
1754                None => {
1755                    let wikitext = &block_text[start_ix..end_ix];
1756                    // bail early if the wikiname would be empty
1757                    if wikitext.is_empty() {
1758                        return None;
1759                    }
1760                    let body_node = self.tree.create_node(Item {
1761                        start: start_ix,
1762                        end: end_ix,
1763                        body: ItemBody::Text {
1764                            backslash_escaped: false,
1765                        },
1766                    });
1767                    Some((false, body_node, wikitext))
1768                }
1769            };
1770
1771            if let Some((has_pothole, body_node, wikiname)) = wikilink {
1772                let link_ix = self.allocs.allocate_link(
1773                    LinkType::WikiLink { has_pothole },
1774                    wikiname.into(),
1775                    "".into(),
1776                    "".into(),
1777                );
1778                if let Some(prev_ix) = prev {
1779                    self.tree[prev_ix].next = None;
1780                }
1781                if tos.ty == LinkStackTy::Image {
1782                    self.tree[tos.node].item.body = ItemBody::Image(link_ix);
1783                } else {
1784                    self.tree[tos.node].item.body = ItemBody::Link(link_ix);
1785                }
1786                self.tree[tos.node].child = Some(body_node);
1787                self.tree[tos.node].next = self.tree[next_ix].next;
1788                self.tree[tos.node].item.end = end_ix + 2;
1789                self.disable_all_links();
1790                return Some(tos.node);
1791            }
1792        }
1793
1794        None
1795    }
1796
1797    fn handle_emphasis_in_scope(&mut self, start: Option<TreeIndex>) {
1798        let mut prev = None;
1799        let mut prev_ix: TreeIndex;
1800        let mut cur = start;
1801
1802        let mut single_quote_open: Option<TreeIndex> = None;
1803        let mut double_quote_open: bool = false;
1804
1805        while let Some(mut cur_ix) = cur {
1806            match self.tree[cur_ix].item.body {
1807                ItemBody::MaybeEmphasis(mut count, can_open, can_close) => {
1808                    let run_length = count;
1809                    let c = self.text.as_bytes()[self.tree[cur_ix].item.start];
1810                    let both = can_open && can_close;
1811                    // Defer `~`/`^` resolution to the post-pass.
1812                    // Without lookahead, the single-pass can't tell whether an
1813                    // earlier `*`/`_` opener will pair (in which case the
1814                    // `~`/`^` should match inside the future emphasis) or
1815                    // remain unmatched (in which case `~`/`^` would cross the
1816                    // boundary). micromark handles this with a separate
1817                    // strikethrough resolve phase that runs after emphasis.
1818                    if c == b'~' || c == b'^' {
1819                        prev_ix = cur_ix + count - 1;
1820                        prev = Some(prev_ix);
1821                        cur = self.tree[prev_ix].next;
1822                        continue;
1823                    }
1824                    if can_close {
1825                        while let Some(el) =
1826                            self.inline_stack
1827                                .find_match(&mut self.tree, c, run_length, count, both)
1828                        {
1829                            // have a match!
1830                            if let Some(prev_ix) = prev {
1831                                self.tree[prev_ix].next = None;
1832                            }
1833                            // Consume at most two markers per inner-loop pass
1834                            // (one `<strong>`/`<em>` per match), matching
1835                            // micromark's `use = open>1 && close>1 ? 2 : 1`.
1836                            // The outer `while let` then drives nesting by
1837                            // re-running `find_match` with the leftover
1838                            // counts, which is how `***foo***` becomes
1839                            // `<em><strong>foo</strong></em>` instead of one
1840                            // flat match.
1841                            let match_count = min(2, min(count, el.count));
1842                            // start, end are tree node indices
1843                            let mut end = cur_ix - 1;
1844                            let mut start = el.start + el.count;
1845
1846                            // work from the inside out
1847                            while start > el.start + el.count - match_count {
1848                                let inc = if start > el.start + el.count - match_count + 1 {
1849                                    2
1850                                } else {
1851                                    1
1852                                };
1853                                let ty = if c == b'~' {
1854                                    if inc == 2 {
1855                                        if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
1856                                            ItemBody::Strikethrough
1857                                        } else {
1858                                            ItemBody::Text {
1859                                                backslash_escaped: false,
1860                                            }
1861                                        }
1862                                    } else if self.options.contains(Options::ENABLE_SUBSCRIPT) {
1863                                        ItemBody::Subscript
1864                                    } else if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
1865                                        ItemBody::Strikethrough
1866                                    } else {
1867                                        ItemBody::Text {
1868                                            backslash_escaped: false,
1869                                        }
1870                                    }
1871                                } else if c == b'^' {
1872                                    if self.options.contains(Options::ENABLE_SUPERSCRIPT) {
1873                                        ItemBody::Superscript
1874                                    } else {
1875                                        ItemBody::Text {
1876                                            backslash_escaped: false,
1877                                        }
1878                                    }
1879                                } else if inc == 2 {
1880                                    ItemBody::Strong
1881                                } else {
1882                                    ItemBody::Emphasis
1883                                };
1884
1885                                let root = start - inc;
1886                                end = end + inc;
1887                                self.tree[root].item.body = ty;
1888                                self.tree[root].item.end = self.tree[end].item.end;
1889                                self.tree[root].child = Some(start);
1890                                self.tree[root].next = None;
1891                                start = root;
1892                            }
1893
1894                            // set next for top most emph level
1895                            prev_ix = el.start + el.count - match_count;
1896                            prev = Some(prev_ix);
1897                            cur = self.tree[cur_ix + match_count - 1].next;
1898                            self.tree[prev_ix].next = cur;
1899
1900                            if el.count > match_count {
1901                                self.inline_stack.push(InlineEl {
1902                                    start: el.start,
1903                                    count: el.count - match_count,
1904                                    run_length: el.run_length,
1905                                    c: el.c,
1906                                    both: el.both,
1907                                })
1908                            }
1909                            count -= match_count;
1910                            if count > 0 {
1911                                cur_ix = cur.unwrap();
1912                            } else {
1913                                break;
1914                            }
1915                        }
1916                    }
1917                    if count > 0 {
1918                        if can_open {
1919                            self.inline_stack.push(InlineEl {
1920                                start: cur_ix,
1921                                run_length,
1922                                count,
1923                                c,
1924                                both,
1925                            });
1926                        } else {
1927                            for i in 0..count {
1928                                self.tree[cur_ix + i].item.body = ItemBody::Text {
1929                                    backslash_escaped: false,
1930                                };
1931                            }
1932                        }
1933                        prev_ix = cur_ix + count - 1;
1934                        prev = Some(prev_ix);
1935                        cur = self.tree[prev_ix].next;
1936                    }
1937                }
1938                ItemBody::MaybeSmartQuote(c, can_open, can_close) => {
1939                    self.tree[cur_ix].item.body = match c {
1940                        b'\'' => {
1941                            if let (Some(open_ix), true) = (single_quote_open, can_close) {
1942                                self.tree[open_ix].item.body = ItemBody::SynthesizeChar('‘');
1943                                single_quote_open = None;
1944                            } else if can_open {
1945                                single_quote_open = Some(cur_ix);
1946                            }
1947                            ItemBody::SynthesizeChar('’')
1948                        }
1949                        _ /* double quote */ => {
1950                            if can_close && double_quote_open {
1951                                double_quote_open = false;
1952                                ItemBody::SynthesizeChar('”')
1953                            } else if can_open {
1954                                double_quote_open = true;
1955                                ItemBody::SynthesizeChar('“')
1956                            } else if can_close {
1957                                // Unmatched close-flanking quote (e.g. the inch
1958                                // mark in `24"`): render a closing curly quote.
1959                                ItemBody::SynthesizeChar('”')
1960                            } else {
1961                                // Non-flanking quote: opening curly quote.
1962                                ItemBody::SynthesizeChar('“')
1963                            }
1964                        }
1965                    };
1966                    prev = cur;
1967                    cur = self.tree[cur_ix].next;
1968                }
1969                ItemBody::HardBreak(true) => {
1970                    if self.tree[cur_ix].next.is_none() {
1971                        self.tree[cur_ix].item.body = ItemBody::SynthesizeChar('\\');
1972                    }
1973                    prev = cur;
1974                    cur = self.tree[cur_ix].next;
1975                }
1976                _ => {
1977                    prev = cur;
1978                    cur = self.tree[cur_ix].next;
1979                }
1980            }
1981        }
1982        self.inline_stack.pop_all(&mut self.tree);
1983    }
1984
1985    /// Second-pass strikethrough/sub/sup resolution. Walks the tree
1986    /// hierarchically and resolves `~`/`^` MaybeEmphasis tokens within
1987    /// each inline scope independently. This matches micromark's
1988    /// post-emphasis resolve phase: a `~..~` pair only forms when both
1989    /// ends lie within the same enclosing scope (root, emphasis, link,
1990    /// etc.). Multi-char `~~` strikethrough was already resolved in
1991    /// the main pass.
1992    /// Resolve `~`/`^` runs at this scope. With `descend`, also recurse into
1993    /// already-formed spans; the per-scope driver passes `false` and handles
1994    /// descent itself so it can flip the resolve order for nested content.
1995    fn resolve_tildes_carets_in_scope(&mut self, start: Option<TreeIndex>, descend: bool) {
1996        let mut stack: Vec<InlineEl> = Vec::new();
1997        let mut cur = start;
1998        let mut prev: Option<TreeIndex> = None;
1999        while let Some(mut cur_ix) = cur {
2000            match self.tree[cur_ix].item.body {
2001                ItemBody::MaybeEmphasis(count, can_open, can_close) => {
2002                    let c = self.text.as_bytes()[self.tree[cur_ix].item.start];
2003                    if c != b'~' && c != b'^' {
2004                        prev = Some(cur_ix);
2005                        cur = self.tree[cur_ix].next;
2006                        continue;
2007                    }
2008                    let run_length = count;
2009                    let mut remaining = count;
2010                    if can_close {
2011                        while remaining > 0 {
2012                            let res = stack
2013                                .iter()
2014                                .enumerate()
2015                                .rfind(|(_, el)| el.c == c && el.run_length == run_length);
2016                            let Some((matching_ix, matching_el)) = res else {
2017                                break;
2018                            };
2019                            let matching_el = *matching_el;
2020                            if let Some(prev_ix) = prev {
2021                                self.tree[prev_ix].next = None;
2022                            }
2023                            // Convert intermediate `~`/`^` openers above the
2024                            // match to text — they failed to find a pair.
2025                            for el in &stack[(matching_ix + 1)..] {
2026                                for i in 0..el.count {
2027                                    self.tree[el.start + i].item.body = ItemBody::Text {
2028                                        backslash_escaped: false,
2029                                    };
2030                                }
2031                            }
2032                            stack.truncate(matching_ix);
2033                            let match_count =
2034                                core::cmp::min(2, core::cmp::min(remaining, matching_el.count));
2035                            let mut end = cur_ix - 1;
2036                            let mut sub_start = matching_el.start + matching_el.count;
2037                            while sub_start > matching_el.start + matching_el.count - match_count {
2038                                let inc = if sub_start
2039                                    > matching_el.start + matching_el.count - match_count + 1
2040                                {
2041                                    2
2042                                } else {
2043                                    1
2044                                };
2045                                let ty = if c == b'~' {
2046                                    if inc == 2 {
2047                                        if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
2048                                            ItemBody::Strikethrough
2049                                        } else {
2050                                            ItemBody::Text {
2051                                                backslash_escaped: false,
2052                                            }
2053                                        }
2054                                    } else if self.options.contains(Options::ENABLE_SUBSCRIPT) {
2055                                        ItemBody::Subscript
2056                                    } else if self.options.contains(Options::ENABLE_STRIKETHROUGH) {
2057                                        ItemBody::Strikethrough
2058                                    } else {
2059                                        ItemBody::Text {
2060                                            backslash_escaped: false,
2061                                        }
2062                                    }
2063                                } else if self.options.contains(Options::ENABLE_SUPERSCRIPT) {
2064                                    ItemBody::Superscript
2065                                } else {
2066                                    ItemBody::Text {
2067                                        backslash_escaped: false,
2068                                    }
2069                                };
2070                                let root = sub_start - inc;
2071                                end = end + inc;
2072                                self.tree[root].item.body = ty;
2073                                self.tree[root].item.end = self.tree[end].item.end;
2074                                self.tree[root].child = Some(sub_start);
2075                                self.tree[root].next = None;
2076                                sub_start = root;
2077                            }
2078                            let new_prev_ix = matching_el.start + matching_el.count - match_count;
2079                            let new_cur = self.tree[cur_ix + match_count - 1].next;
2080                            self.tree[new_prev_ix].next = new_cur;
2081                            prev = Some(new_prev_ix);
2082                            if matching_el.count > match_count {
2083                                stack.push(InlineEl {
2084                                    start: matching_el.start,
2085                                    count: matching_el.count - match_count,
2086                                    run_length: matching_el.run_length,
2087                                    c: matching_el.c,
2088                                    both: matching_el.both,
2089                                });
2090                            }
2091                            remaining -= match_count;
2092                            if remaining > 0 {
2093                                let Some(next_cur) = new_cur else { break };
2094                                cur_ix = next_cur;
2095                            } else {
2096                                break;
2097                            }
2098                        }
2099                    }
2100                    if remaining > 0 {
2101                        if can_open {
2102                            stack.push(InlineEl {
2103                                start: cur_ix,
2104                                count: remaining,
2105                                run_length,
2106                                c,
2107                                both: can_open && can_close,
2108                            });
2109                        } else {
2110                            for i in 0..remaining {
2111                                self.tree[cur_ix + i].item.body = ItemBody::Text {
2112                                    backslash_escaped: false,
2113                                };
2114                            }
2115                        }
2116                        let prev_ix = cur_ix + remaining - 1;
2117                        prev = Some(prev_ix);
2118                        cur = self.tree[prev_ix].next;
2119                    } else {
2120                        cur = self.tree[prev.unwrap()].next;
2121                    }
2122                    continue;
2123                }
2124                ItemBody::Emphasis
2125                | ItemBody::Strong
2126                | ItemBody::Strikethrough
2127                | ItemBody::Subscript
2128                | ItemBody::Superscript
2129                | ItemBody::Link(_)
2130                | ItemBody::Image(_)
2131                    if descend =>
2132                {
2133                    let child = self.tree[cur_ix].child;
2134                    self.resolve_tildes_carets_in_scope(child, true);
2135                }
2136                _ => {}
2137            }
2138            prev = Some(cur_ix);
2139            cur = self.tree[cur_ix].next;
2140        }
2141        // End of scope: any remaining openers couldn't find a closer.
2142        for el in stack {
2143            for i in 0..el.count {
2144                self.tree[el.start + i].item.body = ItemBody::Text {
2145                    backslash_escaped: false,
2146                };
2147            }
2148        }
2149    }
2150
2151    fn disable_all_links(&mut self) {
2152        self.link_stack.disable_all_links();
2153        self.wikilink_stack.disable_all_links();
2154    }
2155
2156    fn defined_footnote_label(&self, open_ix: TreeIndex, close_ix: TreeIndex) -> bool {
2157        let start = self.tree[open_ix].item.start;
2158        if !self.options.contains(Options::ENABLE_FOOTNOTES)
2159            || self.text.as_bytes().get(start + 1) != Some(&b'^')
2160        {
2161            return false;
2162        }
2163        let label_text = &self.text[start..self.tree[close_ix].item.end];
2164        let Some((len, ReferenceLabel::Footnote(label))) =
2165            scan_link_label(&self.tree, label_text, self.options)
2166        else {
2167            return false;
2168        };
2169        // micromark ends a footnote call at whitespace, where the shared label scan collapses it.
2170        !label_text.as_bytes()[..len]
2171            .iter()
2172            .any(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
2173            && self.allocs.footdefs.contains(&label)
2174    }
2175
2176    /// Returns next byte index, url and title.
2177    fn scan_inline_link(
2178        &self,
2179        underlying: &'input str,
2180        mut ix: usize,
2181        node: Option<TreeIndex>,
2182    ) -> Option<(usize, CowStr<'input>, CowStr<'input>)> {
2183        if underlying.as_bytes().get(ix) != Some(&b'(') {
2184            return None;
2185        }
2186        ix += 1;
2187
2188        let scan_separator = |ix: &mut usize| {
2189            *ix += scan_while(&underlying.as_bytes()[*ix..], is_space_or_tab);
2190            if let Some(bl) = scan_eol(&underlying.as_bytes()[*ix..]) {
2191                *ix += bl;
2192                *ix += skip_container_prefixes(
2193                    &self.tree,
2194                    &underlying.as_bytes()[*ix..],
2195                    self.options,
2196                );
2197            }
2198            *ix += scan_while(&underlying.as_bytes()[*ix..], is_space_or_tab);
2199        };
2200
2201        scan_separator(&mut ix);
2202
2203        let (dest_length, dest) = scan_link_dest(underlying, ix, LINK_MAX_NESTED_PARENS)?;
2204        let dest = unescape(dest, self.tree.is_in_table());
2205        ix += dest_length;
2206
2207        let dest_end = ix;
2208        scan_separator(&mut ix);
2209
2210        // A title is only reachable through whitespace after the destination.
2211        let title = if ix > dest_end
2212            && let Some((bytes_scanned, t)) = self.scan_link_title(underlying, ix, node)
2213        {
2214            ix += bytes_scanned;
2215            scan_separator(&mut ix);
2216            t
2217        } else {
2218            "".into()
2219        };
2220        if underlying.as_bytes().get(ix) != Some(&b')') {
2221            return None;
2222        }
2223        ix += 1;
2224
2225        Some((ix, dest, title))
2226    }
2227
2228    // returns (bytes scanned, title cow)
2229    fn scan_link_title(
2230        &self,
2231        text: &'input str,
2232        start_ix: usize,
2233        node: Option<TreeIndex>,
2234    ) -> Option<(usize, CowStr<'input>)> {
2235        let bytes = text.as_bytes();
2236        let open = match bytes.get(start_ix) {
2237            Some(b @ b'\'') | Some(b @ b'\"') | Some(b @ b'(') => *b,
2238            _ => return None,
2239        };
2240        if open == b'(' && start_ix >= self.unclosed_paren_title_floor.get() {
2241            return None;
2242        }
2243        // Unlike CommonMark, remark keeps an unescaped `(` inside a paren title as content.
2244        let close = if open == b'(' { b')' } else { open };
2245
2246        let mut title = String::new();
2247        let mut mark = start_ix + 1;
2248        let mut i = start_ix + 1;
2249
2250        while i < bytes.len() {
2251            let c = bytes[i];
2252
2253            if c == close {
2254                let cow = if title.is_empty() {
2255                    (i - start_ix + 1, text[mark..i].into())
2256                } else {
2257                    title.push_str(&text[mark..i]);
2258                    (i - start_ix + 1, title.into())
2259                };
2260
2261                return Some(cow);
2262            }
2263
2264            if (c == b'\n' || c == b'\r')
2265                && let Some(node_ix) = scan_nodes_to_ix(&self.tree, node, i + 1)
2266                && self.tree[node_ix].item.start > i
2267            {
2268                title.push_str(&text[mark..i]);
2269                // The title's line endings are content, kept byte for byte.
2270                title.push(c as char);
2271                if c == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
2272                    title.push('\n');
2273                }
2274                i = self.tree[node_ix].item.start;
2275                mark = i;
2276                continue;
2277            }
2278            if c == b'&'
2279                && let (n, Some(value)) = scan_entity(&bytes[i..])
2280            {
2281                title.push_str(&text[mark..i]);
2282                title.push_str(&value);
2283                i += n;
2284                mark = i;
2285                continue;
2286            }
2287            if self.tree.is_in_table()
2288                && c == b'\\'
2289                && i + 2 < bytes.len()
2290                && bytes[i + 1] == b'\\'
2291                && bytes[i + 2] == b'|'
2292            {
2293                // this runs if there are an even number of pipes in a table
2294                // if it's odd, then it gets parsed as normal
2295                title.push_str(&text[mark..i]);
2296                i += 2;
2297                mark = i;
2298            }
2299            if c == b'\\' && i + 1 < bytes.len() && is_ascii_punctuation(bytes[i + 1]) {
2300                title.push_str(&text[mark..i]);
2301                i += 1;
2302                mark = i;
2303            }
2304
2305            i += 1;
2306        }
2307
2308        if open == b'(' {
2309            let floor = self.unclosed_paren_title_floor.get();
2310            self.unclosed_paren_title_floor.set(floor.min(start_ix));
2311        }
2312        None
2313    }
2314
2315    fn make_math_span(&mut self, open: TreeIndex, close: TreeIndex) {
2316        // Find the end of the opening run of consecutive $ tokens
2317        let mut open_end = open;
2318        {
2319            let mut peek = self.tree[open].next;
2320            while let Some(peek_ix) = peek {
2321                if matches!(self.tree[peek_ix].item.body, ItemBody::MaybeMath(..))
2322                    && self.tree[peek_ix].item.start == self.tree[open_end].item.end
2323                    && peek_ix != close
2324                {
2325                    open_end = peek_ix;
2326                    peek = self.tree[peek_ix].next;
2327                } else {
2328                    break;
2329                }
2330            }
2331        }
2332        // Find the end of the closing run
2333        let mut close_end = close;
2334        {
2335            let mut peek = self.tree[close].next;
2336            while let Some(peek_ix) = peek {
2337                if matches!(self.tree[peek_ix].item.body, ItemBody::MaybeMath(..))
2338                    && self.tree[peek_ix].item.start == self.tree[close_end].item.end
2339                {
2340                    close_end = peek_ix;
2341                    peek = self.tree[peek_ix].next;
2342                } else {
2343                    break;
2344                }
2345            }
2346        }
2347
2348        let span_start = self.tree[open_end].item.end;
2349        let span_end = self.tree[close].item.start;
2350
2351        if span_start > span_end {
2352            self.tree[open].item.body = ItemBody::Text {
2353                backslash_escaped: false,
2354            };
2355            return;
2356        }
2357
2358        let spanned_text = &self.text[span_start..span_end];
2359        let spanned_bytes = spanned_text.as_bytes();
2360        let mut buf: Option<String> = None;
2361
2362        let mut start_ix = 0;
2363        let mut ix = 0;
2364        while ix < spanned_bytes.len() {
2365            let c = spanned_bytes[ix];
2366            if c == b'\r' || c == b'\n' {
2367                ix += 1;
2368                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2369                buf.push_str(&spanned_text[start_ix..ix]);
2370                // Use the full source bytes from this position (not just
2371                // the span slice) so scan_containers can see the real
2372                // line content past the closing backtick. With only the
2373                // span slice, a partial-indent line followed by buffer
2374                // end (e.g. `    ` + closing) was misread as EOL by
2375                // is_at_eol — letting the ListItem container "match" the
2376                // 4 spaces of a 5-indent item and over-strip the code
2377                // span's trailing whitespace.
2378                let from = span_start + ix;
2379                let (scanned, leftover) = skip_container_prefixes_with_remaining(
2380                    &self.tree,
2381                    &self.text.as_bytes()[from..],
2382                    self.options,
2383                );
2384                let scanned = scanned.min(spanned_bytes.len() - ix);
2385                ix += scanned;
2386                start_ix = ix;
2387                // Preserve leftover virtual columns from a tab the
2388                // container only partially consumed (e.g. `\t` in a 2-col
2389                // listitem leaves 2 spaces of content).
2390                for _ in 0..leftover {
2391                    buf.push(' ');
2392                }
2393            } else if c == b'\\'
2394                && spanned_bytes.get(ix + 1) == Some(&b'|')
2395                && self.tree.is_in_table()
2396            {
2397                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2398                buf.push_str(&spanned_text[start_ix..ix]);
2399                buf.push('|');
2400                ix += 2;
2401                start_ix = ix;
2402            } else {
2403                ix += 1;
2404            }
2405        }
2406
2407        if let Some(buf) = &mut buf {
2408            buf.push_str(&spanned_text[start_ix..]);
2409        }
2410        let cow: CowStr<'input> = strip_span_padding(buf, spanned_text);
2411
2412        self.tree[open].item.body = ItemBody::Math(self.allocs.allocate_cow(cow), false);
2413        self.tree[open].item.end = self.tree[close_end].item.end;
2414        self.tree[open].next = self.tree[close_end].next;
2415    }
2416
2417    /// Make a code span.
2418    ///
2419    /// Both `open` and `close` are matching MaybeCode items.
2420    fn make_code_span(&mut self, open: TreeIndex, close: TreeIndex, preceding_backslash: bool) {
2421        let span_start = self.tree[open].item.end;
2422        let span_end = self.tree[close].item.start;
2423        let mut buf: Option<String> = None;
2424
2425        let spanned_text = &self.text[span_start..span_end];
2426        let spanned_bytes = spanned_text.as_bytes();
2427        let mut start_ix = 0;
2428        let mut ix = 0;
2429        while ix < spanned_bytes.len() {
2430            let c = spanned_bytes[ix];
2431            if c == b'\r' || c == b'\n' {
2432                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2433                // The span's own line endings are content and are kept byte for
2434                // byte; only the container prefix below is stripped.
2435                buf.push_str(&spanned_text[start_ix..ix]);
2436                buf.push(c as char);
2437                ix += 1;
2438                if c == b'\r' && spanned_bytes.get(ix) == Some(&b'\n') {
2439                    buf.push('\n');
2440                    ix += 1;
2441                }
2442                // Use the full source bytes from this position (not just
2443                // the span slice) so scan_containers can see the real
2444                // line content past the closing backtick. With only the
2445                // span slice, a partial-indent line followed by buffer
2446                // end (e.g. `    ` + closing) was misread as EOL by
2447                // is_at_eol — letting the ListItem container "match" the
2448                // 4 spaces of a 5-indent item and over-strip the code
2449                // span's trailing whitespace.
2450                let from = span_start + ix;
2451                let (scanned, leftover) = skip_container_prefixes_with_remaining(
2452                    &self.tree,
2453                    &self.text.as_bytes()[from..],
2454                    self.options,
2455                );
2456                let scanned = scanned.min(spanned_bytes.len() - ix);
2457                ix += scanned;
2458                start_ix = ix;
2459                // Preserve leftover virtual columns from a tab the
2460                // container only partially consumed (e.g. `\t` in a 2-col
2461                // listitem leaves 2 spaces of content).
2462                for _ in 0..leftover {
2463                    buf.push(' ');
2464                }
2465            } else if c == b'\\'
2466                && spanned_bytes.get(ix + 1) == Some(&b'|')
2467                && self.tree.is_in_table()
2468            {
2469                let buf = buf.get_or_insert_with(|| String::with_capacity(spanned_bytes.len()));
2470                buf.push_str(&spanned_text[start_ix..ix]);
2471                buf.push('|');
2472                ix += 2;
2473                start_ix = ix;
2474            } else {
2475                ix += 1;
2476            }
2477        }
2478
2479        if let Some(buf) = &mut buf {
2480            buf.push_str(&spanned_text[start_ix..]);
2481        }
2482        let cow: CowStr<'input> = strip_span_padding(buf, spanned_text);
2483
2484        if preceding_backslash {
2485            self.tree[open].item.body = ItemBody::Text {
2486                backslash_escaped: true,
2487            };
2488            self.tree[open].item.end = self.tree[open].item.start + 1;
2489            self.tree[open].next = Some(close);
2490            self.tree[close].item.body = ItemBody::Code(self.allocs.allocate_cow(cow));
2491            self.tree[close].item.start = self.tree[open].item.start + 1;
2492        } else {
2493            self.tree[open].item.body = ItemBody::Code(self.allocs.allocate_cow(cow));
2494            self.tree[open].item.end = self.tree[close].item.end;
2495            self.tree[open].next = self.tree[close].next;
2496        }
2497
2498        // MDX: errors recorded in pass 1 for `{` inside what turned out to be a
2499        // code span are false positives — the `{` is literal text.
2500        if !self.mdx_errors.is_empty() {
2501            self.mdx_errors
2502                .retain(|(offset, _)| *offset < span_start || *offset >= span_end);
2503        }
2504    }
2505
2506    /// On success, returns a buffer containing the inline html and byte offset.
2507    /// When no bytes were skipped, the buffer will be empty and the html can be
2508    /// represented as a subslice of the input string.
2509    fn scan_inline_html(&mut self, bytes: &[u8], ix: usize) -> Option<(Vec<u8>, usize)> {
2510        let c = *bytes.get(ix)?;
2511        if c == b'!' {
2512            Some((
2513                vec![],
2514                scan_inline_html_comment(bytes, ix + 1, &mut self.html_scan_guard)?,
2515            ))
2516        } else if c == b'?' {
2517            Some((
2518                vec![],
2519                scan_inline_html_processing(bytes, ix + 1, &mut self.html_scan_guard)?,
2520            ))
2521        } else {
2522            let (span, i) = scan_html_block_inner(
2523                // Subtract 1 to include the < character
2524                &bytes[(ix - 1)..],
2525                Some(&|bytes| skip_container_prefixes(&self.tree, bytes, self.options)),
2526            )?;
2527            Some((span, i + ix - 1))
2528        }
2529    }
2530}
2531
2532/// Returns number of containers scanned.
2533pub(crate) fn scan_containers(
2534    tree: &Tree<Item>,
2535    line_start: &mut LineStart<'_>,
2536    options: Options,
2537) -> usize {
2538    let mut i = 0;
2539    for &node_ix in tree.walk_spine() {
2540        match tree[node_ix].item.body {
2541            ItemBody::BlockQuote(..) => {
2542                let save = line_start.clone();
2543                // In MDX mode indented code blocks are disabled, so the
2544                // ≤3-space cap on blockquote prefix indent doesn't apply —
2545                // tab- or 4+-space-indented `>` should still continue the
2546                // blockquote (matches micromark + remark-mdx).
2547                if options.contains(Options::ENABLE_MDX) {
2548                    line_start.scan_all_space();
2549                } else {
2550                    let _ = line_start.scan_space(3);
2551                }
2552                if !line_start.scan_blockquote_marker() {
2553                    *line_start = save;
2554                    break;
2555                }
2556            }
2557            ItemBody::ListItem(indent, _) => {
2558                let save = line_start.clone();
2559                if !line_start.scan_space(indent) && !line_start.is_at_eol() {
2560                    *line_start = save;
2561                    break;
2562                }
2563            }
2564            ItemBody::DefinitionListDefinition(indent, _) => {
2565                let save = line_start.clone();
2566                if !line_start.scan_space(indent) && !line_start.is_at_eol() {
2567                    *line_start = save;
2568                    break;
2569                }
2570            }
2571            ItemBody::FootnoteDefinition(..) if options.contains(Options::ENABLE_FOOTNOTES) => {
2572                let save = line_start.clone();
2573                if !line_start.scan_space(4) && !line_start.is_at_eol() {
2574                    *line_start = save;
2575                    break;
2576                }
2577            }
2578            _ => (),
2579        }
2580        i += 1;
2581    }
2582    i
2583}
2584
2585/// Strip one leading and one trailing space or line ending from a code or math
2586/// span, but only when both are present and the span isn't all whitespace. A
2587/// CRLF is a single line ending, so it goes as a pair.
2588///
2589/// `buf` is the span's rewritten content when a container prefix or `\|` escape
2590/// forced an allocation; otherwise the span is `spanned_text` verbatim.
2591fn strip_span_padding<'input>(buf: Option<String>, spanned_text: &'input str) -> CowStr<'input> {
2592    let s = buf.as_deref().unwrap_or(spanned_text);
2593    let lead = if s.starts_with("\r\n") {
2594        2
2595    } else {
2596        usize::from(matches!(s.as_bytes().first(), Some(b' ' | b'\n' | b'\r')))
2597    };
2598    let trail = if s.ends_with("\r\n") {
2599        2
2600    } else {
2601        usize::from(matches!(s.as_bytes().last(), Some(b' ' | b'\n' | b'\r')))
2602    };
2603    let all_spaces = s.bytes().all(|b| matches!(b, b' ' | b'\n' | b'\r'));
2604
2605    if !all_spaces && lead > 0 && trail > 0 {
2606        if let Some(mut buf) = buf {
2607            if !buf.is_empty() {
2608                buf.truncate(buf.len() - trail);
2609                buf.replace_range(..lead, "");
2610            }
2611            buf.into()
2612        } else {
2613            spanned_text[lead..(spanned_text.len() - trail).max(lead)].into()
2614        }
2615    } else if let Some(buf) = buf {
2616        buf.into()
2617    } else {
2618        spanned_text.into()
2619    }
2620}
2621
2622pub(crate) fn skip_container_prefixes(tree: &Tree<Item>, bytes: &[u8], options: Options) -> usize {
2623    let mut line_start = LineStart::new(bytes);
2624    let _ = scan_containers(tree, &mut line_start, options);
2625    line_start.bytes_scanned()
2626}
2627
2628/// Like `skip_container_prefixes`, but also returns the leftover virtual
2629/// space columns from tab-stop expansion past the last consumed container
2630/// prefix. Used by math-span content extraction to faithfully reproduce
2631/// indentation that the container "ate" only partially — e.g. a single
2632/// `\t` (4 cols) in a list item with 2-col content indent leaves 2
2633/// trailing spaces of content.
2634fn skip_container_prefixes_with_remaining(
2635    tree: &Tree<Item>,
2636    bytes: &[u8],
2637    options: Options,
2638) -> (usize, usize) {
2639    let mut line_start = LineStart::new(bytes);
2640    let _ = scan_containers(tree, &mut line_start, options);
2641    (line_start.bytes_scanned(), line_start.remaining_space())
2642}
2643
2644impl Tree<Item> {
2645    pub(crate) fn append_text(&mut self, start: usize, end: usize, backslash_escaped: bool) {
2646        if end > start {
2647            if let Some(ix) = self.cur()
2648                && matches!(self[ix].item.body, ItemBody::Text { .. })
2649                && self[ix].item.end == start
2650            {
2651                self[ix].item.end = end;
2652                return;
2653            }
2654            self.append(Item {
2655                start,
2656                end,
2657                body: ItemBody::Text { backslash_escaped },
2658            });
2659        }
2660    }
2661    /// Returns true if the current node is inside a table.
2662    ///
2663    /// If `cur` is an ItemBody::Table, it would return false,
2664    /// but since the `TableRow` and `TableHead` and `TableCell`
2665    /// are children of the table, anything doing inline parsing
2666    /// doesn't need to care about that.
2667    pub(crate) fn is_in_table(&self) -> bool {
2668        fn might_be_in_table(item: &Item) -> bool {
2669            item.body.is_inline()
2670                || matches!(item.body, |ItemBody::TableHead| ItemBody::TableRow
2671                    | ItemBody::TableCell)
2672        }
2673        for &ix in self.walk_spine().rev() {
2674            if matches!(self[ix].item.body, ItemBody::Table(_)) {
2675                return true;
2676            }
2677            if !might_be_in_table(&self[ix].item) {
2678                return false;
2679            }
2680        }
2681        false
2682    }
2683}
2684
2685#[derive(Copy, Clone, Debug)]
2686struct InlineEl {
2687    /// offset of tree node
2688    start: TreeIndex,
2689    /// number of delimiters available for matching
2690    count: usize,
2691    /// length of the run that these delimiters came from
2692    run_length: usize,
2693    /// b'*', b'_', or b'~'
2694    c: u8,
2695    /// can both open and close
2696    both: bool,
2697}
2698
2699#[derive(Debug, Clone, Default)]
2700struct InlineStack {
2701    stack: Vec<InlineEl>,
2702    // Lower bounds for matching indices in the stack. For example
2703    // a strikethrough delimiter will never match with any element
2704    // in the stack with index smaller than
2705    // `lower_bounds[InlineStack::TILDES]`.
2706    lower_bounds: [usize; 10],
2707}
2708
2709impl InlineStack {
2710    /// These are indices into the lower bounds array.
2711    /// Not both refers to the property that the delimiter can not both
2712    /// be opener as a closer.
2713    const UNDERSCORE_NOT_BOTH: usize = 0;
2714    const ASTERISK_NOT_BOTH: usize = 1;
2715    const ASTERISK_BASE: usize = 2;
2716    const TILDES: usize = 5;
2717    const UNDERSCORE_BASE: usize = 6;
2718    const CIRCUMFLEXES: usize = 9;
2719
2720    fn pop_all(&mut self, tree: &mut Tree<Item>) {
2721        for el in self.stack.drain(..) {
2722            for i in 0..el.count {
2723                tree[el.start + i].item.body = ItemBody::Text {
2724                    backslash_escaped: false,
2725                };
2726            }
2727        }
2728        self.lower_bounds = [0; 10];
2729    }
2730
2731    fn get_lowerbound(&self, c: u8, count: usize, both: bool) -> usize {
2732        if c == b'_' {
2733            let mod3_lower = self.lower_bounds[InlineStack::UNDERSCORE_BASE + count % 3];
2734            if both {
2735                mod3_lower
2736            } else {
2737                min(
2738                    mod3_lower,
2739                    self.lower_bounds[InlineStack::UNDERSCORE_NOT_BOTH],
2740                )
2741            }
2742        } else if c == b'*' {
2743            let mod3_lower = self.lower_bounds[InlineStack::ASTERISK_BASE + count % 3];
2744            if both {
2745                mod3_lower
2746            } else {
2747                min(
2748                    mod3_lower,
2749                    self.lower_bounds[InlineStack::ASTERISK_NOT_BOTH],
2750                )
2751            }
2752        } else if c == b'^' {
2753            self.lower_bounds[InlineStack::CIRCUMFLEXES]
2754        } else {
2755            self.lower_bounds[InlineStack::TILDES]
2756        }
2757    }
2758
2759    fn set_lowerbound(&mut self, c: u8, count: usize, both: bool, new_bound: usize) {
2760        if c == b'_' {
2761            if both {
2762                self.lower_bounds[InlineStack::UNDERSCORE_BASE + count % 3] = new_bound;
2763            } else {
2764                self.lower_bounds[InlineStack::UNDERSCORE_NOT_BOTH] = new_bound;
2765            }
2766        } else if c == b'*' {
2767            self.lower_bounds[InlineStack::ASTERISK_BASE + count % 3] = new_bound;
2768            if !both {
2769                self.lower_bounds[InlineStack::ASTERISK_NOT_BOTH] = new_bound;
2770            }
2771        } else if c == b'^' {
2772            self.lower_bounds[InlineStack::CIRCUMFLEXES] = new_bound;
2773        } else {
2774            self.lower_bounds[InlineStack::TILDES] = new_bound;
2775        }
2776    }
2777
2778    fn truncate(&mut self, new_bound: usize) {
2779        self.stack.truncate(new_bound);
2780        for lower_bound in &mut self.lower_bounds {
2781            if *lower_bound > new_bound {
2782                *lower_bound = new_bound;
2783            }
2784        }
2785    }
2786
2787    /// Find an opener that can match `c` of original `run_length`.
2788    ///
2789    /// `current_count` is the **remaining** length of the closer being
2790    /// processed (chars not yet consumed by earlier inner-loop matches).
2791    /// We use it for CommonMark rule 9 (the "mod 3" both-side rule) so
2792    /// that after a partial consumption like `3*foo *bar**` the outer `*`
2793    /// can pair with what's left of the `**` — micromark re-evaluates the
2794    /// rule using only the *current* run lengths on each side.
2795    ///
2796    /// `run_length` is the original closer length; it stays stable across
2797    /// inner-loop iterations and is what the lower-bounds optimisation and
2798    /// the strict tilde/caret length check key off.
2799    fn find_match(
2800        &mut self,
2801        tree: &mut Tree<Item>,
2802        c: u8,
2803        run_length: usize,
2804        current_count: usize,
2805        both: bool,
2806    ) -> Option<InlineEl> {
2807        // Use current_count (the post-partial-consumption remaining length)
2808        // for the rule-9 mod-3 lowerbound key, not run_length. After an
2809        // inner-loop pass consumes part of the closer, the remaining
2810        // length sits in a different mod-3 bucket and may now satisfy
2811        // rule 9 with openers the earlier (longer) attempt failed
2812        // against. Keying on run_length would carry over the earlier
2813        // failure into the new bucket and block valid matches like the
2814        // outer `*` in `cz*x` `*foo***bar***baz` (closer `***` partial
2815        // remainder 1 should still reach the opener at offset 2).
2816        let lowerbound = min(
2817            self.stack.len(),
2818            self.get_lowerbound(c, current_count, both),
2819        );
2820        let res = self.stack[lowerbound..]
2821            .iter()
2822            .cloned()
2823            .enumerate()
2824            .rfind(|(_, el)| {
2825                if (c == b'~' || c == b'^') && run_length != el.run_length {
2826                    return false;
2827                }
2828                // Rule 9 (mod-3): for `*`/`_`, the openers on the stack are
2829                // checked against the *current* lengths — `el.count` reflects
2830                // remaining-after-partial-consumption when an opener has been
2831                // re-pushed, and `current_count` is the remaining closer.
2832                el.c == c
2833                    && (!both && !el.both
2834                        || !(current_count + el.count).is_multiple_of(3)
2835                        || current_count.is_multiple_of(3))
2836            });
2837
2838        if let Some((matching_ix, matching_el)) = res {
2839            let matching_ix = matching_ix + lowerbound;
2840            for el in &self.stack[(matching_ix + 1)..] {
2841                for i in 0..el.count {
2842                    tree[el.start + i].item.body = ItemBody::Text {
2843                        backslash_escaped: false,
2844                    };
2845                }
2846            }
2847            self.truncate(matching_ix);
2848            Some(matching_el)
2849        } else {
2850            // For `*`/`_`, the lower-bound optimisation is safe because their
2851            // matching rule (CM "rule of three") is monotonic across future
2852            // closers with the same count. Tildes/carets match strictly by
2853            // equal run-length, so a failure at run-length 2 must not close
2854            // the door on a later run-length 1 closer matching an earlier
2855            // run-length 1 opener still on the stack. Key the bound by
2856            // `current_count` (the post-partial-consumption length) so it
2857            // applies only to closers whose remaining bucket actually
2858            // shares this failure mode.
2859            if c != b'~' && c != b'^' {
2860                self.set_lowerbound(c, current_count, both, self.stack.len());
2861            }
2862            None
2863        }
2864    }
2865
2866    fn trim_lower_bound(&mut self, ix: usize) {
2867        self.lower_bounds[ix] = self.lower_bounds[ix].min(self.stack.len());
2868    }
2869
2870    fn push(&mut self, el: InlineEl) {
2871        if el.c == b'~' {
2872            self.trim_lower_bound(InlineStack::TILDES);
2873        } else if el.c == b'^' {
2874            self.trim_lower_bound(InlineStack::CIRCUMFLEXES);
2875        }
2876        self.stack.push(el)
2877    }
2878}
2879
2880#[derive(Debug, Clone)]
2881enum RefScan<'a> {
2882    // label, source ix of label end
2883    LinkLabel(CowStr<'a>, usize),
2884    // contains next node index
2885    Collapsed(Option<TreeIndex>),
2886    UnexpectedFootnote,
2887    Failed,
2888    // `[text][...]` where `[...]` started but is an invalid label
2889    // (e.g. contains unescaped `[`). The shortcut form for `[text]` is
2890    // suppressed because the spec says a shortcut link must NOT be
2891    // followed by `[` — even if that `[` doesn't form a valid label.
2892    FailedInvalidLabel,
2893}
2894
2895/// Skips forward within a block to a node which spans (ends inclusive) the given
2896/// index into the source.
2897fn scan_nodes_to_ix(
2898    tree: &Tree<Item>,
2899    mut node: Option<TreeIndex>,
2900    ix: usize,
2901) -> Option<TreeIndex> {
2902    while let Some(node_ix) = node {
2903        let item = tree[node_ix].item;
2904        // A zero-width node *at* `ix` must not be skipped: callers splice the
2905        // result back in as the tail, so skipping drops it from the tree.
2906        if item.end <= ix && item.start < ix {
2907            node = tree[node_ix].next;
2908        } else {
2909            break;
2910        }
2911    }
2912    node
2913}
2914
2915/// Scans an inline link label, which cannot be interrupted.
2916/// Returns number of bytes (including brackets) and label on success.
2917fn scan_link_label<'text>(
2918    tree: &Tree<Item>,
2919    text: &'text str,
2920    options: Options,
2921) -> Option<(usize, ReferenceLabel<'text>)> {
2922    let bytes = text.as_bytes();
2923    if bytes.len() < 2 || bytes[0] != b'[' {
2924        return None;
2925    }
2926    let linebreak_handler = |bytes: &[u8]| Some(skip_container_prefixes(tree, bytes, options));
2927    if options.contains(Options::ENABLE_FOOTNOTES)
2928        && b'^' == bytes[1]
2929        && bytes.get(2) != Some(&b']')
2930    {
2931        // GFM footnote labels don't wrap across line breaks.
2932        let linebreak_handler: &dyn Fn(&[u8]) -> Option<usize> = &|_| None;
2933        if let Some((byte_index, cow)) =
2934            scan_link_label_rest(&text[2..], linebreak_handler, tree.is_in_table())
2935        {
2936            return Some((byte_index + 2, ReferenceLabel::Footnote(cow)));
2937        }
2938    }
2939    let (byte_index, cow) =
2940        scan_link_label_rest(&text[1..], &linebreak_handler, tree.is_in_table())?;
2941    Some((byte_index + 1, ReferenceLabel::Link(cow)))
2942}
2943
2944fn scan_reference<'b>(
2945    tree: &Tree<Item>,
2946    text: &'b str,
2947    cur: Option<TreeIndex>,
2948    options: Options,
2949) -> RefScan<'b> {
2950    let cur_ix = match cur {
2951        None => return RefScan::Failed,
2952        Some(cur_ix) => cur_ix,
2953    };
2954    let start = tree[cur_ix].item.start;
2955    let tail = &text.as_bytes()[start..];
2956
2957    // If the `[` opening the candidate label was escaped in source
2958    // (preceded by an odd run of backslashes), it's a literal `[` and
2959    // can't start a reference label. Without this check the label
2960    // scanner walks raw source, which doesn't know that pulldown-cmark
2961    // already absorbed the `\` into a backslash-escape token, and it
2962    // would falsely consume `\[foo]` as `[foo]`.
2963    if tail.first() == Some(&b'[') && start > 0 {
2964        let src = text.as_bytes();
2965        let mut backslashes = 0usize;
2966        let mut j = start;
2967        while j > 0 && src[j - 1] == b'\\' {
2968            backslashes += 1;
2969            j -= 1;
2970        }
2971        if backslashes % 2 == 1 {
2972            return RefScan::Failed;
2973        }
2974    }
2975
2976    if tail.starts_with(b"[]") {
2977        // The trailing `]` of the collapsed reference must already exist as a
2978        // tree node — pulldown-cmark emits each bracket as its own item, and
2979        // we only reach here when `tail` already contains `]`. Defensive
2980        // fallback to `Failed` if that invariant is somehow broken.
2981        let Some(closing_node) = tree[cur_ix].next else {
2982            return RefScan::Failed;
2983        };
2984        RefScan::Collapsed(tree[closing_node].next)
2985    } else {
2986        let label = scan_link_label(tree, &text[start..], options);
2987        match label {
2988            Some((ix, ReferenceLabel::Link(label))) => RefScan::LinkLabel(label, start + ix),
2989            Some((_ix, ReferenceLabel::Footnote(_label))) => RefScan::UnexpectedFootnote,
2990            None => {
2991                // If `[text]` is followed by `[` that looked like a label
2992                // opener, the shortcut form is suppressed even though the
2993                // label parse failed (CommonMark requires shortcut links
2994                // not be followed by `[`).
2995                if tail.starts_with(b"[") {
2996                    RefScan::FailedInvalidLabel
2997                } else {
2998                    RefScan::Failed
2999                }
3000            }
3001        }
3002    }
3003}
3004
3005#[derive(Clone, Default)]
3006struct LinkStack {
3007    inner: Vec<LinkStackEl>,
3008    disabled_ix: usize,
3009}
3010
3011impl LinkStack {
3012    fn is_empty(&self) -> bool {
3013        self.inner.is_empty()
3014    }
3015
3016    fn push(&mut self, el: LinkStackEl) {
3017        self.inner.push(el);
3018    }
3019
3020    fn pop(&mut self) -> Option<LinkStackEl> {
3021        let el = self.inner.pop();
3022        self.disabled_ix = core::cmp::min(self.disabled_ix, self.inner.len());
3023        el
3024    }
3025
3026    fn clear(&mut self) {
3027        self.inner.clear();
3028        self.disabled_ix = 0;
3029    }
3030
3031    fn disable_all_links(&mut self) {
3032        for el in &mut self.inner[self.disabled_ix..] {
3033            if el.ty == LinkStackTy::Link {
3034                el.ty = LinkStackTy::Disabled;
3035            }
3036        }
3037        self.disabled_ix = self.inner.len();
3038    }
3039}
3040
3041#[derive(Clone, Debug)]
3042struct LinkStackEl {
3043    node: TreeIndex,
3044    ty: LinkStackTy,
3045}
3046
3047#[derive(PartialEq, Clone, Debug)]
3048enum LinkStackTy {
3049    Link,
3050    Image,
3051    Disabled,
3052}
3053
3054/// Contains the destination URL, title and source span of a reference definition.
3055#[derive(Clone, Debug)]
3056pub struct LinkDef<'a> {
3057    pub dest: CowStr<'a>,
3058    pub title: Option<CowStr<'a>>,
3059    pub span: Range<usize>,
3060}
3061
3062impl<'a> LinkDef<'a> {
3063    pub fn into_static(self) -> LinkDef<'static> {
3064        LinkDef {
3065            dest: self.dest.into_static(),
3066            title: self.title.map(|s| s.into_static()),
3067            span: self.span,
3068        }
3069    }
3070}
3071
3072/// Contains the destination URL, title and source span of a reference definition.
3073#[derive(Clone, Debug)]
3074pub struct FootnoteDef {
3075    pub use_count: usize,
3076}
3077
3078/// Tracks tree indices of code span delimiters of each length. It should prevent
3079/// quadratic scanning behaviours by providing (amortized) constant time lookups.
3080struct CodeDelims {
3081    inner: FxHashMap<usize, VecDeque<TreeIndex>>,
3082    seen_first: bool,
3083}
3084
3085impl CodeDelims {
3086    fn new() -> Self {
3087        Self {
3088            inner: Default::default(),
3089            seen_first: false,
3090        }
3091    }
3092
3093    fn insert(&mut self, count: usize, ix: TreeIndex) {
3094        if self.seen_first {
3095            self.inner.entry(count).or_default().push_back(ix);
3096        } else {
3097            // Skip the first insert, since that delimiter will always
3098            // be an opener and not a closer.
3099            self.seen_first = true;
3100        }
3101    }
3102
3103    fn is_populated(&self) -> bool {
3104        !self.inner.is_empty()
3105    }
3106
3107    fn find(&mut self, open_ix: TreeIndex, count: usize) -> Option<TreeIndex> {
3108        while let Some(ix) = self.inner.get_mut(&count)?.pop_front() {
3109            if ix > open_ix {
3110                return Some(ix);
3111            }
3112        }
3113        None
3114    }
3115
3116    fn clear(&mut self) {
3117        self.inner.clear();
3118        self.seen_first = false;
3119    }
3120}
3121
3122/// Tracks brace contexts and delimiter length for math delimiters.
3123/// Provides amortized constant-time lookups.
3124struct MathDelims {
3125    inner: FxHashMap<u8, VecDeque<(TreeIndex, bool, bool)>>,
3126}
3127
3128impl MathDelims {
3129    fn new() -> Self {
3130        Self {
3131            inner: Default::default(),
3132        }
3133    }
3134
3135    fn clear(&mut self) {
3136        self.inner.clear();
3137    }
3138}
3139
3140#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3141pub(crate) struct LinkIndex(usize);
3142
3143#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3144pub(crate) struct CowIndex(usize);
3145
3146#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3147pub(crate) struct AlignmentIndex(usize);
3148
3149#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3150pub(crate) struct HeadingIndex(NonZeroUsize);
3151
3152#[cfg(feature = "mdx")]
3153#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3154pub(crate) struct JsxElementIndex(usize);
3155
3156#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3157pub(crate) struct DirectiveIndex(usize);
3158
3159#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3160pub(crate) struct AutolinkCandidateIndex(usize);
3161
3162/// A GFM autolink literal the first pass found but did not commit to. The
3163/// `Link` is allocated up front so firing is only a body swap.
3164#[derive(Copy, Clone, Debug)]
3165pub(crate) struct AutolinkCandidate {
3166    /// Precedes the trigger byte when the email scan walks back over the local
3167    /// part.
3168    pub start: usize,
3169    pub end: usize,
3170    pub link: LinkIndex,
3171}
3172
3173/// A parsed JSX attribute.
3174#[cfg(feature = "mdx")]
3175#[derive(Debug, Clone)]
3176pub(crate) enum JsxAttr<'a> {
3177    Boolean(CowStr<'a>),
3178    Literal(CowStr<'a>, CowStr<'a>),
3179    /// `name={value}`. The two `usize`s are the byte range of `value` within
3180    /// the opening tag, so a parse error can be validated against the verbatim
3181    /// source slice and resolved to an exact source position.
3182    Expression(CowStr<'a>, CowStr<'a>, usize, usize),
3183    /// `{...value}`. The two `usize`s are the byte range of `value` (including
3184    /// the leading `...`) within the opening tag.
3185    Spread(CowStr<'a>, usize, usize),
3186}
3187
3188#[cfg(feature = "mdx")]
3189impl<'a> JsxAttr<'a> {
3190    pub fn into_static(self) -> JsxAttr<'static> {
3191        match self {
3192            JsxAttr::Boolean(n) => JsxAttr::Boolean(n.into_static()),
3193            JsxAttr::Literal(n, v) => JsxAttr::Literal(n.into_static(), v.into_static()),
3194            JsxAttr::Expression(n, v, start, end) => {
3195                JsxAttr::Expression(n.into_static(), v.into_static(), start, end)
3196            }
3197            JsxAttr::Spread(v, start, end) => JsxAttr::Spread(v.into_static(), start, end),
3198        }
3199    }
3200}
3201
3202/// Pre-parsed JSX element data (name + attributes + tag classification).
3203#[cfg(feature = "mdx")]
3204#[derive(Debug, Clone)]
3205pub(crate) struct JsxElementData<'a> {
3206    pub name: CowStr<'a>,
3207    pub attrs: Vec<JsxAttr<'a>>,
3208    pub raw: CowStr<'a>,
3209    pub is_closing: bool,
3210    pub is_self_closing: bool,
3211}
3212
3213#[cfg(feature = "mdx")]
3214impl<'a> JsxElementData<'a> {
3215    pub fn into_static(self) -> JsxElementData<'static> {
3216        JsxElementData {
3217            name: self.name.into_static(),
3218            attrs: self.attrs.into_iter().map(|a| a.into_static()).collect(),
3219            raw: self.raw.into_static(),
3220            is_closing: self.is_closing,
3221            is_self_closing: self.is_self_closing,
3222        }
3223    }
3224}
3225
3226#[derive(Debug, Clone)]
3227pub(crate) struct DirectiveAttrData<'a> {
3228    pub name: CowStr<'a>,
3229    pub attributes: Vec<(CowStr<'a>, CowStr<'a>)>,
3230    pub label_start: usize,
3231    pub label_end: usize,
3232    /// Cols of leading whitespace before `:::` on the opening line, after
3233    /// outer-container prefix stripping. Mirrors micromark-extension-directive's
3234    /// `initialSize`, which controls how much the directive body's per-line
3235    /// linePrefix is stripped (up to `initialSize + 1` cols). Only meaningful
3236    /// for container directives — leaf/text directives leave this 0.
3237    pub initial_size: u8,
3238}
3239
3240#[derive(Clone)]
3241pub(crate) struct Allocations<'a> {
3242    pub refdefs: RefDefs<'a>,
3243    /// Every refdef occurrence in source order, including duplicates that
3244    /// `refdefs` drops (it's a map and only keeps the first per label, since
3245    /// resolution picks the first match per CommonMark). Used to emit every
3246    /// definition as its own mdast `definition` node.
3247    pub refdefs_all: Vec<(LinkLabel<'a>, LinkDef<'a>)>,
3248    pub footdefs: FootnoteDefs<'a>,
3249    links: Vec<(LinkType, CowStr<'a>, CowStr<'a>, CowStr<'a>)>,
3250    cows: Vec<CowStr<'a>>,
3251    alignments: Vec<Vec<Alignment>>,
3252    headings: Vec<HeadingAttributes<'a>>,
3253    #[cfg(feature = "mdx")]
3254    jsx_elements: Vec<JsxElementData<'a>>,
3255    directives: Vec<DirectiveAttrData<'a>>,
3256    autolink_candidates: Vec<AutolinkCandidate>,
3257}
3258
3259/// Used by the heading attributes extension.
3260#[derive(Clone)]
3261pub(crate) struct HeadingAttributes<'a> {
3262    pub id: Option<CowStr<'a>>,
3263    pub classes: Vec<CowStr<'a>>,
3264    pub attrs: Vec<(CowStr<'a>, Option<CowStr<'a>>)>,
3265}
3266
3267/// Keeps track of the reference definitions defined in the document.
3268#[derive(Clone, Default, Debug)]
3269pub struct RefDefs<'input>(pub(crate) FxHashMap<LinkLabel<'input>, LinkDef<'input>>);
3270
3271/// Keeps track of the footnote definitions defined in the document.
3272#[derive(Clone, Default, Debug)]
3273pub struct FootnoteDefs<'input>(pub(crate) FxHashMap<FootnoteLabel<'input>, FootnoteDef>);
3274
3275impl<'input, 'b, 's> RefDefs<'input>
3276where
3277    's: 'b,
3278{
3279    /// Performs a lookup on reference label using unicode case folding.
3280    pub fn get(&'s self, key: &'b str) -> Option<&'b LinkDef<'input>> {
3281        self.0.get(&UniCase::new(key.into()))
3282    }
3283
3284    /// Provides an iterator over all the document's reference definitions.
3285    pub fn iter(
3286        &'s self,
3287    ) -> impl Iterator<Item = (&'s str, &'s LinkDef<'input>)> + use<'s, 'input> {
3288        self.0.iter().map(|(k, v)| (k.as_ref(), v))
3289    }
3290}
3291
3292impl<'input, 'b, 's> FootnoteDefs<'input>
3293where
3294    's: 'b,
3295{
3296    /// Performs a lookup on reference label using unicode case folding.
3297    pub fn contains(&'s self, key: &'b str) -> bool {
3298        self.0.contains_key(&UniCase::new(key.into()))
3299    }
3300    /// Performs a lookup on reference label using unicode case folding.
3301    pub fn get_mut(&'s mut self, key: CowStr<'input>) -> Option<&'s mut FootnoteDef> {
3302        self.0.get_mut(&UniCase::new(key))
3303    }
3304}
3305
3306impl<'a> Allocations<'a> {
3307    pub fn new() -> Self {
3308        Self {
3309            refdefs: RefDefs::default(),
3310            refdefs_all: Vec::new(),
3311            footdefs: FootnoteDefs::default(),
3312            links: Vec::with_capacity(128),
3313            cows: Vec::new(),
3314            alignments: Vec::new(),
3315            headings: Vec::new(),
3316            #[cfg(feature = "mdx")]
3317            jsx_elements: Vec::new(),
3318            directives: Vec::new(),
3319            autolink_candidates: Vec::new(),
3320        }
3321    }
3322
3323    pub fn allocate_autolink_candidate(
3324        &mut self,
3325        candidate: AutolinkCandidate,
3326    ) -> AutolinkCandidateIndex {
3327        let ix = self.autolink_candidates.len();
3328        self.autolink_candidates.push(candidate);
3329        AutolinkCandidateIndex(ix)
3330    }
3331
3332    pub fn allocate_cow(&mut self, cow: CowStr<'a>) -> CowIndex {
3333        let ix = self.cows.len();
3334        self.cows.push(cow);
3335        CowIndex(ix)
3336    }
3337
3338    pub fn allocate_link(
3339        &mut self,
3340        ty: LinkType,
3341        url: CowStr<'a>,
3342        title: CowStr<'a>,
3343        id: CowStr<'a>,
3344    ) -> LinkIndex {
3345        let ix = self.links.len();
3346        self.links.push((ty, url, title, id));
3347        LinkIndex(ix)
3348    }
3349
3350    pub fn allocate_alignment(&mut self, alignment: Vec<Alignment>) -> AlignmentIndex {
3351        let ix = self.alignments.len();
3352        self.alignments.push(alignment);
3353        AlignmentIndex(ix)
3354    }
3355
3356    pub fn allocate_heading(&mut self, attrs: HeadingAttributes<'a>) -> HeadingIndex {
3357        let ix = self.headings.len();
3358        self.headings.push(attrs);
3359        // This won't panic. `self.headings.len()` can't be `usize::MAX` since
3360        // such a long Vec cannot fit in memory.
3361        let ix_nonzero = NonZeroUsize::new(ix.wrapping_add(1)).expect("too many headings");
3362        HeadingIndex(ix_nonzero)
3363    }
3364
3365    pub fn take_cow(&mut self, ix: CowIndex) -> CowStr<'a> {
3366        core::mem::replace(&mut self.cows[ix.0], "".into())
3367    }
3368
3369    pub fn take_link(&mut self, ix: LinkIndex) -> (LinkType, CowStr<'a>, CowStr<'a>, CowStr<'a>) {
3370        let default_link = (LinkType::ShortcutUnknown, "".into(), "".into(), "".into());
3371        core::mem::replace(&mut self.links[ix.0], default_link)
3372    }
3373
3374    pub fn take_alignment(&mut self, ix: AlignmentIndex) -> Vec<Alignment> {
3375        core::mem::take(&mut self.alignments[ix.0])
3376    }
3377
3378    #[cfg(feature = "mdx")]
3379    pub fn allocate_jsx_element(&mut self, data: JsxElementData<'a>) -> JsxElementIndex {
3380        let ix = self.jsx_elements.len();
3381        self.jsx_elements.push(data);
3382        JsxElementIndex(ix)
3383    }
3384
3385    pub fn allocate_directive(&mut self, data: DirectiveAttrData<'a>) -> DirectiveIndex {
3386        let ix = self.directives.len();
3387        self.directives.push(data);
3388        DirectiveIndex(ix)
3389    }
3390
3391    pub fn take_directive(&mut self, ix: DirectiveIndex) -> DirectiveAttrData<'a> {
3392        core::mem::replace(
3393            &mut self.directives[ix.0],
3394            DirectiveAttrData {
3395                name: "".into(),
3396                attributes: Vec::new(),
3397                label_start: 0,
3398                label_end: 0,
3399                initial_size: 0,
3400            },
3401        )
3402    }
3403
3404    pub fn directive_ref(&self, ix: DirectiveIndex) -> &DirectiveAttrData<'a> {
3405        &self.directives[ix.0]
3406    }
3407
3408    #[cfg(feature = "mdx")]
3409    pub fn take_jsx_element(&mut self, ix: JsxElementIndex) -> JsxElementData<'a> {
3410        core::mem::replace(
3411            &mut self.jsx_elements[ix.0],
3412            JsxElementData {
3413                name: "".into(),
3414                attrs: Vec::new(),
3415                raw: "".into(),
3416                is_closing: false,
3417                is_self_closing: false,
3418            },
3419        )
3420    }
3421}
3422
3423impl<'a> Index<CowIndex> for Allocations<'a> {
3424    type Output = CowStr<'a>;
3425
3426    fn index(&self, ix: CowIndex) -> &Self::Output {
3427        self.cows.index(ix.0)
3428    }
3429}
3430
3431impl<'a> Index<LinkIndex> for Allocations<'a> {
3432    type Output = (LinkType, CowStr<'a>, CowStr<'a>, CowStr<'a>);
3433
3434    fn index(&self, ix: LinkIndex) -> &Self::Output {
3435        self.links.index(ix.0)
3436    }
3437}
3438
3439impl<'a> Index<AutolinkCandidateIndex> for Allocations<'a> {
3440    type Output = AutolinkCandidate;
3441
3442    fn index(&self, ix: AutolinkCandidateIndex) -> &Self::Output {
3443        self.autolink_candidates.index(ix.0)
3444    }
3445}
3446
3447impl<'a> Index<AlignmentIndex> for Allocations<'a> {
3448    type Output = Vec<Alignment>;
3449
3450    fn index(&self, ix: AlignmentIndex) -> &Self::Output {
3451        self.alignments.index(ix.0)
3452    }
3453}
3454
3455impl<'a> Index<HeadingIndex> for Allocations<'a> {
3456    type Output = HeadingAttributes<'a>;
3457
3458    fn index(&self, ix: HeadingIndex) -> &Self::Output {
3459        self.headings.index(ix.0.get() - 1)
3460    }
3461}
3462
3463/// A struct containing information on the reachability of certain inline HTML
3464/// elements. In particular, for cdata elements (`<![CDATA[`), processing
3465/// elements (`<?`) and declarations (`<!DECLARATION`). The respectives usizes
3466/// represent the indices before which a scan will always fail and can hence
3467/// be skipped.
3468#[derive(Clone, Default)]
3469pub(crate) struct HtmlScanGuard {
3470    pub cdata: usize,
3471    pub processing: usize,
3472    pub declaration: usize,
3473    pub comment: usize,
3474}
3475
3476/// Trait to customize [`Parser`] behavior with callbacks. See [`Parser::new_with_callbacks`].
3477///
3478/// All methods have a default implementation, so you can choose which ones to override.
3479pub trait ParserCallbacks<'input> {
3480    /// Potentially provide a custom definition for a broken link.
3481    ///
3482    /// In case the parser encounters any potential links that have a broken
3483    /// reference (e.g `[foo]` when there is no `[foo]: ` entry at the bottom)
3484    /// this callback will be called with information about the reference,
3485    /// and the returned pair will be used as the link URL and title if it is not
3486    /// `None`.
3487    fn handle_broken_link(
3488        &mut self,
3489        #[allow(unused_variables)] link: BrokenLink<'input>,
3490    ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3491        None
3492    }
3493}
3494
3495/// Wrapper to implement [`ParserCallbacks::handle_broken_link`] with a closure.
3496///
3497/// Used internally by [`Parser::new_with_broken_link_callback`].
3498#[allow(missing_debug_implementations)]
3499pub struct BrokenLinkCallback<F>(Option<F>);
3500
3501impl<'input, F> ParserCallbacks<'input> for BrokenLinkCallback<F>
3502where
3503    F: FnMut(BrokenLink<'input>) -> Option<(CowStr<'input>, CowStr<'input>)>,
3504{
3505    fn handle_broken_link(
3506        &mut self,
3507        link: BrokenLink<'input>,
3508    ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3509        self.0.as_mut().and_then(|cb| cb(link))
3510    }
3511}
3512
3513impl<'input> ParserCallbacks<'input> for Box<dyn ParserCallbacks<'input>> {
3514    fn handle_broken_link(
3515        &mut self,
3516        link: BrokenLink<'input>,
3517    ) -> Option<(CowStr<'input>, CowStr<'input>)> {
3518        (**self).handle_broken_link(link)
3519    }
3520}
3521
3522/// [Parser] callbacks that do nothing.
3523///
3524/// Used when no custom callbacks are provided.
3525#[allow(missing_debug_implementations)]
3526pub struct DefaultParserCallbacks;
3527
3528impl<'input> ParserCallbacks<'input> for DefaultParserCallbacks {}
3529
3530/// Markdown event and source range iterator.
3531///
3532/// Generates tuples where the first element is the markdown event and the second
3533/// is a the corresponding range in the source string.
3534///
3535/// Constructed from a `Parser` using its
3536/// [`into_offset_iter`](struct.Parser.html#method.into_offset_iter) method.
3537#[derive(Debug)]
3538pub struct OffsetIter<'a, CB> {
3539    parser: Parser<'a, CB>,
3540}
3541
3542impl<'a, CB: ParserCallbacks<'a>> OffsetIter<'a, CB> {
3543    /// Returns a reference to the internal reference definition tracker.
3544    pub fn reference_definitions(&self) -> &RefDefs<'_> {
3545        self.parser.reference_definitions()
3546    }
3547
3548    /// Returns MDX validation errors collected during parsing.
3549    pub fn mdx_errors(&self) -> &[(usize, String)] {
3550        self.parser.mdx_errors()
3551    }
3552}
3553
3554impl<'a, CB: ParserCallbacks<'a>> Iterator for OffsetIter<'a, CB> {
3555    type Item = (Event<'a>, Range<usize>);
3556
3557    fn next(&mut self) -> Option<Self::Item> {
3558        self.parser
3559            .inner
3560            .next_event_range(&mut self.parser.callbacks)
3561    }
3562}
3563
3564impl<'a, CB: ParserCallbacks<'a>> Iterator for Parser<'a, CB> {
3565    type Item = Event<'a>;
3566
3567    fn next(&mut self) -> Option<Event<'a>> {
3568        self.inner
3569            .next_event_range(&mut self.callbacks)
3570            .map(|(event, _range)| event)
3571    }
3572}
3573
3574impl<'a, CB: ParserCallbacks<'a>> FusedIterator for Parser<'a, CB> {}
3575
3576impl<'input> ParserInner<'input> {
3577    fn next_event_range(
3578        &mut self,
3579        callbacks: &mut dyn ParserCallbacks<'input>,
3580    ) -> Option<(Event<'input>, Range<usize>)> {
3581        match self.tree.cur() {
3582            None => {
3583                let ix = self.tree.pop()?;
3584                let ix = if matches!(self.tree[ix].item.body, ItemBody::TightParagraph) {
3585                    // tight paragraphs emit nothing
3586                    self.tree.next_sibling(ix);
3587                    return self.next_event_range(callbacks);
3588                } else {
3589                    ix
3590                };
3591                let tag_end = body_to_tag_end(&self.tree[ix].item.body);
3592                self.tree.next_sibling(ix);
3593                let span = self.tree[ix].item.start..self.tree[ix].item.end;
3594                debug_assert!(span.start <= span.end);
3595                Some((Event::End(tag_end), span))
3596            }
3597            Some(cur_ix) => {
3598                let cur_ix = if matches!(self.tree[cur_ix].item.body, ItemBody::TightParagraph) {
3599                    // tight paragraphs emit nothing
3600                    self.tree.push();
3601                    self.tree.cur().unwrap()
3602                } else {
3603                    cur_ix
3604                };
3605                if self.tree[cur_ix].item.body.is_maybe_inline() {
3606                    self.handle_inline(callbacks);
3607                }
3608
3609                let node = self.tree[cur_ix];
3610                let item = node.item;
3611                let event = item_to_event(item, self.text, &mut self.allocs);
3612                if let Event::Start(..) = event {
3613                    self.tree.push();
3614                } else {
3615                    self.tree.next_sibling(cur_ix);
3616                }
3617                debug_assert!(item.start <= item.end);
3618                Some((event, item.start..item.end))
3619            }
3620        }
3621    }
3622}
3623
3624fn body_to_tag_end(body: &ItemBody) -> TagEnd {
3625    match *body {
3626        ItemBody::Paragraph => TagEnd::Paragraph,
3627        ItemBody::Emphasis => TagEnd::Emphasis,
3628        ItemBody::Superscript => TagEnd::Superscript,
3629        ItemBody::Subscript => TagEnd::Subscript,
3630        ItemBody::Strong => TagEnd::Strong,
3631        ItemBody::Strikethrough => TagEnd::Strikethrough,
3632        ItemBody::Link(..) => TagEnd::Link,
3633        ItemBody::Image(..) => TagEnd::Image,
3634        ItemBody::Heading(level, _) => TagEnd::Heading(level),
3635        ItemBody::IndentCodeBlock(..) | ItemBody::FencedCodeBlock(..) | ItemBody::MathBlock(..) => {
3636            TagEnd::CodeBlock
3637        }
3638        ItemBody::ContainerDirective(..) => TagEnd::Directive(DirectiveKind::Container),
3639        ItemBody::LeafDirective(..) => TagEnd::Directive(DirectiveKind::Leaf),
3640        ItemBody::TextDirective(..) => TagEnd::Directive(DirectiveKind::Text),
3641        ItemBody::BlockQuote(kind) => TagEnd::BlockQuote(kind),
3642        ItemBody::HtmlBlock(_) => TagEnd::HtmlBlock,
3643        ItemBody::List(_, c, _) => {
3644            let is_ordered = c == b'.' || c == b')';
3645            TagEnd::List(is_ordered)
3646        }
3647        ItemBody::ListItem(_, _) => TagEnd::Item,
3648        ItemBody::TableHead => TagEnd::TableHead,
3649        ItemBody::TableCell => TagEnd::TableCell,
3650        ItemBody::TableRow => TagEnd::TableRow,
3651        ItemBody::Table(..) => TagEnd::Table,
3652        ItemBody::FootnoteDefinition(..) => TagEnd::FootnoteDefinition,
3653        ItemBody::MetadataBlock(kind) => TagEnd::MetadataBlock(kind),
3654        ItemBody::DefinitionList(_) => TagEnd::DefinitionList,
3655        ItemBody::DefinitionListTitle => TagEnd::DefinitionListTitle,
3656        ItemBody::DefinitionListDefinition(..) => TagEnd::DefinitionListDefinition,
3657        #[cfg(feature = "mdx")]
3658        ItemBody::MdxJsxFlowElement(..) => TagEnd::MdxJsxFlowElement,
3659        #[cfg(feature = "mdx")]
3660        ItemBody::MdxJsxTextElement(..) => TagEnd::MdxJsxTextElement,
3661        _ => panic!("unexpected item body {:?}", body),
3662    }
3663}
3664
3665fn item_to_event<'a>(item: Item, text: &'a str, allocs: &mut Allocations<'a>) -> Event<'a> {
3666    let tag = match item.body {
3667        ItemBody::Text { .. } => return Event::Text(text[item.start..item.end].into()),
3668        ItemBody::Code(cow_ix) => return Event::Code(allocs.take_cow(cow_ix)),
3669        ItemBody::SynthesizeText(cow_ix) => return Event::Text(allocs.take_cow(cow_ix)),
3670        ItemBody::SynthesizeChar(c) => return Event::Text(c.into()),
3671        ItemBody::HtmlBlock(_) => Tag::HtmlBlock,
3672        ItemBody::Html => return Event::Html(text[item.start..item.end].into()),
3673        ItemBody::InlineHtml => return Event::InlineHtml(text[item.start..item.end].into()),
3674        ItemBody::OwnedInlineHtml(cow_ix) => return Event::InlineHtml(allocs.take_cow(cow_ix)),
3675        ItemBody::SoftBreak => return Event::SoftBreak,
3676        ItemBody::HardBreak(_) => return Event::HardBreak,
3677        ItemBody::FootnoteReference(cow_ix) => {
3678            return Event::FootnoteReference(allocs.take_cow(cow_ix));
3679        }
3680        ItemBody::TaskListMarker(checked) => return Event::TaskListMarker(checked),
3681        ItemBody::Rule => return Event::Rule,
3682        ItemBody::Paragraph => Tag::Paragraph,
3683        ItemBody::Emphasis => Tag::Emphasis,
3684        ItemBody::Superscript => Tag::Superscript,
3685        ItemBody::Subscript => Tag::Subscript,
3686        ItemBody::Strong => Tag::Strong,
3687        ItemBody::Strikethrough => Tag::Strikethrough,
3688        ItemBody::Link(link_ix) => {
3689            let (link_type, dest_url, title, id) = allocs.take_link(link_ix);
3690            Tag::Link {
3691                link_type,
3692                dest_url,
3693                title,
3694                id,
3695            }
3696        }
3697        ItemBody::Image(link_ix) => {
3698            let (link_type, dest_url, title, id) = allocs.take_link(link_ix);
3699            Tag::Image {
3700                link_type,
3701                dest_url,
3702                title,
3703                id,
3704            }
3705        }
3706        ItemBody::Heading(level, Some(heading_ix)) => {
3707            let HeadingAttributes { id, classes, attrs } = allocs.index(heading_ix);
3708            Tag::Heading {
3709                level,
3710                id: id.clone(),
3711                classes: classes.clone(),
3712                attrs: attrs.clone(),
3713            }
3714        }
3715        ItemBody::Heading(level, None) => Tag::Heading {
3716            level,
3717            id: None,
3718            classes: Vec::new(),
3719            attrs: Vec::new(),
3720        },
3721        ItemBody::MathBlock(cow_ix) => {
3722            Tag::CodeBlock(CodeBlockKind::Fenced(allocs.take_cow(cow_ix)))
3723        }
3724        ItemBody::FencedCodeBlock(cow_ix, _) => {
3725            Tag::CodeBlock(CodeBlockKind::Fenced(allocs.take_cow(cow_ix)))
3726        }
3727        ItemBody::IndentCodeBlock(..) => Tag::CodeBlock(CodeBlockKind::Indented),
3728        ItemBody::ContainerDirective(_, dir_ix)
3729        | ItemBody::LeafDirective(dir_ix)
3730        | ItemBody::TextDirective(dir_ix) => {
3731            let kind = match item.body {
3732                ItemBody::ContainerDirective(..) => DirectiveKind::Container,
3733                ItemBody::LeafDirective(..) => DirectiveKind::Leaf,
3734                _ => DirectiveKind::Text,
3735            };
3736            let dir = allocs.take_directive(dir_ix);
3737            Tag::Directive {
3738                kind,
3739                name: dir.name,
3740                attributes: dir.attributes,
3741            }
3742        }
3743        ItemBody::BlockQuote(kind) => Tag::BlockQuote(kind),
3744        ItemBody::List(is_tight, c, listitem_start) => {
3745            if c == b'.' || c == b')' {
3746                Tag::List(Some(listitem_start), is_tight)
3747            } else {
3748                Tag::List(None, is_tight)
3749            }
3750        }
3751        ItemBody::ListItem(_, _) => Tag::Item,
3752        ItemBody::TableHead => Tag::TableHead,
3753        ItemBody::TableCell => Tag::TableCell,
3754        ItemBody::TableRow => Tag::TableRow,
3755        ItemBody::Table(alignment_ix) => Tag::Table(allocs.take_alignment(alignment_ix)),
3756        ItemBody::FootnoteDefinition(cow_ix) => Tag::FootnoteDefinition(allocs.take_cow(cow_ix)),
3757        ItemBody::MetadataBlock(kind) => Tag::MetadataBlock(kind),
3758        ItemBody::Math(cow_ix, is_display) => {
3759            return if is_display {
3760                Event::DisplayMath(allocs.take_cow(cow_ix))
3761            } else {
3762                Event::InlineMath(allocs.take_cow(cow_ix))
3763            };
3764        }
3765        ItemBody::DefinitionList(_) => Tag::DefinitionList,
3766        ItemBody::DefinitionListTitle => Tag::DefinitionListTitle,
3767        ItemBody::DefinitionListDefinition(..) => Tag::DefinitionListDefinition,
3768        #[cfg(feature = "mdx")]
3769        ItemBody::MdxJsxFlowElement(jsx_ix) => {
3770            let jsx = allocs.take_jsx_element(jsx_ix);
3771            Tag::MdxJsxFlowElement(jsx.raw)
3772        }
3773        #[cfg(feature = "mdx")]
3774        ItemBody::MdxJsxTextElement(jsx_ix) => {
3775            let jsx = allocs.take_jsx_element(jsx_ix);
3776            Tag::MdxJsxTextElement(jsx.raw)
3777        }
3778        #[cfg(feature = "mdx")]
3779        ItemBody::MdxFlowExpression(cow_ix) => {
3780            return Event::MdxFlowExpression(allocs.take_cow(cow_ix));
3781        }
3782        #[cfg(feature = "mdx")]
3783        ItemBody::MdxTextExpression(cow_ix) => {
3784            return Event::MdxTextExpression(allocs.take_cow(cow_ix));
3785        }
3786        #[cfg(feature = "mdx")]
3787        ItemBody::MdxEsm(cow_ix) => return Event::MdxEsm(allocs.take_cow(cow_ix)),
3788        _ => panic!("unexpected item body {:?}", item.body),
3789    };
3790
3791    Event::Start(tag)
3792}
3793
3794#[cfg(test)]
3795mod test {
3796    use alloc::{borrow::ToOwned, string::ToString, vec::Vec};
3797
3798    use super::*;
3799    use crate::tree::Node;
3800
3801    // TODO: move these tests to tests/html.rs?
3802
3803    fn parser_with_extensions(text: &str) -> Parser<'_> {
3804        let mut opts = Options::empty();
3805        opts.insert(Options::ENABLE_TABLES);
3806        opts.insert(Options::ENABLE_FOOTNOTES);
3807        opts.insert(Options::ENABLE_STRIKETHROUGH);
3808        opts.insert(Options::ENABLE_SUPERSCRIPT);
3809        opts.insert(Options::ENABLE_SUBSCRIPT);
3810        opts.insert(Options::ENABLE_TASKLISTS);
3811
3812        Parser::new_ext(text, opts)
3813    }
3814
3815    #[test]
3816    #[cfg(target_pointer_width = "64")]
3817    fn node_size() {
3818        let node_size = core::mem::size_of::<Node<Item>>();
3819        assert_eq!(48, node_size);
3820    }
3821
3822    #[test]
3823    #[cfg(target_pointer_width = "64")]
3824    fn body_size() {
3825        let body_size = core::mem::size_of::<ItemBody>();
3826        assert_eq!(16, body_size);
3827    }
3828
3829    #[test]
3830    fn single_open_fish_bracket() {
3831        // dont crash
3832        assert_eq!(3, Parser::new("<").count());
3833    }
3834
3835    #[test]
3836    fn lone_hashtag() {
3837        // dont crash
3838        assert_eq!(2, Parser::new("#").count());
3839    }
3840
3841    #[test]
3842    fn lots_of_backslashes() {
3843        // dont crash
3844        Parser::new("\\\\\r\r").count();
3845        Parser::new("\\\r\r\\.\\\\\r\r\\.\\").count();
3846    }
3847
3848    #[test]
3849    fn issue_1030() {
3850        let mut opts = Options::empty();
3851        opts.insert(Options::ENABLE_WIKILINKS);
3852
3853        let parser = Parser::new_ext("For a new ferrari, [[Wikientry|click here]]!", opts);
3854
3855        let offsets = parser
3856            .into_offset_iter()
3857            .map(|(_ev, range)| range)
3858            .collect::<Vec<_>>();
3859        let expected_offsets = vec![
3860            (0..44),  // Paragraph START
3861            (0..19),  // `For a new ferrari, `
3862            (19..43), // Wikilink START
3863            (31..41), // `click here`
3864            (19..43), // Wikilink END
3865            (43..44), // `!`
3866            (0..44),  // Paragraph END
3867        ];
3868        assert_eq!(offsets, expected_offsets);
3869    }
3870
3871    #[test]
3872    fn issue_320() {
3873        // dont crash
3874        parser_with_extensions(":\r\t> |\r:\r\t> |\r").count();
3875    }
3876
3877    #[test]
3878    fn issue_319() {
3879        // dont crash
3880        parser_with_extensions("|\r-]([^|\r-]([^").count();
3881        parser_with_extensions("|\r\r=][^|\r\r=][^car").count();
3882    }
3883
3884    #[test]
3885    fn issue_303() {
3886        // dont crash
3887        parser_with_extensions("[^\r\ra]").count();
3888        parser_with_extensions("\r\r]Z[^\x00\r\r]Z[^\x00").count();
3889    }
3890
3891    #[test]
3892    fn issue_313() {
3893        // dont crash
3894        parser_with_extensions("*]0[^\r\r*]0[^").count();
3895        parser_with_extensions("[^\r> `][^\r> `][^\r> `][").count();
3896    }
3897
3898    #[test]
3899    fn issue_311() {
3900        // dont crash
3901        parser_with_extensions("\\\u{0d}-\u{09}\\\u{0d}-\u{09}").count();
3902    }
3903
3904    #[test]
3905    fn issue_283() {
3906        let input = core::str::from_utf8(b"\xf0\x9b\xb2\x9f<td:^\xf0\x9b\xb2\x9f").unwrap();
3907        // dont crash
3908        parser_with_extensions(input).count();
3909    }
3910
3911    #[test]
3912    fn issue_289() {
3913        // dont crash
3914        parser_with_extensions("> - \\\n> - ").count();
3915        parser_with_extensions("- \n\n").count();
3916    }
3917
3918    #[test]
3919    fn issue_306() {
3920        // dont crash
3921        parser_with_extensions("*\r_<__*\r_<__*\r_<__*\r_<__").count();
3922    }
3923
3924    #[test]
3925    fn issue_305() {
3926        // dont crash
3927        parser_with_extensions("_6**6*_*").count();
3928    }
3929
3930    #[test]
3931    fn another_emphasis_panic() {
3932        parser_with_extensions("*__#_#__*").count();
3933    }
3934
3935    #[test]
3936    fn offset_iter() {
3937        let event_offsets: Vec<_> = Parser::new("*hello* world")
3938            .into_offset_iter()
3939            .map(|(_ev, range)| range)
3940            .collect();
3941        let expected_offsets = vec![(0..13), (0..7), (1..6), (0..7), (7..13), (0..13)];
3942        assert_eq!(expected_offsets, event_offsets);
3943    }
3944
3945    #[test]
3946    fn reference_link_offsets() {
3947        let range =
3948            Parser::new("# H1\n[testing][Some reference]\n\n[Some reference]: https://github.com")
3949                .into_offset_iter()
3950                .filter_map(|(ev, range)| match ev {
3951                    Event::Start(
3952                        Tag::Link {
3953                            link_type: LinkType::Reference,
3954                            ..
3955                        },
3956                        ..,
3957                    ) => Some(range),
3958                    _ => None,
3959                })
3960                .next()
3961                .unwrap();
3962        assert_eq!(5..30, range);
3963    }
3964
3965    #[test]
3966    fn footnote_offsets() {
3967        let range = parser_with_extensions("Testing this[^1] out.\n\n[^1]: Footnote.")
3968            .into_offset_iter()
3969            .filter_map(|(ev, range)| match ev {
3970                Event::FootnoteReference(..) => Some(range),
3971                _ => None,
3972            })
3973            .next()
3974            .unwrap();
3975        assert_eq!(12..16, range);
3976    }
3977
3978    #[test]
3979    fn footnote_offsets_exclamation() {
3980        let mut immediately_before_footnote = None;
3981        let range = parser_with_extensions("Testing this![^1] out.\n\n[^1]: Footnote.")
3982            .into_offset_iter()
3983            .filter_map(|(ev, range)| match ev {
3984                Event::FootnoteReference(..) => Some(range),
3985                _ => {
3986                    immediately_before_footnote = Some((ev, range));
3987                    None
3988                }
3989            })
3990            .next()
3991            .unwrap();
3992        assert_eq!(13..17, range);
3993        if let (Event::Text(exclamation), range_exclamation) =
3994            immediately_before_footnote.as_ref().unwrap()
3995        {
3996            assert_eq!("!", &exclamation[..]);
3997            assert_eq!(&(12..13), range_exclamation);
3998        } else {
3999            panic!("what came first, then? {immediately_before_footnote:?}");
4000        }
4001    }
4002
4003    #[test]
4004    fn table_offset() {
4005        let markdown = "a\n\nTesting|This|Outtt\n--|:--:|--:\nSome Data|Other data|asdf";
4006        let event_offset = parser_with_extensions(markdown)
4007            .into_offset_iter()
4008            .map(|(_ev, range)| range)
4009            .nth(3)
4010            .unwrap();
4011        let expected_offset = 3..59;
4012        assert_eq!(expected_offset, event_offset);
4013    }
4014
4015    #[test]
4016    fn table_cell_span() {
4017        let markdown = "a|b|c\n--|--|--\na|  |c";
4018        let event_offset = parser_with_extensions(markdown)
4019            .into_offset_iter()
4020            .filter_map(|(ev, span)| match ev {
4021                Event::Start(Tag::TableCell) => Some(span),
4022                _ => None,
4023            })
4024            .nth(4)
4025            .unwrap();
4026        // Cell span includes the leading `|` delimiter (matching remark).
4027        let expected_offset_start = "a|b|c\n--|--|--\na".len();
4028        assert_eq!(
4029            expected_offset_start..(expected_offset_start + 3),
4030            event_offset
4031        );
4032    }
4033
4034    #[test]
4035    fn offset_iter_issue_378() {
4036        let event_offsets: Vec<_> = Parser::new("a [b](c) d")
4037            .into_offset_iter()
4038            .map(|(_ev, range)| range)
4039            .collect();
4040        let expected_offsets = vec![(0..10), (0..2), (2..8), (3..4), (2..8), (8..10), (0..10)];
4041        assert_eq!(expected_offsets, event_offsets);
4042    }
4043
4044    #[test]
4045    fn offset_iter_issue_404() {
4046        let event_offsets: Vec<_> = Parser::new("###\n")
4047            .into_offset_iter()
4048            .map(|(_ev, range)| range)
4049            .collect();
4050        let expected_offsets = vec![(0..4), (0..4)];
4051        assert_eq!(expected_offsets, event_offsets);
4052    }
4053
4054    #[test]
4055    fn broken_links_called_only_once() {
4056        for &(markdown, expected) in &[
4057            ("See also [`g()`][crate::g].", 1),
4058            ("See also [`g()`][crate::g][].", 1),
4059            ("[brokenlink1] some other node [brokenlink2]", 2),
4060        ] {
4061            let mut times_called = 0;
4062            let callback = &mut |_broken_link: BrokenLink| {
4063                times_called += 1;
4064                None
4065            };
4066            let parser =
4067                Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(callback));
4068            for _ in parser {}
4069            assert_eq!(times_called, expected);
4070        }
4071    }
4072
4073    #[test]
4074    fn simple_broken_link_callback() {
4075        let test_str = "This is a link w/o def: [hello][world]";
4076        let mut callback = |broken_link: BrokenLink| {
4077            assert_eq!("world", broken_link.reference.as_ref());
4078            assert_eq!(&test_str[broken_link.span], "[hello][world]");
4079            let url = "YOLO".into();
4080            let title = "SWAG".to_owned().into();
4081            Some((url, title))
4082        };
4083        let parser =
4084            Parser::new_with_broken_link_callback(test_str, Options::empty(), Some(&mut callback));
4085        let mut link_tag_count = 0;
4086        for (typ, url, title, id) in parser.filter_map(|event| match event {
4087            Event::Start(Tag::Link {
4088                link_type,
4089                dest_url,
4090                title,
4091                id,
4092            }) => Some((link_type, dest_url, title, id)),
4093            _ => None,
4094        }) {
4095            link_tag_count += 1;
4096            assert_eq!(typ, LinkType::ReferenceUnknown);
4097            assert_eq!(url.as_ref(), "YOLO");
4098            assert_eq!(title.as_ref(), "SWAG");
4099            assert_eq!(id.as_ref(), "world");
4100        }
4101        assert!(link_tag_count > 0);
4102    }
4103
4104    #[test]
4105    fn code_block_kind_check_fenced() {
4106        let parser = Parser::new("hello\n```test\ntadam\n```");
4107        let mut found = 0;
4108        for (ev, _range) in parser.into_offset_iter() {
4109            if let Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(syntax))) = ev {
4110                assert_eq!(syntax.as_ref(), "test");
4111                found += 1;
4112            }
4113        }
4114        assert_eq!(found, 1);
4115    }
4116
4117    #[test]
4118    fn code_block_kind_check_indented() {
4119        let parser = Parser::new("hello\n\n    ```test\n    tadam\nhello");
4120        let mut found = 0;
4121        for (ev, _range) in parser.into_offset_iter() {
4122            if let Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) = ev {
4123                found += 1;
4124            }
4125        }
4126        assert_eq!(found, 1);
4127    }
4128
4129    #[test]
4130    fn ref_defs() {
4131        let input = r###"[a B c]: http://example.com
4132[another]: https://google.com
4133
4134text
4135
4136[final ONE]: http://wikipedia.org
4137"###;
4138        let mut parser = Parser::new(input);
4139
4140        assert!(parser.reference_definitions().get("a b c").is_some());
4141        assert!(parser.reference_definitions().get("nope").is_none());
4142
4143        if let Some(_event) = parser.next() {
4144            // testing keys with shorter lifetimes than parser and its input
4145            let s = "final one".to_owned();
4146            let link_def = parser.reference_definitions().get(&s).unwrap();
4147            let span = &input[link_def.span.clone()];
4148            assert_eq!(span, "[final ONE]: http://wikipedia.org");
4149        }
4150    }
4151
4152    #[test]
4153    #[allow(clippy::extra_unused_lifetimes)]
4154    fn common_lifetime_patterns_allowed<'b>() {
4155        let temporary_str = String::from("xyz");
4156
4157        // NOTE: this is a limitation of Rust, it doesn't allow putting lifetime parameters on the closure itself.
4158        // Hack it by attaching the lifetime to the test function instead.
4159        // TODO: why is the `'b` lifetime required at all? Changing it to `'_` breaks things :(
4160        let mut closure = |link: BrokenLink<'b>| Some(("#".into(), link.reference));
4161
4162        fn function(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)> {
4163            Some(("#".into(), link.reference))
4164        }
4165
4166        for _ in Parser::new_with_broken_link_callback(
4167            "static lifetime",
4168            Options::empty(),
4169            Some(&mut closure),
4170        ) {}
4171        /* This fails to compile. Because the closure can't say `for <'a> fn(BrokenLink<'a>) ->
4172         * CowStr<'a>` and has to use the enclosing `'b` lifetime parameter, `temporary_str` lives
4173         * shorter than `'b`. I think this is unlikely to occur in real life, and if it does, the
4174         * fix is simple: move it out to a function that allows annotating the lifetimes.
4175         */
4176        //for _ in Parser::new_with_broken_link_callback(&temporary_str, Options::empty(), Some(&mut callback)) {
4177        //}
4178
4179        for _ in Parser::new_with_broken_link_callback(
4180            "static lifetime",
4181            Options::empty(),
4182            Some(&mut function),
4183        ) {}
4184        for _ in Parser::new_with_broken_link_callback(
4185            &temporary_str,
4186            Options::empty(),
4187            Some(&mut function),
4188        ) {}
4189    }
4190
4191    #[test]
4192    fn inline_html_inside_blockquote() {
4193        // Regression for #960
4194        let input = "> <foo\n> bar>";
4195        let events: Vec<_> = Parser::new(input).collect();
4196        let expected = [
4197            Event::Start(Tag::BlockQuote(None)),
4198            Event::Start(Tag::Paragraph),
4199            Event::InlineHtml(CowStr::Boxed("<foo\nbar>".to_string().into())),
4200            Event::End(TagEnd::Paragraph),
4201            Event::End(TagEnd::BlockQuote(None)),
4202        ];
4203        assert_eq!(&events, &expected);
4204    }
4205
4206    #[test]
4207    fn wikilink_has_pothole() {
4208        let input = "[[foo]] [[bar|baz]]";
4209        let events: Vec<_> = Parser::new_ext(input, Options::ENABLE_WIKILINKS).collect();
4210        let expected = [
4211            Event::Start(Tag::Paragraph),
4212            Event::Start(Tag::Link {
4213                link_type: LinkType::WikiLink { has_pothole: false },
4214                dest_url: CowStr::Borrowed("foo"),
4215                title: CowStr::Borrowed(""),
4216                id: CowStr::Borrowed(""),
4217            }),
4218            Event::Text(CowStr::Borrowed("foo")),
4219            Event::End(TagEnd::Link),
4220            Event::Text(CowStr::Borrowed(" ")),
4221            Event::Start(Tag::Link {
4222                link_type: LinkType::WikiLink { has_pothole: true },
4223                dest_url: CowStr::Borrowed("bar"),
4224                title: CowStr::Borrowed(""),
4225                id: CowStr::Borrowed(""),
4226            }),
4227            Event::Text(CowStr::Borrowed("baz")),
4228            Event::End(TagEnd::Link),
4229            Event::End(TagEnd::Paragraph),
4230        ];
4231        assert_eq!(&events, &expected);
4232    }
4233
4234    #[cfg(feature = "mdx")]
4235    fn mdx_parser(text: &str) -> Parser<'_> {
4236        Parser::new_ext(text, Options::ENABLE_MDX)
4237    }
4238
4239    #[cfg(feature = "mdx")]
4240    #[test]
4241    fn mdx_esm_import() {
4242        let events: Vec<_> = mdx_parser("import {Chart} from './chart.js'\n").collect();
4243        assert_eq!(events.len(), 1);
4244        assert!(matches!(&events[0], Event::MdxEsm(s) if s.contains("import")));
4245    }
4246
4247    #[cfg(feature = "mdx")]
4248    #[test]
4249    fn mdx_esm_export() {
4250        let events: Vec<_> = mdx_parser("export const meta = {}\n").collect();
4251        assert_eq!(events.len(), 1);
4252        assert!(matches!(&events[0], Event::MdxEsm(s) if s.contains("export")));
4253    }
4254
4255    #[cfg(feature = "mdx")]
4256    #[test]
4257    fn mdx_flow_expression() {
4258        let events: Vec<_> = mdx_parser("{1 + 1}\n").collect();
4259        assert_eq!(events.len(), 1);
4260        assert!(matches!(&events[0], Event::MdxFlowExpression(s) if s.as_ref() == "1 + 1"));
4261    }
4262
4263    #[cfg(feature = "mdx")]
4264    #[test]
4265    fn mdx_jsx_flow_self_closing() {
4266        let events: Vec<_> = mdx_parser("<Chart values={[1,2,3]} />\n").collect();
4267        assert!(!events.is_empty());
4268        assert!(
4269            matches!(&events[0], Event::Start(Tag::MdxJsxFlowElement(s)) if s.contains("Chart"))
4270        );
4271    }
4272
4273    #[cfg(feature = "mdx")]
4274    #[test]
4275    fn mdx_jsx_flow_fragment() {
4276        let events: Vec<_> = mdx_parser("<>\n").collect();
4277        assert!(!events.is_empty());
4278        assert!(matches!(
4279            &events[0],
4280            Event::Start(Tag::MdxJsxFlowElement(_))
4281        ));
4282    }
4283
4284    #[cfg(feature = "mdx")]
4285    #[test]
4286    fn mdx_inline_expression() {
4287        let events: Vec<_> = mdx_parser("hello {name} world\n").collect();
4288        let has_expr = events
4289            .iter()
4290            .any(|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "name"));
4291        assert!(
4292            has_expr,
4293            "Expected inline MDX expression, got: {:?}",
4294            events
4295        );
4296    }
4297
4298    #[cfg(feature = "mdx")]
4299    #[test]
4300    fn mdx_inline_jsx() {
4301        let events: Vec<_> = mdx_parser("hello <Badge /> world\n").collect();
4302        let has_jsx = events
4303            .iter()
4304            .any(|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("Badge")));
4305        assert!(has_jsx, "Expected inline MDX JSX, got: {:?}", events);
4306    }
4307
4308    #[cfg(feature = "mdx")]
4309    #[test]
4310    fn mdx_all_tags_are_jsx() {
4311        // In MDX mode, all tags (including lowercase) are JSX, not HTML.
4312        let events: Vec<_> = mdx_parser("hello <em>world</em>\n").collect();
4313        let has_jsx = events
4314            .iter()
4315            .any(|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(_))));
4316        assert!(has_jsx, "In MDX mode, <em> should be JSX: {:?}", events);
4317    }
4318
4319    #[test]
4320    fn mdx_does_not_interfere_without_flag() {
4321        // Without ENABLE_MDX, none of this should be parsed as MDX.
4322        let events: Vec<_> = Parser::new("import foo from 'bar'\n").collect();
4323        // Should be a regular paragraph.
4324        assert!(
4325            events
4326                .iter()
4327                .any(|e| matches!(e, Event::Start(Tag::Paragraph)))
4328        );
4329    }
4330
4331    #[cfg(feature = "mdx")]
4332    #[test]
4333    fn mdx_expression_in_heading() {
4334        let events: Vec<_> = mdx_parser("# {title}\n").collect();
4335        let has_heading = events
4336            .iter()
4337            .any(|e| matches!(e, Event::Start(Tag::Heading { .. })));
4338        assert!(has_heading, "Should have a heading");
4339        let has_expr = events
4340            .iter()
4341            .any(|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "title"));
4342        assert!(
4343            has_expr,
4344            "Heading should contain MdxTextExpression, got: {:?}",
4345            events
4346        );
4347    }
4348
4349    #[cfg(feature = "mdx")]
4350    #[test]
4351    fn mdx_expression_mixed_text_in_heading() {
4352        let events: Vec<_> = mdx_parser("## Hello {name}\n").collect();
4353        let has_text = events
4354            .iter()
4355            .any(|e| matches!(e, Event::Text(s) if s.contains("Hello")));
4356        let has_expr = events
4357            .iter()
4358            .any(|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "name"));
4359        assert!(has_text, "Should have text, got: {:?}", events);
4360        assert!(has_expr, "Should have expression, got: {:?}", events);
4361    }
4362}