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