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