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