Skip to main content

markdown_syntax/
parse.rs

1//! Markdown source to AST. The entry point is the free [`parse`] function,
2//! which reads the crate's one syntax. Parsing is tolerant: problems are
3//! collected as [`Diagnostic`]s rather than aborting.
4
5use alloc::{borrow::Cow, collections::BTreeMap, string::String, vec, vec::Vec};
6
7use crate::{
8    ast::*,
9    decode::{
10        decode_escapes_and_references, decode_selected_escapes_and_references,
11        parse_character_reference,
12    },
13    diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
14    memo::{
15        bracket_walk, path_walk, pattern_starts, BracketMemo, BracketStep, PathMemo, Positions,
16        Step,
17    },
18    span::Span,
19    validate::is_directive_name,
20};
21
22mod blank_lines;
23mod blocks;
24mod nul_replacement;
25#[cfg(test)]
26mod scan_tests;
27mod source_map;
28mod tags;
29mod tokens;
30
31pub(crate) use blocks::interrupts_paragraph;
32use source_map::{DerivedText, Segment, SourceMap};
33
34/// The largest ordered-list item number: a marker holds at most 9 digits.
35pub(crate) const MAX_ORDERED_NUMBER: u64 = 999_999_999;
36
37/// The result of a tolerant parse: the document plus any diagnostics gathered
38/// along the way (empty on a clean parse).
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ParseOutput {
41    /// The parsed document tree.
42    pub document: Document,
43    /// Diagnostics collected during parsing.
44    pub diagnostics: Vec<Diagnostic>,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48struct ParsedLinkResource {
49    destination: String,
50    destination_kind: LinkDestinationKind,
51    title: Option<Title>,
52}
53
54/// Where a link's destination and title lie in the input the tail was read
55/// from: the destination without its angle brackets, empty where it is
56/// omitted, and the title without its delimiters.
57#[derive(Clone, Copy)]
58struct LinkTailRanges {
59    destination: (usize, usize),
60    title: Option<(usize, usize)>,
61}
62
63const REFERENCE_LABEL_MAX_CHARS: usize = 999;
64const WIKILINK_MAX_BYTES: usize = 999;
65
66/// One line of the text a block parser reads. `start`, `end`, and
67/// `end_with_eol` are positions in the original input; offsets into `text`
68/// translate to the input through `source_start` / `source_end`, since a
69/// container's lines are derived from its source lines with prefixes removed.
70#[derive(Clone, Copy, Debug)]
71struct Line<'a> {
72    text: &'a str,
73    eol: &'a str,
74    start: usize,
75    end: usize,
76    end_with_eol: usize,
77    /// The source-map segments covering `text` and `eol`, in the coordinates of
78    /// the string the line was split from, where `text` starts at `text_offset`.
79    segments: &'a [Segment],
80    text_offset: usize,
81    /// The source column `text` starts at, from which its tabs reach their
82    /// tab stops (every four columns).
83    column: usize,
84}
85
86impl<'a> Line<'a> {
87    /// A line checked for its shape alone, whose positions are not used.
88    fn detached(text: &'a str) -> Self {
89        Line {
90            text,
91            eol: "",
92            start: 0,
93            end: text.len(),
94            end_with_eol: text.len(),
95            segments: &[],
96            text_offset: 0,
97            column: 0,
98        }
99    }
100
101    /// The source column after `self.text[..offset]`.
102    fn column_at(&self, offset: usize) -> usize {
103        advance_columns(self.column, &self.text[..offset])
104    }
105
106    /// The input position where a node starting at byte `offset` of `text`
107    /// starts.
108    fn source_start(&self, offset: usize) -> usize {
109        source_map::start_of(self.segments, self.text_offset + offset)
110    }
111
112    /// The input position where a node ending at byte `offset` of `text` ends.
113    fn source_end(&self, offset: usize) -> usize {
114        source_map::end_of(self.segments, self.text_offset + offset)
115    }
116
117    /// The input range of this line's ending.
118    fn eol_source(&self) -> (usize, usize) {
119        (self.end, self.end_with_eol)
120    }
121
122    /// Records in `map`, from text position `at`, what `text[from..to]` was
123    /// read from.
124    fn copy_into(&self, map: &mut SourceMap, at: usize, from: usize, to: usize) {
125        map.copy(
126            at,
127            self.segments,
128            self.text_offset + from,
129            self.text_offset + to,
130        );
131    }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135enum HtmlBlockKind {
136    RawTag,
137    BlockTag,
138    Until(&'static str),
139    UntilBlank,
140}
141
142/// Parse `input`. Infallible and tolerant: problems are reported as
143/// diagnostics in the returned [`ParseOutput`].
144pub fn parse(input: &str) -> ParseOutput {
145    let mut diagnostics = Vec::new();
146    // A leading byte order mark is not content; parsing starts after it while
147    // spans keep counting from the start of `input`.
148    let start = if input.starts_with('\u{feff}') {
149        '\u{feff}'.len_utf8()
150    } else {
151        0
152    };
153    let source = &input[start..];
154    let map = SourceMap::verbatim(source.len(), start);
155    let lines = collect_lines(source, &map);
156    let mut children = blocks::parse_document(&lines, &mut diagnostics);
157    blank_lines::add_blank_lines(input, &mut children);
158    let mut document = Document::parsed(
159        NodeMeta::new(Some(Span::new(0, input.len()))),
160        children,
161        input,
162    );
163    if source.contains('\0') {
164        nul_replacement::replace_in_document(&mut document);
165    }
166
167    ParseOutput {
168        document,
169        diagnostics,
170    }
171}
172
173/// The link reference definitions an inline parse resolves references
174/// against: they decide whether `[a][b]`, `[a][]`, and `[a]` are reference
175/// links or images.
176#[derive(Clone, Debug, Default, Eq, PartialEq)]
177pub struct Definitions {
178    /// Normalized identifiers, sorted and deduplicated.
179    identifiers: Vec<String>,
180}
181
182impl Definitions {
183    /// The definitions of `document`, at every depth, as `parse` reads them
184    /// for the document's own references.
185    pub fn of(document: &Document) -> Self {
186        fn walk(blocks: &[Block], identifiers: &mut Vec<String>) {
187            for block in blocks {
188                match block {
189                    Block::Definition(node) => identifiers.push(node.identifier.clone()),
190                    Block::BlockQuote(node) => walk(&node.children, identifiers),
191                    Block::Alert(node) => walk(&node.children, identifiers),
192                    Block::List(node) => {
193                        for item in &node.children {
194                            walk(&item.children, identifiers);
195                        }
196                    }
197                    Block::FootnoteDefinition(node) => walk(&node.children, identifiers),
198                    Block::HtmlContainer(node) => {
199                        if let HtmlContainerContent::Blocks(children) = &node.content {
200                            walk(children, identifiers);
201                        }
202                    }
203                    Block::ContainerDirective(node) => walk(&node.children, identifiers),
204                    _ => {}
205                }
206            }
207        }
208        let mut identifiers = Vec::new();
209        walk(&document.children, &mut identifiers);
210        Self::sorted(identifiers)
211    }
212
213    /// Definitions with the given labels, as they would be written between
214    /// the brackets of a definition.
215    pub fn new<I, S>(labels: I) -> Self
216    where
217        I: IntoIterator<Item = S>,
218        S: AsRef<str>,
219    {
220        Self::sorted(
221            labels
222                .into_iter()
223                .map(|label| identifier(label.as_ref()))
224                .collect(),
225        )
226    }
227
228    fn sorted(mut identifiers: Vec<String>) -> Self {
229        identifiers.sort_unstable();
230        identifiers.dedup();
231        Self { identifiers }
232    }
233
234    fn labels(&self) -> Labels<'_> {
235        Labels {
236            own: &self.identifiers,
237        }
238    }
239}
240
241/// The result of an inline parse: the inlines, the tokens of the text, and
242/// any diagnostics.
243#[derive(Clone, Debug, Eq, PartialEq)]
244#[non_exhaustive]
245pub struct InlineOutput {
246    /// The inline nodes, with spans that are byte ranges into the text.
247    pub inlines: Vec<Inline>,
248    /// Runs that tile the text in order, each naming the innermost inline
249    /// that holds it and the part of that inline it is.
250    pub tokens: Vec<InlineToken>,
251    /// Diagnostics collected during parsing.
252    pub diagnostics: Vec<Diagnostic>,
253}
254
255impl InlineOutput {
256    /// The inlines in pre-order through [`Inline::children`]: a link's text,
257    /// an image's alt, and a directive's label included. An
258    /// [`InlineToken`]'s `node` indexes this list.
259    pub fn nodes(&self) -> Vec<&Inline> {
260        let mut nodes = Vec::new();
261        let mut stack: Vec<&Inline> = self.inlines.iter().rev().collect();
262        while let Some(inline) = stack.pop() {
263            nodes.push(inline);
264            stack.extend(inline.children().iter().rev());
265        }
266        nodes
267    }
268}
269
270/// A run of the text of an inline parse: the innermost inline that holds it,
271/// as its index in [`InlineOutput::nodes`], and the part of that inline it
272/// is.
273#[derive(Clone, Copy, Debug, Eq, PartialEq)]
274pub struct InlineToken {
275    /// The run, a byte range into the text.
276    pub span: Span,
277    /// The innermost inline holding the run, as its index in
278    /// [`InlineOutput::nodes`].
279    pub node: usize,
280    /// The part of that inline the run is.
281    pub role: TokenRole,
282}
283
284/// The part of an inline a token is.
285#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
286#[non_exhaustive]
287pub enum TokenRole {
288    /// The inline's content: a text node, raw HTML, a soft break, a hard
289    /// break's line ending, an escaped character, a character reference
290    /// between `&` and `;`, and code or math inside its delimiters apart
291    /// from the padding its value drops, which is syntax.
292    Text,
293    /// A character of the inline's own syntax.
294    Syntax,
295    /// A link's, image's, or autolink's destination.
296    Destination,
297    /// A link's or image's title, inside its delimiters.
298    Title,
299    /// A full reference's second label, a footnote reference's label, or a
300    /// wiki link's text after its pipe.
301    Label,
302    /// A wiki link's target.
303    Target,
304    /// A directive's, shortcode's, or tag's name.
305    Name,
306    /// A directive's attributes, inside its braces.
307    Attributes,
308}
309
310/// Parse `text` as the inline content of one leaf block, with the same
311/// syntax as [`parse`] and no block rule: `# a` and `- a` stay text. A
312/// reference forms only for a label `definitions` holds. Infallible, like
313/// [`parse`]; a leading U+FEFF is content.
314pub fn parse_inline(text: &str, definitions: &Definitions) -> InlineOutput {
315    read_inline(text, definitions, true)
316}
317
318/// What `parse_inline` reads from `text`, without its tokens, for callers
319/// that read only the tree.
320pub(crate) fn parse_inline_tree(text: &str, definitions: &Definitions) -> InlineOutput {
321    read_inline(text, definitions, false)
322}
323
324fn read_inline(text: &str, definitions: &Definitions, with_tokens: bool) -> InlineOutput {
325    let mut diagnostics = Vec::new();
326    // Line endings are read as `\n`, as a block's content is joined, while
327    // spans keep counting in `text`.
328    let (content, map) = if text.contains('\r') {
329        let mut content = String::with_capacity(text.len());
330        let mut map = SourceMap::default();
331        for line in collect_lines(text, &SourceMap::verbatim(text.len(), 0)) {
332            map.push(content.len(), line.text.len(), line.start, line.end);
333            content.push_str(line.text);
334            if !line.eol.is_empty() {
335                map.push(content.len(), 1, line.end, line.end_with_eol);
336                content.push('\n');
337            }
338        }
339        (Cow::Owned(content), map)
340    } else {
341        (Cow::Borrowed(text), SourceMap::verbatim(text.len(), 0))
342    };
343    let mut tokens = Vec::new();
344    let mut inlines = parse_inlines(
345        &content,
346        &map,
347        Some(definitions.labels()),
348        &mut diagnostics,
349        with_tokens.then_some(&mut tokens),
350    );
351    if text.contains('\0') {
352        nul_replacement::replace_in_inlines(&mut inlines);
353    }
354    diagnostics.sort_by_key(|diagnostic| diagnostic.span.map(|span| span.start));
355    InlineOutput {
356        inlines,
357        tokens,
358        diagnostics,
359    }
360}
361
362/// The inline content the parser read for one paragraph or heading, from
363/// [`Document::inline_source`], with where each of its offsets sits in the
364/// document's source.
365#[derive(Clone, Debug)]
366pub struct InlineSource {
367    /// The text the inline pass read: container markers, indentation, and an
368    /// ATX heading's opening and closing sequences removed, lines joined with
369    /// `\n`, and a setext heading without its underline.
370    pub text: String,
371    map: SourceMap,
372}
373
374impl InlineSource {
375    /// The source offset of `text[offset..]`; `offset == text.len()` maps to
376    /// the end of the leaf's content.
377    pub fn to_source(&self, offset: usize) -> usize {
378        self.span_to_source(Span::new(offset, offset)).start
379    }
380
381    /// The source span of `span`, a span in `text`, as the parser maps the
382    /// spans of the inlines it reads from `text`.
383    pub fn span_to_source(&self, span: Span) -> Span {
384        source_map::translate_span(&self.map, span)
385    }
386}
387
388/// Maps the spans of `inlines`, read from `source.text`, to the source, as
389/// the parser maps a leaf's inlines. The writer uses it; it is public for
390/// tests.
391#[doc(hidden)]
392pub fn inline_spans_to_source(source: &InlineSource, inlines: &mut [Inline]) {
393    source_map::translate_inlines(&source.map, inlines, &mut []);
394}
395
396impl Document {
397    /// The inline content the parser read for the leaf at `path`: a
398    /// paragraph, a heading, a table cell (its table's path, its row's index,
399    /// and its own), or the label of a leaf or container directive (its
400    /// directive's path), read again from the source of the top-level block
401    /// holding it, so that the cost is linear in that block. `None` for a
402    /// document without a source, or a path that names no such leaf, a
403    /// directive without a label, or a node without a span or one the source
404    /// does not hold.
405    pub fn inline_source(&self, path: &NodePath) -> Option<InlineSource> {
406        let source = self.source()?;
407        let (&first, rest) = path.0.split_first()?;
408        let top = self.children.get(first)?;
409        let mut block = top;
410        let mut rest = rest.iter();
411        let leaf = loop {
412            let Some(&index) = rest.next() else {
413                break match block {
414                    Block::Paragraph(node) => node.meta.span?,
415                    Block::Heading(node) => node.meta.span?,
416                    Block::LeafDirective(node) => node.meta.span?,
417                    Block::ContainerDirective(node) => node.meta.span?,
418                    _ => return None,
419                };
420            };
421            block = match block {
422                Block::BlockQuote(node) => node.children.get(index)?,
423                Block::Alert(node) => node.children.get(index)?,
424                Block::FootnoteDefinition(node) => node.children.get(index)?,
425                Block::ContainerDirective(node) => node.children.get(index)?,
426                Block::HtmlContainer(node) => match &node.content {
427                    HtmlContainerContent::Blocks(children) => children.get(index)?,
428                    _ => return None,
429                },
430                Block::List(node) => {
431                    let item = node.children.get(index)?;
432                    item.children.get(*rest.next()?)?
433                }
434                Block::Table(node) => {
435                    let cell = node.rows.get(index)?.cells.get(*rest.next()?)?;
436                    if rest.next().is_some() {
437                        return None;
438                    }
439                    break cell.meta.span?;
440                }
441                _ => return None,
442            };
443        };
444        let end = top.span()?.end;
445        // The top-level block is read with the definitions right before it,
446        // which may have opened its paragraph, so that a line reads as a
447        // continuation there and not as indented code; and from the start of
448        // its line, where it reads as it does in the document.
449        let opening = self.children[..first]
450            .iter()
451            .rev()
452            .take_while(|block| matches!(block, Block::Definition(_)))
453            .last()
454            .unwrap_or(top);
455        let start = opening.span()?.start;
456        if end > source.len() || start > end || !source.is_char_boundary(start) {
457            return None;
458        }
459        let bytes = source.as_bytes();
460        let mut start = start;
461        while start > 0 && !matches!(bytes[start - 1], b'\n' | b'\r') {
462            start -= 1;
463        }
464        if start == 0 && source.starts_with('\u{feff}') {
465            start = '\u{feff}'.len_utf8().min(end);
466        }
467        let slice = source.get(start..end)?;
468        let map = SourceMap::verbatim(slice.len(), start);
469        let lines = collect_lines(slice, &map);
470        let (_, text) = blocks::leaf_contents(&lines)
471            .into_iter()
472            .find(|(span, _)| *span == leaf)?;
473        let (text, map) = text.into_parts();
474        Some(InlineSource { text, map })
475    }
476
477    /// The path and inline source of every leaf of the document, in document
478    /// order, each as [`Document::inline_source`] returns it, read in one
479    /// pass over the source. Empty for a document without a source.
480    pub fn leaf_sources(&self) -> Vec<(NodePath, InlineSource)> {
481        let mut found = Vec::new();
482        self.each_leaf_source(|path, _, source| found.push((NodePath(path.to_vec()), source)));
483        found
484    }
485
486    /// Calls `found` with the path, span, and inline source of every leaf of
487    /// the document, in document order, as `leaf_sources` returns them.
488    pub(crate) fn each_leaf_source(&self, mut found: impl FnMut(&[usize], Span, InlineSource)) {
489        let Some(source) = self.source() else {
490            return;
491        };
492        let start = if source.starts_with('\u{feff}') {
493            '\u{feff}'.len_utf8()
494        } else {
495            0
496        };
497        let map = SourceMap::verbatim(source.len() - start, start);
498        let lines = collect_lines(&source[start..], &map);
499        let mut contents: Vec<(Span, Option<DerivedText>)> = blocks::leaf_contents(&lines)
500            .into_iter()
501            .map(|(span, text)| (span, Some(text)))
502            .collect();
503        // The contents are in document order, so their starts never fall.
504        let mut take = |path: &[usize], span: Option<Span>, _: Option<&str>| {
505            let Some(span) = span else { return };
506            let first = contents.partition_point(|(other, _)| other.start < span.start);
507            let entry = contents[first..]
508                .iter_mut()
509                .take_while(|(other, _)| other.start == span.start)
510                .find(|(other, text)| *other == span && text.is_some());
511            if let Some((_, text)) = entry {
512                let (text, map) = text.take().expect("found unused").into_parts();
513                found(path, span, InlineSource { text, map });
514            }
515        };
516        crate::leaves::visit(&self.children, &mut Vec::new(), &mut take);
517    }
518}
519
520/// CommonMark reads U+0000 as U+FFFD. Character classifications that differ
521/// between the two call this; node values are rewritten after parsing by
522/// `nul_replacement::replace_in_document`, so spans stay in source coordinates.
523fn source_char(char: char) -> char {
524    if char == '\0' {
525        '\u{FFFD}'
526    } else {
527        char
528    }
529}
530
531/// The deepest block-container nesting (block quotes, list items, container
532/// directives, footnote definitions, HTML containers) the parser opens. Deeper container markers stay leaf-block text, so recursion
533/// and the native stack it uses stay bounded.
534pub(crate) const MAX_BLOCK_NESTING: usize = 32;
535
536/// Parses the blocks of a container's derived content.
537/// The column `text` reaches from column `column`, with tab stops every four.
538fn advance_columns(column: usize, text: &str) -> usize {
539    text.chars().fold(column, |column, char| {
540        if char == '\t' {
541            column + 4 - column % 4
542        } else {
543            column + 1
544        }
545    })
546}
547
548/// Splits `input` into lines, with positions translated through `map`, the
549/// source map of `input`.
550fn collect_lines<'a>(input: &'a str, map: &'a SourceMap) -> Vec<Line<'a>> {
551    let bytes = input.as_bytes();
552    let mut lines = Vec::new();
553    let mut segments = LineSegments {
554        segments: map.segments(),
555        first: 0,
556    };
557    let mut start = 0;
558    let mut index = 0;
559
560    while index < bytes.len() {
561        let eol_end = match bytes[index] {
562            b'\n' => index + 1,
563            b'\r' if bytes.get(index + 1) == Some(&b'\n') => index + 2,
564            b'\r' => index + 1,
565            _ => {
566                index += 1;
567                continue;
568            }
569        };
570        lines.push(segments.line(input, start, index, eol_end));
571        index = eol_end;
572        start = index;
573    }
574
575    if start < bytes.len() || input.is_empty() {
576        lines.push(segments.line(input, start, bytes.len(), bytes.len()));
577    }
578
579    lines
580}
581
582/// Hands each line of a split string the segments that cover it, moving
583/// forward through the map once.
584struct LineSegments<'a> {
585    segments: &'a [Segment],
586    first: usize,
587}
588
589impl<'a> LineSegments<'a> {
590    fn line(&mut self, input: &'a str, start: usize, end: usize, eol_end: usize) -> Line<'a> {
591        let segments = self.segments;
592        while self.first + 1 < segments.len() && segments[self.first].text_end() <= start {
593            self.first += 1;
594        }
595        let mut last = self.first;
596        while last + 1 < segments.len() && segments[last + 1].text_start() < eol_end {
597            last += 1;
598        }
599        let covering = if segments.is_empty() {
600            segments
601        } else {
602            &segments[self.first..=last]
603        };
604        let source_start = source_map::start_of(covering, start);
605        let source_end = if end == start {
606            source_start
607        } else {
608            source_map::end_of(covering, end)
609        };
610        let source_end_with_eol = if eol_end == end {
611            source_end
612        } else {
613            source_map::end_of(covering, eol_end)
614        };
615        Line {
616            text: &input[start..end],
617            eol: &input[end..eol_end],
618            start: source_start,
619            end: source_end,
620            end_with_eol: source_end_with_eol,
621            segments: covering,
622            text_offset: start,
623            column: 0,
624        }
625    }
626}
627
628pub(crate) fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
629    match line.trim_end_matches([' ', '\t']) {
630        "---" => Some(FrontmatterKind::Yaml),
631        "+++" => Some(FrontmatterKind::Toml),
632        _ => None,
633    }
634}
635
636fn directive_container_opener_prefix(input: &str) -> Option<(usize, &str)> {
637    let fence_len = input
638        .as_bytes()
639        .iter()
640        .take_while(|byte| **byte == b':')
641        .count();
642    if fence_len >= 3 {
643        Some((fence_len, &input[fence_len..]))
644    } else {
645        None
646    }
647}
648
649fn directive_container_closing_fence(input: &str, min_len: usize) -> Option<usize> {
650    let fence_len = input
651        .as_bytes()
652        .iter()
653        .take_while(|byte| **byte == b':')
654        .count();
655    if fence_len >= min_len && is_blank(&input[fence_len..]) {
656        Some(fence_len)
657    } else {
658        None
659    }
660}
661
662/// Length of the leading `$` run if `input` (already indent-stripped) is a valid
663/// math-flow opener: `>=2` dollars, then an info string with no further `$`.
664fn math_block_fence_length(input: &str) -> Option<usize> {
665    let length = input
666        .as_bytes()
667        .iter()
668        .take_while(|byte| **byte == b'$')
669        .count();
670    if length < 2 || input[length..].contains('$') {
671        return None;
672    }
673    Some(length)
674}
675
676/// A math-flow closing line (already indent-stripped) is a run of `>=length`
677/// dollars and nothing else (trailing whitespace aside).
678fn math_block_fence_closes(input: &str, length: usize) -> bool {
679    let count = input
680        .as_bytes()
681        .iter()
682        .take_while(|byte| **byte == b'$')
683        .count();
684    count >= length && is_blank(&input[count..])
685}
686
687fn parse_alert_marker(line: &str) -> Option<(AlertKind, Option<String>)> {
688    let close = line.find(']')?;
689    let marker = line.get(0..close + 1)?;
690    if !marker.starts_with("[!") {
691        return None;
692    }
693    let kind = match &marker[2..close].to_ascii_lowercase()[..] {
694        "note" => AlertKind::Note,
695        "tip" => AlertKind::Tip,
696        "important" => AlertKind::Important,
697        "warning" => AlertKind::Warning,
698        "caution" => AlertKind::Caution,
699        _ => return None,
700    };
701    Some((kind, alert_title(&line[close + 1..])))
702}
703
704/// The title an alert marker line gives with `text` after its `]`: the text
705/// without the spaces and tabs around it, or none when nothing else is left.
706pub(crate) fn alert_title(text: &str) -> Option<String> {
707    let title = text.trim_matches([' ', '\t']);
708    (!title.is_empty()).then(|| title.into())
709}
710
711fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
712    let text = trim_up_to_three_spaces(line.text)?;
713    let mut marker = None;
714    let mut count = 0;
715    for char in text.chars() {
716        if char == ' ' || char == '\t' {
717            continue;
718        }
719        let current = match char {
720            '-' => ThematicBreakMarker::Dash,
721            '*' => ThematicBreakMarker::Asterisk,
722            '_' => ThematicBreakMarker::Underscore,
723            _ => return None,
724        };
725        if marker.is_some_and(|marker| marker != current) {
726            return None;
727        }
728        marker = Some(current);
729        count += 1;
730    }
731    if count >= 3 {
732        Some(Block::ThematicBreak(ThematicBreak {
733            meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
734            marker: marker?,
735        }))
736    } else {
737        None
738    }
739}
740
741/// The label of a definition starting paragraph line `index`, followed by
742/// `:`: the text of the lines up to the one the label closes on, joined by
743/// `\n`, the offset of the label's `]` in it, and that line.
744///
745/// A label may span lines (CommonMark §4.7), up to its length limit. The
746/// walk to its `]` (`find_reference_label_end`) goes on from where the lines
747/// before left it, so that a long label is walked once.
748fn definition_label(lines: &[Line<'_>], index: usize) -> Option<(String, usize, usize)> {
749    let text = trim_ascii_start(lines[index].text);
750    if !text.starts_with('[') {
751        return None;
752    }
753    let mut accumulated = String::from(text);
754    let mut label_end_line = index;
755    let mut cursor = 1;
756    loop {
757        let bytes = accumulated.as_bytes();
758        // The walk has run out of text: it goes on at the `\n` the next line
759        // is joined with. A `\` that ended the text escapes that `\n`, and
760        // both step to the next line's start.
761        if cursor >= bytes.len() {
762            let next = label_end_line + 1;
763            if next >= lines.len() || accumulated.len() > 4 * REFERENCE_LABEL_MAX_CHARS + 2 {
764                return None;
765            }
766            accumulated.push('\n');
767            accumulated.push_str(lines[next].text);
768            label_end_line = next;
769            continue;
770        }
771        match reference_label_step(&accumulated, cursor) {
772            Step::Next(next) => cursor = next,
773            Step::Done(Some(close)) if reference_label_is_within_limit(&accumulated[1..close]) => {
774                // A closed label not followed by `:` is not a definition.
775                if bytes.get(close + 1) != Some(&b':') {
776                    return None;
777                }
778                return Some((accumulated, close, label_end_line));
779            }
780            // An unescaped `[`, or a label over the length limit: no line
781            // after it makes a definition.
782            Step::Done(_) => return None,
783        }
784    }
785}
786
787/// The link reference definition that starts paragraph line `index`, and
788/// the line after it. The lines are a paragraph's, each from where the
789/// containers around it leave it; the definition's span is left to the
790/// caller.
791fn parse_definition(lines: &[Line<'_>], index: usize) -> Option<(Definition, usize)> {
792    let (accumulated, close, label_end_line) = definition_label(lines, index)?;
793    let label = String::from(&accumulated[1..close]);
794    if normalize_label(&label).is_empty() {
795        return None;
796    }
797    let mut source = String::from(&accumulated[close + 2..]);
798    let mut cursor = label_end_line;
799    let mut best_without_title = None;
800    // The char that closes a title the source leaves open: a line without
801    // it cannot change the parse, so the source is parsed again only once a
802    // line holds it, which keeps a long open title linear.
803    let mut open_title: Option<char> = None;
804
805    loop {
806        let parses =
807            open_title.is_none_or(|closer| line_may_close_title(lines[cursor].text, closer));
808        if parses {
809            match parse_definition_destination_title(&source) {
810                Some(resource) => {
811                    if resource.title.is_some() {
812                        return Some((definition_node(&label, resource), cursor + 1));
813                    }
814                    best_without_title = Some((resource, cursor + 1));
815                    let next = cursor + 1;
816                    if next >= lines.len() || !line_can_start_definition_title(lines[next].text) {
817                        break;
818                    }
819                }
820                // A destination, or a title once it has closed, that does
821                // not parse cannot parse with more lines.
822                None if !is_blank(&source) => match open_definition_title(&source) {
823                    Some(closer) if open_title.is_none() => open_title = Some(closer),
824                    _ => break,
825                },
826                None => {}
827            }
828        }
829        let next = cursor + 1;
830        if next >= lines.len() {
831            break;
832        }
833        source.push('\n');
834        source.push_str(lines[next].text);
835        cursor = next;
836    }
837
838    let (resource, next) = best_without_title?;
839    Some((definition_node(&label, resource), next))
840}
841
842fn definition_node(label: &str, resource: ParsedLinkResource) -> Definition {
843    Definition {
844        meta: NodeMeta::default(),
845        label: label.into(),
846        identifier: normalize_label(label),
847        destination: resource.destination,
848        destination_kind: resource.destination_kind,
849        title: resource.title,
850    }
851}
852
853#[derive(Clone, Copy, Eq, PartialEq)]
854enum HtmlContainerTag {
855    Opening,
856    Closing,
857}
858
859#[derive(Clone, Copy)]
860struct HtmlContainerFence {
861    marker: FenceMarker,
862    length: usize,
863}
864
865/// One step of the line walk to an HTML container's closing tag line: a code
866/// fence is stepped over whole, and same-name opening and closing tag lines
867/// nest.
868fn html_container_close_step(lines: &[Line<'_>], cursor: usize, tag: &str) -> BracketStep {
869    let Some(line) = lines.get(cursor) else {
870        return BracketStep::End;
871    };
872    if let Some(fence) = html_container_fence_opens(line) {
873        let after_fence = (cursor + 1..lines.len())
874            .find(|close| html_container_fence_closes(&lines[*close], fence))
875            .map_or(lines.len(), |close| close + 1);
876        return BracketStep::Pass(after_fence);
877    }
878    if parse_html_container_tag_line(*line, tag, HtmlContainerTag::Closing).is_some() {
879        return BracketStep::Close(cursor + 1);
880    }
881    if parse_html_container_opening_line(*line, tag).is_some() {
882        return BracketStep::Open(cursor + 1);
883    }
884    BracketStep::Pass(cursor + 1)
885}
886
887fn parse_html_container_tag_line(
888    line: Line<'_>,
889    tag: &str,
890    kind: HtmlContainerTag,
891) -> Option<HtmlTag> {
892    let (trimmed, indent_bytes) = trim_html_container_line(&line)?;
893    let (end, name) = parse_html_tag(trimmed, 0)?;
894    if !name.eq_ignore_ascii_case(tag) || !is_blank(&trimmed[end..]) {
895        return None;
896    }
897
898    let closing = html_tag_is_closing(trimmed, 0);
899    if (kind == HtmlContainerTag::Opening && closing)
900        || (kind == HtmlContainerTag::Closing && !closing)
901        || html_tag_is_self_closing(&trimmed[..end])
902    {
903        return None;
904    }
905
906    Some(HtmlTag {
907        meta: NodeMeta::new(Some(Span::new(
908            line.source_start(indent_bytes),
909            line.source_end(indent_bytes + end),
910        ))),
911        name: tag.into(),
912        raw: trimmed[..end].into(),
913    })
914}
915
916fn parse_html_container_opening_line(line: Line<'_>, tag: &str) -> Option<HtmlTag> {
917    let (trimmed, indent_bytes) = trim_html_container_line(&line)?;
918    let (end, name) = parse_html_tag(trimmed, 0)?;
919    if !name.eq_ignore_ascii_case(tag)
920        || html_tag_is_closing(trimmed, 0)
921        || html_tag_is_self_closing(&trimmed[..end])
922    {
923        return None;
924    }
925
926    let rest_start = end + leading_ascii_whitespace_len(&trimmed[end..]);
927    if !trimmed[rest_start..].is_empty() && !html_container_line_has_summary(&trimmed[rest_start..])
928    {
929        return None;
930    }
931
932    Some(HtmlTag {
933        meta: NodeMeta::new(Some(Span::new(
934            line.source_start(indent_bytes),
935            line.source_end(indent_bytes + end),
936        ))),
937        name: tag.into(),
938        raw: trimmed[..end].into(),
939    })
940}
941
942fn html_container_line_has_summary(input: &str) -> bool {
943    let Some((open_end, name)) = parse_html_tag(input, 0) else {
944        return false;
945    };
946    if !name.eq_ignore_ascii_case("summary")
947        || html_tag_is_closing(input, 0)
948        || html_tag_is_self_closing(&input[..open_end])
949    {
950        return false;
951    }
952    let Some(close_start) = input[open_end..].find("</").map(|offset| open_end + offset) else {
953        return false;
954    };
955    let Some((close_end, close_name)) = parse_html_tag(input, close_start) else {
956        return false;
957    };
958    close_name.eq_ignore_ascii_case("summary")
959        && html_tag_is_closing(input, close_start)
960        && is_blank(&input[close_end..])
961}
962
963/// `line` from its first char other than a space or tab, when its
964/// indentation, from the column it starts at, is at most three columns.
965fn trim_html_container_line<'a>(line: &Line<'a>) -> Option<(&'a str, usize)> {
966    let (columns, bytes) = leading_indent_at(line.text, line.column);
967    (columns <= 3).then(|| (&line.text[bytes..], bytes))
968}
969
970fn leading_ascii_whitespace_len(input: &str) -> usize {
971    input
972        .as_bytes()
973        .iter()
974        .take_while(|byte| byte.is_ascii_whitespace())
975        .count()
976}
977
978fn html_tag_is_closing(input: &str, index: usize) -> bool {
979    input.as_bytes().get(index + 1) == Some(&b'/')
980}
981
982fn html_tag_is_self_closing(input: &str) -> bool {
983    input.trim_end_matches([' ', '\t']).ends_with("/>")
984}
985
986fn html_container_fence_opens(line: &Line<'_>) -> Option<HtmlContainerFence> {
987    let (trimmed, _) = trim_html_container_line(line)?;
988    let (marker, length) = fence_start(trimmed)?;
989    Some(HtmlContainerFence { marker, length })
990}
991
992fn html_container_fence_closes(line: &Line<'_>, fence: HtmlContainerFence) -> bool {
993    trim_html_container_line(line)
994        .is_some_and(|(trimmed, _)| fence_close(trimmed, fence.marker, fence.length))
995}
996
997fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
998    let trimmed = input.trim_end_matches([' ', '\t']);
999    if !trimmed.starts_with('<') {
1000        return None;
1001    }
1002
1003    if raw_html_tag_start(trimmed) {
1004        return Some(HtmlBlockKind::RawTag);
1005    }
1006    if trimmed.starts_with("<!--") {
1007        return Some(HtmlBlockKind::Until("-->"));
1008    }
1009    if trimmed.starts_with("<?") {
1010        return Some(HtmlBlockKind::Until("?>"));
1011    }
1012    if is_declaration_start(trimmed) {
1013        return Some(HtmlBlockKind::Until(">"));
1014    }
1015    if trimmed.starts_with("<![CDATA[") {
1016        return Some(HtmlBlockKind::Until("]]>"));
1017    }
1018
1019    if html_block_tag_start(trimmed) {
1020        return Some(HtmlBlockKind::BlockTag);
1021    }
1022
1023    let Some((end, _tag_name)) = parse_html_tag(trimmed, 0) else {
1024        return None;
1025    };
1026    if is_blank(&trimmed[end..]) {
1027        Some(HtmlBlockKind::UntilBlank)
1028    } else {
1029        None
1030    }
1031}
1032
1033fn raw_html_tag_start(input: &str) -> bool {
1034    for tag in ["script", "pre", "style", "textarea"] {
1035        if html_raw_open_tag_prefix(input, tag) {
1036            return true;
1037        }
1038    }
1039    false
1040}
1041
1042fn html_raw_open_tag_prefix(input: &str, tag: &str) -> bool {
1043    let Some(rest) = input.strip_prefix('<') else {
1044        return false;
1045    };
1046    if rest.starts_with('/') || rest.len() < tag.len() {
1047        return false;
1048    }
1049    let rest_bytes = rest.as_bytes();
1050    let tag_bytes = tag.as_bytes();
1051    if !rest_bytes
1052        .get(..tag_bytes.len())
1053        .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
1054    {
1055        return false;
1056    }
1057    match rest_bytes.get(tag.len()) {
1058        None => true,
1059        Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
1060        Some(b'/') => {
1061            rest_bytes.get(tag.len() + 1) == Some(&b'>') && rest_bytes.get(tag.len() + 2).is_none()
1062        }
1063        _ => false,
1064    }
1065}
1066
1067fn line_contains_raw_closing_tag(input: &str, tag: &str) -> bool {
1068    let bytes = input.as_bytes();
1069    let tag_bytes = tag.as_bytes();
1070    let mut cursor = 0;
1071
1072    while cursor + 2 + tag_bytes.len() <= bytes.len() {
1073        let tag_start = cursor + 2;
1074        let tag_end = tag_start + tag_bytes.len();
1075        if bytes.get(cursor) == Some(&b'<')
1076            && bytes.get(cursor + 1) == Some(&b'/')
1077            && bytes
1078                .get(tag_start..tag_end)
1079                .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
1080        {
1081            match bytes.get(tag_end) {
1082                Some(b'>') => return true,
1083                Some(byte) if byte.is_ascii_whitespace() => {
1084                    let mut after_space = tag_end;
1085                    while bytes
1086                        .get(after_space)
1087                        .is_some_and(|byte| byte.is_ascii_whitespace())
1088                    {
1089                        after_space += 1;
1090                    }
1091                    if bytes.get(after_space) == Some(&b'>') {
1092                        return true;
1093                    }
1094                }
1095                _ => {}
1096            }
1097        }
1098        cursor += 1;
1099    }
1100
1101    false
1102}
1103
1104fn html_block_tag_start(input: &str) -> bool {
1105    let bytes = input.as_bytes();
1106    if bytes.first() != Some(&b'<') {
1107        return false;
1108    }
1109
1110    let mut cursor = 1;
1111    if bytes.get(cursor) == Some(&b'/') {
1112        cursor += 1;
1113    }
1114
1115    let name_start = cursor;
1116    if !bytes
1117        .get(cursor)
1118        .is_some_and(|byte| byte.is_ascii_alphabetic())
1119    {
1120        return false;
1121    }
1122    cursor += 1;
1123    while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
1124        cursor += 1;
1125    }
1126
1127    let name = &input[name_start..cursor];
1128    if !html_block_tag(name) {
1129        return false;
1130    }
1131
1132    match bytes.get(cursor) {
1133        None | Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
1134        Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => true,
1135        _ => false,
1136    }
1137}
1138
1139fn html_block_tag(tag: &str) -> bool {
1140    matches!(
1141        tag.to_ascii_lowercase().as_str(),
1142        "address"
1143            | "article"
1144            | "aside"
1145            | "base"
1146            | "basefont"
1147            | "blockquote"
1148            | "body"
1149            | "caption"
1150            | "center"
1151            | "col"
1152            | "colgroup"
1153            | "dd"
1154            | "details"
1155            | "dialog"
1156            | "dir"
1157            | "div"
1158            | "dl"
1159            | "dt"
1160            | "fieldset"
1161            | "figcaption"
1162            | "figure"
1163            | "footer"
1164            | "form"
1165            | "frame"
1166            | "frameset"
1167            | "h1"
1168            | "h2"
1169            | "h3"
1170            | "h4"
1171            | "h5"
1172            | "h6"
1173            | "head"
1174            | "header"
1175            | "hr"
1176            | "html"
1177            | "iframe"
1178            | "legend"
1179            | "li"
1180            | "link"
1181            | "main"
1182            | "menu"
1183            | "menuitem"
1184            | "nav"
1185            | "noframes"
1186            | "ol"
1187            | "optgroup"
1188            | "option"
1189            | "p"
1190            | "param"
1191            | "search"
1192            | "section"
1193            | "summary"
1194            | "table"
1195            | "tbody"
1196            | "td"
1197            | "tfoot"
1198            | "th"
1199            | "thead"
1200            | "title"
1201            | "tr"
1202            | "track"
1203            | "ul"
1204    )
1205}
1206
1207fn is_declaration_start(input: &str) -> bool {
1208    input
1209        .as_bytes()
1210        .get(2)
1211        .is_some_and(|byte| input.starts_with("<!") && byte.is_ascii_alphabetic())
1212}
1213
1214fn setext_underline_depth(input: &str) -> Option<u8> {
1215    let underline = trim_up_to_three_spaces(input)?.trim_matches([' ', '\t']);
1216    match underline {
1217        text if !text.is_empty() && text.chars().all(|char| char == '=') => Some(1),
1218        text if !text.is_empty() && text.chars().all(|char| char == '-') => Some(2),
1219        _ => None,
1220    }
1221}
1222
1223/// A delimiter run recorded during the inline scan for later resolution by the
1224/// delimiter-stack algorithm (`process_emphasis`). A closer pairs with the
1225/// nearest compatible opener before it, so same-mark spans nest: `*` and `_`
1226/// (CommonMark), `~~` strikethrough, and `==` highlight.
1227#[derive(Clone, Copy)]
1228struct DelimMarker {
1229    /// Index of the placeholder text node in the flat node list. The text node
1230    /// holds the as-yet-unmatched delimiter characters; matching trims it from
1231    /// the appropriate side and matched characters are removed entirely.
1232    node_index: usize,
1233    marker: u8,
1234    /// Remaining unmatched delimiter characters in this run.
1235    length: usize,
1236    /// The run's length as scanned. CommonMark's rule of three, and the
1237    /// `openers_bottom` key that caches its verdicts, read this rather than
1238    /// what is left after earlier pairings.
1239    run_length: usize,
1240    /// Whether the run can open and close; settled by `assign_emphasis_roles`.
1241    can_open: bool,
1242    can_close: bool,
1243    /// How many more times a long `==` run closes, two characters at a time,
1244    /// after closing with its first two.
1245    recloses: usize,
1246    /// The opener of the latest-starting `==` span enclosing this run (see
1247    /// `assign_emphasis_roles`), or `NIL`.
1248    enclosed_from: usize,
1249    /// Offset of the run in the inline input, for adjacency checks.
1250    position: usize,
1251}
1252
1253/// End-of-list marker for the index-linked lists used by `process_emphasis`.
1254const NIL: usize = usize::MAX;
1255
1256/// The deepest `Emphasis`/`Strong`/`Delete` nesting one inline pass builds. A
1257/// delimiter pair that would nest deeper stays literal text, which bounds the
1258/// tree depth that recursive consumers (text merging, serialization, `Drop`)
1259/// walk.
1260const MAX_EMPHASIS_NESTING: usize = 16;
1261
1262/// How deeply the nodes between two delimiter runs nest: `emphasis` counts
1263/// `Emphasis`/`Strong`/`Delete` levels since the nearest enclosed mark span,
1264/// bracket, or directive,
1265/// and `total` counts every inline container level (emphasis, marks, and
1266/// formed brackets and directives). `total` counts toward `MAX_INLINE_NESTING`
1267/// together with the inline passes and open brackets enclosing these nodes;
1268/// emphasis counts toward `MAX_EMPHASIS_NESTING` within one mark.
1269#[derive(Clone, Copy, Default)]
1270struct Nesting {
1271    emphasis: usize,
1272    total: usize,
1273}
1274
1275impl Nesting {
1276    fn max(self, other: Self) -> Self {
1277        Self {
1278            emphasis: self.emphasis.max(other.emphasis),
1279            total: self.total.max(other.total),
1280        }
1281    }
1282}
1283
1284/// The delimiter stack `process_emphasis` works on: live runs form a doubly
1285/// linked list in source order, and each live run records the deepest nesting
1286/// among the nodes between it and the next live run.
1287struct DelimiterLinks {
1288    prev: Vec<usize>,
1289    next: Vec<usize>,
1290    live: Vec<bool>,
1291    nesting_after: Vec<Nesting>,
1292}
1293
1294impl DelimiterLinks {
1295    fn new(count: usize) -> Self {
1296        Self {
1297            prev: (0..count).map(|index| index.wrapping_sub(1)).collect(),
1298            next: (1..=count)
1299                .map(|index| if index < count { index } else { NIL })
1300                .collect(),
1301            live: alloc::vec![true; count],
1302            nesting_after: alloc::vec![Nesting::default(); count],
1303        }
1304    }
1305
1306    /// Unlinks `index`; the nodes after it now follow the previous live run.
1307    fn unlink(&mut self, index: usize) {
1308        let (before, after) = (self.prev[index], self.next[index]);
1309        self.live[index] = false;
1310        if before != NIL {
1311            self.next[before] = after;
1312            self.nesting_after[before] = self.nesting_after[before].max(self.nesting_after[index]);
1313        }
1314        if after != NIL {
1315            self.prev[after] = before;
1316        }
1317    }
1318
1319    /// Unlinks every run strictly between `opener` and `closer` and returns the
1320    /// deepest nesting among the nodes between them.
1321    fn close_span(&mut self, opener: usize, closer: usize) -> Nesting {
1322        let mut nesting = self.nesting_after[opener];
1323        let mut inner = self.next[opener];
1324        while inner != closer {
1325            nesting = nesting.max(self.nesting_after[inner]);
1326            self.live[inner] = false;
1327            inner = self.next[inner];
1328        }
1329        self.next[opener] = closer;
1330        self.prev[closer] = opener;
1331        nesting
1332    }
1333}
1334
1335/// The flat inline list `process_emphasis` rewrites, held as a doubly linked
1336/// list over stable slots. Wrapping a matched span and dropping a consumed
1337/// delimiter run cost time proportional to the wrapped nodes rather than to the
1338/// whole list, and delimiter `node_index` values never need re-indexing.
1339struct InlineList {
1340    slots: Vec<Option<Inline>>,
1341    prev: Vec<usize>,
1342    next: Vec<usize>,
1343    head: usize,
1344}
1345
1346impl InlineList {
1347    fn new(nodes: Vec<Inline>) -> Self {
1348        let count = nodes.len();
1349        Self {
1350            slots: nodes.into_iter().map(Some).collect(),
1351            prev: (0..count).map(|slot| slot.wrapping_sub(1)).collect(),
1352            next: (1..=count)
1353                .map(|slot| if slot < count { slot } else { NIL })
1354                .collect(),
1355            head: if count == 0 { NIL } else { 0 },
1356        }
1357    }
1358
1359    fn node_mut(&mut self, slot: usize) -> &mut Inline {
1360        self.slots[slot]
1361            .as_mut()
1362            .expect("delimiter placeholder slot is live")
1363    }
1364
1365    /// Replaces every node strictly between `before` and `after` with the node
1366    /// `wrap` builds from them (in order).
1367    fn wrap_between(
1368        &mut self,
1369        before: usize,
1370        after: usize,
1371        wrap: impl FnOnce(Vec<Inline>) -> Inline,
1372    ) {
1373        let mut children = Vec::new();
1374        let mut cursor = self.next[before];
1375        while cursor != after {
1376            children.push(self.slots[cursor].take().expect("linked slot is live"));
1377            cursor = self.next[cursor];
1378        }
1379        let slot = self.slots.len();
1380        self.slots.push(Some(wrap(children)));
1381        self.prev.push(before);
1382        self.next.push(after);
1383        self.next[before] = slot;
1384        self.prev[after] = slot;
1385    }
1386
1387    fn remove(&mut self, slot: usize) {
1388        let (prev, next) = (self.prev[slot], self.next[slot]);
1389        if prev == NIL {
1390            self.head = next;
1391        } else {
1392            self.next[prev] = next;
1393        }
1394        if next != NIL {
1395            self.prev[next] = prev;
1396        }
1397        self.slots[slot] = None;
1398    }
1399
1400    fn into_vec(mut self) -> Vec<Inline> {
1401        let mut nodes = Vec::new();
1402        let mut cursor = self.head;
1403        while cursor != NIL {
1404            nodes.push(self.slots[cursor].take().expect("linked slot is live"));
1405            cursor = self.next[cursor];
1406        }
1407        nodes
1408    }
1409}
1410
1411/// Pushes a literal text node for `value` starting at `start` of the block-level
1412/// inline input.
1413fn push_text(nodes: &mut Vec<Inline>, start: usize, value: &str) {
1414    nodes.push(Inline::Text(Text {
1415        meta: NodeMeta::new(Some(Span::new(start, start + value.len()))),
1416        value: value.into(),
1417    }));
1418}
1419
1420/// Resolves the `]` at `close` against the innermost open bracket. Returns the
1421/// end of the construct it forms, or `None` when the `]` stays literal text
1422/// (the opener's characters then stay literal too).
1423#[allow(clippy::too_many_arguments)]
1424fn close_bracket(
1425    input: &str,
1426    base_offset: usize,
1427    close: usize,
1428    definitions: Option<Labels<'_>>,
1429    nodes: &mut Vec<Inline>,
1430    delimiters: &mut Vec<DelimMarker>,
1431    brackets: &mut Brackets,
1432    pass: &mut InlinePass,
1433) -> Option<usize> {
1434    let mut opener = brackets.openers.pop()?;
1435    let depth = pass.state.depth + brackets.openers.len();
1436    // The opener's own stack slot decides whether it may form a link; later
1437    // openers pushed into that slot start out able to.
1438    let link_active = brackets.openers.len() >= brackets.link_floor;
1439    brackets.link_floor = brackets.link_floor.min(brackets.openers.len());
1440
1441    if opener.kind == BracketKind::InlineFootnote {
1442        if !input[opener.position + 2..close].trim().is_empty() {
1443            let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1444            nodes.truncate(opener.node_index - 1);
1445            nodes.push(Inline::InlineFootnote(InlineFootnote {
1446                meta: NodeMeta::new(Some(Span::new(
1447                    base_offset + opener.position,
1448                    base_offset + close + 1,
1449                ))),
1450                children,
1451            }));
1452            brackets.formed.push((nodes.len() - 1, marks + 1));
1453            return Some(close + 1);
1454        }
1455        // An empty inline footnote is a literal `^`, then a plain `[`.
1456        opener.kind = BracketKind::Link;
1457        opener.position += 1;
1458    }
1459
1460    if opener.kind == BracketKind::Image {
1461        let target = match_link_target(
1462            &mut pass.scan,
1463            input,
1464            opener.position + 2,
1465            close,
1466            definitions,
1467        );
1468        if let Some((end, target)) = target {
1469            let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1470            nodes.truncate(opener.node_index - 1);
1471            let span = Span::new(base_offset + opener.position, base_offset + end);
1472            nodes.push(link_node(target, true, span, children));
1473            brackets.formed.push((nodes.len() - 1, marks + 1));
1474            return Some(end);
1475        }
1476        // Not an image: the `!` stays text and the `[` may still open a link.
1477        opener.kind = BracketKind::Link;
1478        opener.position += 1;
1479    }
1480
1481    if link_active {
1482        let target = match_link_target(
1483            &mut pass.scan,
1484            input,
1485            opener.position + 1,
1486            close,
1487            definitions,
1488        );
1489        if let Some((end, target)) = target {
1490            let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1491            nodes.truncate(opener.node_index);
1492            let span = Span::new(base_offset + opener.position, base_offset + end);
1493            nodes.push(link_node(target, false, span, children));
1494            brackets.formed.push((nodes.len() - 1, marks + 1));
1495            // Links do not nest: no earlier `[` forms a link around this one.
1496            brackets.close_links();
1497            return Some(end);
1498        }
1499    }
1500
1501    // `[^label]` is a footnote reference when the label is plain: no link or
1502    // image formed inside it.
1503    let holds_formed = brackets
1504        .formed
1505        .last()
1506        .is_some_and(|&(node, _)| node > opener.node_index);
1507    if !holds_formed && input[opener.position + 1..].starts_with('^') {
1508        let label = &input[opener.position + 2..close];
1509        if is_footnote_label(label) {
1510            drop_label(nodes, delimiters, brackets, &opener);
1511            nodes.push(Inline::FootnoteReference(FootnoteReference {
1512                meta: NodeMeta::new(Some(Span::new(
1513                    base_offset + opener.position,
1514                    base_offset + close + 1,
1515                ))),
1516                label: label.into(),
1517                identifier: normalize_label(label),
1518            }));
1519            return Some(close + 1);
1520        }
1521    }
1522    None
1523}
1524
1525/// Drops `opener`'s `[` and everything recorded after it.
1526fn drop_label(
1527    nodes: &mut Vec<Inline>,
1528    delimiters: &mut Vec<DelimMarker>,
1529    brackets: &mut Brackets,
1530    opener: &BracketOpener,
1531) {
1532    nodes.truncate(opener.node_index);
1533    delimiters.truncate(opener.delimiter_bottom);
1534    let kept = brackets
1535        .formed
1536        .partition_point(|&(node, _)| node < opener.node_index);
1537    brackets.formed.truncate(kept);
1538}
1539
1540/// Pushes the `length`-byte run of `marker` at `index` as a literal text node
1541/// plus a delimiter entry, whose roles `assign_emphasis_roles` settles.
1542fn push_delimiter(
1543    nodes: &mut Vec<Inline>,
1544    delimiters: &mut Vec<DelimMarker>,
1545    index: usize,
1546    base_offset: usize,
1547    marker: u8,
1548    length: usize,
1549) {
1550    let node_index = nodes.len();
1551    nodes.push(Inline::Text(Text {
1552        meta: NodeMeta::new(Some(Span::new(
1553            base_offset + index,
1554            base_offset + index + length,
1555        ))),
1556        value: String::from(marker as char).repeat(length),
1557    }));
1558    delimiters.push(DelimMarker {
1559        node_index,
1560        marker,
1561        length,
1562        run_length: length,
1563        can_open: false,
1564        can_close: false,
1565        recloses: 0,
1566        enclosed_from: NIL,
1567        position: index,
1568    });
1569}
1570
1571/// The CommonMark roles of a `*`/`_`/`~~` run.
1572///
1573/// Flanking is computed on the whole run (CommonMark treats left/right-flanking
1574/// as a property of the run, not of an individual delimiter), including the
1575/// `_` intraword punctuation rules.
1576///
1577/// GFM's cross-marker bonus: a `*` run immediately adjacent to a `~` counts as
1578/// openable/closeable even though `~` is a punctuation character (this is what
1579/// makes `a*~b~*c` emphasize). A `_` run or a `~` run gets plain CommonMark
1580/// flanking beside a `~`.
1581///
1582/// A side that touches the boundary of an enclosing mark span (`bounded_before`
1583/// / `bounded_after`) flanks as the end of the span's content.
1584fn emphasis_roles(
1585    input: &str,
1586    index: usize,
1587    length: usize,
1588    marker: u8,
1589    bounded_before: bool,
1590    bounded_after: bool,
1591) -> (bool, bool) {
1592    let flanking = delimiter_flanking_within(input, index, length, bounded_before, bounded_after);
1593    let (mut can_open, mut can_close) = if marker == b'_' {
1594        (
1595            flanking.left
1596                && (!flanking.right || flanking.previous.is_some_and(is_flanking_punctuation)),
1597            flanking.right
1598                && (!flanking.left || flanking.next.is_some_and(is_flanking_punctuation)),
1599        )
1600    } else {
1601        (flanking.left, flanking.right)
1602    };
1603
1604    // GFM: a `*` run touching a `~` strikethrough marker may open/close even
1605    // when ordinary flanking refuses it (the `~` would otherwise be a blocking
1606    // punctuation neighbour).
1607    if marker == b'*' {
1608        if flanking.next == Some('~') {
1609            can_open = true;
1610        }
1611        if flanking.previous == Some('~') {
1612            can_close = true;
1613        }
1614    }
1615    (can_open, can_close)
1616}
1617
1618/// Settles the roles of every recorded run. The `==` runs take theirs first
1619/// and are paired among themselves; each `*`, `_`, and `~~` run then takes its
1620/// roles once the `==` spans are known, flanking as the edge of a span's
1621/// content where it touches the inner side of the span's delimiter.
1622fn assign_emphasis_roles(input: &str, delimiters: &mut [DelimMarker]) {
1623    let mut opens_span = alloc::vec![false; delimiters.len()];
1624    let mut closes_span = alloc::vec![false; delimiters.len()];
1625    let mut open_marks: Vec<usize> = Vec::new();
1626    // The run each span opened by a run closes at (a run opens at most one).
1627    let mut span_close = alloc::vec![NIL; delimiters.len()];
1628    for (index, run) in delimiters.iter_mut().enumerate() {
1629        if run.marker != b'=' {
1630            continue;
1631        }
1632        assign_double_mark_roles(input, run);
1633        let mut closed = 0;
1634        if run.can_close {
1635            while closed <= run.recloses {
1636                let Some(opener) = open_marks.pop() else {
1637                    break;
1638                };
1639                opens_span[opener] = true;
1640                closes_span[index] = true;
1641                span_close[opener] = index;
1642                closed += 1;
1643            }
1644        }
1645        // What is left of the run after closing may still open.
1646        if run.can_open && run.length >= 2 * closed + 2 {
1647            open_marks.push(index);
1648        }
1649    }
1650
1651    for index in 0..delimiters.len() {
1652        let run = delimiters[index];
1653        if run.marker == b'=' {
1654            continue;
1655        }
1656        let bounded_before = index > 0 && {
1657            let before = &delimiters[index - 1];
1658            before.position + before.length == run.position && opens_span[index - 1]
1659        };
1660        let bounded_after = index + 1 < delimiters.len() && {
1661            let after = &delimiters[index + 1];
1662            after.position == run.position + run.length && closes_span[index + 1]
1663        };
1664        let (can_open, can_close) = emphasis_roles(
1665            input,
1666            run.position,
1667            run.length,
1668            run.marker,
1669            bounded_before,
1670            bounded_after,
1671        );
1672        delimiters[index].can_open = can_open;
1673        delimiters[index].can_close = can_close;
1674    }
1675
1676    // Each run's latest-starting enclosing mark span. Spans are pushed in
1677    // opener order, so the top of `open_spans` is the latest-starting one;
1678    // spans that have ended are dropped when they reach the top.
1679    let mut open_spans: Vec<usize> = Vec::new();
1680    for (index, run) in delimiters.iter_mut().enumerate() {
1681        while open_spans
1682            .last()
1683            .is_some_and(|&opener| span_close[opener] <= index)
1684        {
1685            open_spans.pop();
1686        }
1687        if let Some(&opener) = open_spans.last() {
1688            run.enclosed_from = opener;
1689        }
1690        if span_close[index] != NIL {
1691            open_spans.push(index);
1692        }
1693    }
1694}
1695
1696/// Sets the roles of a `==` run: it opens with its last two characters and
1697/// closes with its first two (and then the next two), each subject to
1698/// CommonMark flanking of that two-character delimiter. A run right after an
1699/// escaped character of the same mark never closes.
1700fn assign_double_mark_roles(input: &str, run: &mut DelimMarker) {
1701    let (index, length) = (run.position, run.length);
1702    let after_escaped_mark =
1703        index > 0 && input.as_bytes()[index - 1] == run.marker && is_escaped_at(input, index - 1);
1704    run.can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
1705    run.recloses = if run.can_close {
1706        (1..length / 2)
1707            .take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
1708            .count()
1709    } else {
1710        0
1711    };
1712    run.can_open = can_open_delimited(input, index + length - 2, 2);
1713}
1714
1715/// Resolves recorded delimiter runs into emphasis and mark nodes using the
1716/// delimiter-stack algorithm, leaving unmatched runs as text. Closers are taken
1717/// in source order; when a pair closes, runs strictly between its opener and
1718/// closer can no longer pair across it.
1719///
1720/// `formed` lists the link, image, footnote, and directive nodes already built
1721/// among `nodes`, with their nesting depth, so spans around them count it.
1722/// Returns the nodes and the deepest nesting among them.
1723///
1724/// `depth` counts the nesting levels enclosing these runs: the inline passes
1725/// around this one, plus the open brackets around a bracket label.
1726fn process_emphasis(
1727    nodes: Vec<Inline>,
1728    mut delimiters: Vec<DelimMarker>,
1729    depth: usize,
1730    formed: &[(usize, usize)],
1731) -> (Vec<Inline>, usize) {
1732    let mut deepest = formed.iter().map(|&(_, depth)| depth).max().unwrap_or(0);
1733    if delimiters.is_empty() {
1734        let mut nodes = nodes;
1735        merge_adjacent_text(&mut nodes);
1736        return (nodes, deepest);
1737    }
1738    let mut nodes = InlineList::new(nodes);
1739
1740    // A run that is consumed, demoted to plain text, or enclosed by a newly
1741    // closed span is unlinked, so the opener walk below only ever visits live
1742    // runs.
1743    let mut links = DelimiterLinks::new(delimiters.len());
1744    for &(node, depth) in formed {
1745        let before = delimiters.partition_point(|run| run.node_index < node);
1746        if before > 0 {
1747            let gap = &mut links.nesting_after[before - 1].total;
1748            *gap = (*gap).max(depth);
1749        }
1750    }
1751
1752    // `openers_bottom` records, per (marker, opener-can-also-close, length % 3),
1753    // the lowest opener index a nested closer is allowed to reach. Closers below
1754    // this bound for their key have already been proven to have no compatible
1755    // opener.
1756    let mut openers_bottom: [Option<usize>; NESTED_MARKERS * 6] = [None; NESTED_MARKERS * 6];
1757    let mut span_bottom: Vec<Vec<(usize, usize)>> = Vec::new();
1758    let mut closer_idx = 0;
1759
1760    // Every run at or after `closer_idx` is still linked: runs are only ever
1761    // unlinked at or before the current closer.
1762    while closer_idx < delimiters.len() {
1763        let closer = delimiters[closer_idx];
1764
1765        let opener = if closer.can_close {
1766            nested_opener(
1767                &delimiters,
1768                &links,
1769                &openers_bottom,
1770                &span_bottom,
1771                closer_idx,
1772            )
1773        } else {
1774            None
1775        };
1776
1777        let Some(opener_idx) = opener else {
1778            if closer.can_close {
1779                // No opener found: remember how far we searched so future
1780                // closers of the same key skip the same dead range.
1781                let key = openers_bottom_key(&closer);
1782                match span_floor(&closer) {
1783                    Some(span) => {
1784                        if span_bottom.len() <= span {
1785                            span_bottom.resize_with(span + 1, Vec::new);
1786                        }
1787                        match span_bottom[span].iter_mut().find(|(k, _)| *k == key) {
1788                            Some(bound) => bound.1 = closer_idx,
1789                            None => span_bottom[span].push((key, closer_idx)),
1790                        }
1791                    }
1792                    None => openers_bottom[key] = Some(closer_idx),
1793                }
1794            }
1795            if !closer.can_open {
1796                links.unlink(closer_idx);
1797            }
1798            closer_idx += 1;
1799            continue;
1800        };
1801
1802        let (used, wrap) = pair_shape(&delimiters[opener_idx], &delimiters[closer_idx]);
1803
1804        // Drop delimiters strictly between the opener and closer: they could not
1805        // match outward across this newly closed span.
1806        let inner = links.close_span(opener_idx, closer_idx);
1807        let fits = depth + inner.total < MAX_INLINE_NESTING
1808            && (wrap.is_mark() || inner.emphasis < MAX_EMPHASIS_NESTING);
1809        if fits {
1810            apply_emphasis(
1811                &mut nodes,
1812                &mut delimiters,
1813                opener_idx,
1814                closer_idx,
1815                used,
1816                wrap,
1817            );
1818            links.nesting_after[opener_idx] = Nesting {
1819                emphasis: if wrap.is_mark() {
1820                    0
1821                } else {
1822                    inner.emphasis + 1
1823                },
1824                total: inner.total + 1,
1825            };
1826            deepest = deepest.max(inner.total + 1);
1827        } else {
1828            // Too deep to wrap: both runs stay whole in their text nodes as
1829            // literal text and pair no further. Any span enclosing this one is
1830            // at least as deep, so it stays literal too.
1831            delimiters[opener_idx].length = 0;
1832            delimiters[closer_idx].length = 0;
1833            links.nesting_after[opener_idx] = inner;
1834        }
1835        if closer.marker == b'=' {
1836            // `==` runs open only with their last two characters, and a long
1837            // run closes a second time only with its next two characters.
1838            delimiters[opener_idx].can_open = false;
1839            let run = &mut delimiters[closer_idx];
1840            run.can_close = run.recloses > 0;
1841            run.recloses = run.recloses.saturating_sub(1);
1842        }
1843
1844        if delimiters[opener_idx].length == 0 {
1845            links.unlink(opener_idx);
1846        }
1847        if delimiters[closer_idx].length == 0 {
1848            links.unlink(closer_idx);
1849            closer_idx += 1;
1850        }
1851        // When a closer still has delimiters left it stays the active closer
1852        // so the leftover can match an earlier opener (e.g. `***foo*` keeps
1853        // `**`).
1854    }
1855
1856    // Adjacent text nodes can appear where unmatched delimiter runs ended up
1857    // beside literal text (`**foo*bar*` -> `**foo` + emphasis). CommonMark
1858    // coalesces them as the final step; do the same for the spans we created.
1859    let mut nodes = nodes.into_vec();
1860    merge_adjacent_text(&mut nodes);
1861    (nodes, deepest)
1862}
1863
1864/// The markers that pair through nested roles, in `openers_bottom` order.
1865const NESTED_MARKERS: usize = 4;
1866
1867/// The nearest live opener before `closer_idx` that the nested closer there can
1868/// pair with, searching no lower than its `openers_bottom` bound.
1869///
1870/// A closer that can also open does not close across a `==` span enclosing it:
1871/// it searches no lower than that span's opener, bounded per span by
1872/// `span_bottom` the way `openers_bottom` bounds the whole list.
1873fn nested_opener(
1874    delimiters: &[DelimMarker],
1875    links: &DelimiterLinks,
1876    openers_bottom: &[Option<usize>],
1877    span_bottom: &[Vec<(usize, usize)>],
1878    closer_idx: usize,
1879) -> Option<usize> {
1880    let closer = &delimiters[closer_idx];
1881    let key = openers_bottom_key(closer);
1882    let mut bottom = openers_bottom[key];
1883    if let Some(span) = span_floor(closer) {
1884        let span_floor = span_bottom
1885            .get(span)
1886            .and_then(|bounds| bounds.iter().find(|(k, _)| *k == key))
1887            .map_or(span + 1, |&(_, bound)| bound);
1888        bottom = bottom.max(Some(span_floor));
1889    }
1890    let mut search = links.prev[closer_idx];
1891    while search != NIL {
1892        if bottom.is_some_and(|bottom| search < bottom) {
1893            return None;
1894        }
1895        let candidate = &delimiters[search];
1896        if candidate.marker == closer.marker
1897            && candidate.can_open
1898            && emphasis_delimiters_match(candidate, closer)
1899        {
1900            return Some(search);
1901        }
1902        search = links.prev[search];
1903    }
1904    None
1905}
1906
1907/// The enclosing mark span a nested closer may not close across: only a run
1908/// that can also open is held inside it.
1909fn span_floor(closer: &DelimMarker) -> Option<usize> {
1910    (closer.can_open && closer.enclosed_from != NIL).then_some(closer.enclosed_from)
1911}
1912
1913/// How many characters a pair consumes from each run, and the node it forms.
1914fn pair_shape(opener: &DelimMarker, closer: &DelimMarker) -> (usize, EmphasisWrap) {
1915    match closer.marker {
1916        // Strikethrough consumes the whole `~~` run on each side.
1917        b'~' => (2, EmphasisWrap::Delete),
1918        b'=' => (2, EmphasisWrap::Mark),
1919        marker => {
1920            let delimiter = if marker == b'_' {
1921                EmphasisDelimiter::Underscore
1922            } else {
1923                EmphasisDelimiter::Asterisk
1924            };
1925            if opener.length >= 2 && closer.length >= 2 {
1926                (2, EmphasisWrap::Strong(delimiter))
1927            } else {
1928                (1, EmphasisWrap::Emphasis(delimiter))
1929            }
1930        }
1931    }
1932}
1933
1934/// Merges consecutive `Text` nodes in a list, recursing into the emphasis and
1935/// mark nodes produced at this level. Other containers were already finalized
1936/// by their own `parse_inlines` pass and are left untouched.
1937fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
1938    let mut write = 0;
1939    for read in 0..nodes.len() {
1940        if read != write {
1941            nodes.swap(read, write);
1942        }
1943        if write > 0 {
1944            let (head, tail) = nodes.split_at_mut(write);
1945            if let (Inline::Text(previous), Inline::Text(current)) =
1946                (&mut head[write - 1], &tail[0])
1947            {
1948                previous.value.push_str(&current.value);
1949                if let (Some(previous_span), Some(current_span)) =
1950                    (previous.meta.span.as_mut(), current.meta.span)
1951                {
1952                    previous_span.end = current_span.end;
1953                }
1954                continue;
1955            }
1956        }
1957        write += 1;
1958    }
1959    nodes.truncate(write);
1960
1961    for node in nodes.iter_mut() {
1962        match node {
1963            Inline::Emphasis(node) => merge_adjacent_text(&mut node.children),
1964            Inline::Strong(node) => merge_adjacent_text(&mut node.children),
1965            Inline::Delete(node) => merge_adjacent_text(&mut node.children),
1966            Inline::Mark(node) => merge_adjacent_text(&mut node.children),
1967            _ => {}
1968        }
1969    }
1970}
1971
1972/// Index into `openers_bottom` for a nested closer's (marker, both-flags,
1973/// length % 3) key.
1974fn openers_bottom_key(closer: &DelimMarker) -> usize {
1975    let marker = match closer.marker {
1976        b'_' => 1,
1977        b'~' => 2,
1978        b'=' => 3,
1979        _ => 0,
1980    };
1981    let both = usize::from(closer.can_open && closer.can_close);
1982    // The key holds what `emphasis_delimiters_match` reads of the closer: the
1983    // whole run's length for the rule of three on `*` / `_`, and what is left
1984    // of the run for the other marks.
1985    let length = match closer.marker {
1986        b'*' | b'_' => closer.run_length,
1987        _ => closer.length,
1988    };
1989    let modulo = length % 3;
1990    ((marker * 2) + both) * 3 + modulo
1991}
1992
1993/// Nested opener/closer compatibility, including CommonMark's rule of three.
1994fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
1995    match opener.marker {
1996        // GFM strikethrough: only runs of exactly two `~` are recorded, and a
1997        // pair consumes both whole, so any two live runs match. The rule of
1998        // three does not apply to `~`.
1999        b'~' => true,
2000        // `==` pairs two characters from each run.
2001        b'=' => opener.length >= 2 && closer.length >= 2,
2002        _ => {
2003            // Rule of three: if either delimiter can both open and close, the
2004            // sum of the lengths of the runs containing them must not be a
2005            // multiple of three, unless both are themselves multiples of three.
2006            // CommonMark counts whole runs, not what earlier pairings left.
2007            let opener_both = opener.can_open && opener.can_close;
2008            let closer_both = closer.can_open && closer.can_close;
2009            if opener_both || closer_both {
2010                let (opener_run, closer_run) = (opener.run_length, closer.run_length);
2011                let sum = opener_run + closer_run;
2012                if sum % 3 == 0 && !(opener_run % 3 == 0 && closer_run % 3 == 0) {
2013                    return false;
2014                }
2015            }
2016            true
2017        }
2018    }
2019}
2020
2021/// The node a matched delimiter pair collapses into.
2022#[derive(Clone, Copy)]
2023enum EmphasisWrap {
2024    Emphasis(EmphasisDelimiter),
2025    Strong(EmphasisDelimiter),
2026    Delete,
2027    Mark,
2028}
2029
2030impl EmphasisWrap {
2031    /// Whether the node counts toward `MAX_INLINE_NESTING` rather than
2032    /// `MAX_EMPHASIS_NESTING`.
2033    fn is_mark(self) -> bool {
2034        matches!(self, Self::Mark)
2035    }
2036}
2037
2038/// Wraps the nodes between two delimiter runs into the node `wrap` names and
2039/// consumes `used` characters from each side.
2040fn apply_emphasis(
2041    nodes: &mut InlineList,
2042    delimiters: &mut [DelimMarker],
2043    opener_idx: usize,
2044    closer_idx: usize,
2045    used: usize,
2046    wrap: EmphasisWrap,
2047) {
2048    let opener_node = delimiters[opener_idx].node_index;
2049    let closer_node = delimiters[closer_idx].node_index;
2050
2051    // A span covers exactly its consumed delimiters and what they enclose: the
2052    // last `used` characters left in the opener's text node through the first
2053    // `used` left in the closer's.
2054    let span_start = nodes
2055        .node_mut(opener_node)
2056        .span()
2057        .map(|span| span.end - used);
2058    let span_end = nodes
2059        .node_mut(closer_node)
2060        .span()
2061        .map(|span| span.start + used);
2062    // Trim the consumed characters from the opener's text node (right side) and
2063    // the closer's text node (left side), updating their recorded lengths/spans.
2064    trim_delimiter_text_tail(nodes.node_mut(opener_node), used);
2065    trim_delimiter_text_head(nodes.node_mut(closer_node), used);
2066    delimiters[opener_idx].length -= used;
2067    let closer = &mut delimiters[closer_idx];
2068    closer.length -= used;
2069    closer.position += used;
2070
2071    // The wrapped children are the nodes strictly between the opener and closer
2072    // text nodes.
2073    let meta = NodeMeta::new(
2074        span_start
2075            .zip(span_end)
2076            .map(|(start, end)| Span::new(start, end)),
2077    );
2078    nodes.wrap_between(opener_node, closer_node, |children| match wrap {
2079        EmphasisWrap::Strong(delimiter) => Inline::Strong(Strong {
2080            meta,
2081            delimiter,
2082            children,
2083        }),
2084        EmphasisWrap::Emphasis(delimiter) => Inline::Emphasis(Emphasis {
2085            meta,
2086            delimiter,
2087            children,
2088        }),
2089        EmphasisWrap::Delete => Inline::Delete(Delete { meta, children }),
2090        EmphasisWrap::Mark => Inline::Mark(Mark { meta, children }),
2091    });
2092
2093    // Drop any placeholder text node that has been fully consumed so leftover
2094    // delimiters never survive as literal text.
2095    if delimiters[closer_idx].length == 0 {
2096        nodes.remove(closer_node);
2097    }
2098    if delimiters[opener_idx].length == 0 {
2099        nodes.remove(opener_node);
2100    }
2101}
2102
2103/// What a `[`-family opener can become when its `]` arrives.
2104#[derive(Clone, Copy, PartialEq, Eq)]
2105enum BracketKind {
2106    Link,
2107    Image,
2108    InlineFootnote,
2109}
2110
2111/// A `[`, `![`, or `^[` waiting for its `]`. Its `[` is a placeholder text
2112/// node; an image's `!` and an inline footnote's `^` sit in the node before it.
2113struct BracketOpener {
2114    kind: BracketKind,
2115    /// The placeholder text node holding the `[`.
2116    node_index: usize,
2117    /// Offset of the opener's first character (`!`, `^`, or `[`) in the input.
2118    position: usize,
2119    /// `delimiters.len()` when the opener was pushed: later runs are in its
2120    /// label.
2121    delimiter_bottom: usize,
2122}
2123
2124/// What a `]` resolves a link-kind opener to.
2125enum LinkTarget {
2126    Resource(ParsedLinkResource),
2127    Reference {
2128        identifier: String,
2129        kind: ReferenceKind,
2130    },
2131}
2132
2133/// Matches what follows the `]` at `close` of a link or image whose label is
2134/// `input[label_start..close]`: an inline `(…)` resource, a full or collapsed
2135/// reference, or a shortcut reference to a defined label. A `(…)` that is not a
2136/// valid resource leaves the reference forms to try, for images and links
2137/// alike. Returns the end of the construct and its target.
2138fn match_link_target(
2139    scan: &mut InlineScan,
2140    input: &str,
2141    label_start: usize,
2142    close: usize,
2143    definitions: Option<Labels<'_>>,
2144) -> Option<(usize, LinkTarget)> {
2145    let label = &input[label_start..close];
2146    let after = close + 1;
2147    if input.as_bytes().get(after) == Some(&b'(') {
2148        match parse_link_resource(&mut scan.lookups, input, after) {
2149            Some((end, resource, _)) => return Some((end, LinkTarget::Resource(resource))),
2150            // A present-but-invalid `(...)` resource is not an inline link or
2151            // image, but CommonMark still resolves `[label]` as a shortcut
2152            // reference and leaves the invalid `(...)` as literal text (links
2153            // 568).
2154            None => {}
2155        }
2156    }
2157    // A `[` that opens no reference label leaves `[label]` a shortcut
2158    // reference followed by literal text.
2159    let reference_close = if input.as_bytes().get(after) == Some(&b'[') {
2160        scan.reference_label_end(after)
2161    } else {
2162        None
2163    };
2164    if let Some(reference_close) = reference_close {
2165        let reference = &input[after + 1..reference_close];
2166        let identifier = if reference.is_empty() {
2167            label
2168        } else {
2169            reference
2170        };
2171        // A present `[...]` second label that resolves to no definition is
2172        // not a link, and CommonMark does not fall back to treating the first
2173        // label as a shortcut (`[x][ ]`, `[x][undef]` stay literal).
2174        return definition_exists(definitions, identifier).then(|| {
2175            let kind = if reference.is_empty() {
2176                ReferenceKind::Collapsed
2177            } else {
2178                ReferenceKind::Full
2179            };
2180            (
2181                reference_close + 1,
2182                LinkTarget::Reference {
2183                    identifier: identifier.into(),
2184                    kind,
2185                },
2186            )
2187        });
2188    }
2189    definition_exists(definitions, label).then(|| {
2190        (
2191            after,
2192            LinkTarget::Reference {
2193                identifier: label.into(),
2194                kind: ReferenceKind::Shortcut,
2195            },
2196        )
2197    })
2198}
2199
2200/// Builds the link or image node for a matched target over `children`.
2201fn link_node(target: LinkTarget, image: bool, span: Span, mut children: Vec<Inline>) -> Inline {
2202    let meta = NodeMeta::new(Some(span));
2203    if !image {
2204        demote_links(&mut children);
2205    }
2206    match (target, image) {
2207        (LinkTarget::Resource(resource), false) => Inline::Link(Link {
2208            meta,
2209            destination: resource.destination,
2210            destination_kind: resource.destination_kind,
2211            title: resource.title,
2212            children,
2213        }),
2214        (LinkTarget::Resource(resource), true) => Inline::Image(Image {
2215            meta,
2216            destination: resource.destination,
2217            destination_kind: resource.destination_kind,
2218            title: resource.title,
2219            alt: children,
2220        }),
2221        (LinkTarget::Reference { identifier, kind }, false) => {
2222            Inline::LinkReference(LinkReference {
2223                meta,
2224                identifier: normalize_label(&identifier),
2225                label: identifier,
2226                kind,
2227                children,
2228            })
2229        }
2230        (LinkTarget::Reference { identifier, kind }, true) => {
2231            Inline::ImageReference(ImageReference {
2232                meta,
2233                identifier: normalize_label(&identifier),
2234                label: identifier,
2235                kind,
2236                alt: children,
2237            })
2238        }
2239    }
2240}
2241
2242/// An autolink written in `form` as `text`, spanning `span`.
2243fn autolink_node(form: AutolinkForm, span: Span, text: &str) -> Inline {
2244    Inline::Autolink(Autolink {
2245        meta: NodeMeta::new(Some(span)),
2246        form,
2247        text: text.into(),
2248    })
2249}
2250
2251/// Link text holds no links: autolinks inside it, the only links a formed
2252/// link's text can hold, read as their text, an angle-bracket autolink
2253/// without its brackets. Image alt text inside it keeps its own links.
2254fn demote_links(nodes: &mut Vec<Inline>) {
2255    for node in nodes.iter_mut() {
2256        match node {
2257            Inline::Autolink(autolink) => {
2258                *node = Inline::Text(Text {
2259                    meta: autolink.meta.clone(),
2260                    value: core::mem::take(&mut autolink.text),
2261                });
2262            }
2263            Inline::Emphasis(node) => demote_links(&mut node.children),
2264            Inline::Strong(node) => demote_links(&mut node.children),
2265            Inline::Delete(node) => demote_links(&mut node.children),
2266            Inline::Mark(node) => demote_links(&mut node.children),
2267            Inline::InlineFootnote(node) => demote_links(&mut node.children),
2268            Inline::TextDirective(node) => demote_links(&mut node.label),
2269            Inline::Image(node) => demote_links(&mut node.alt),
2270            Inline::ImageReference(node) => demote_links(&mut node.alt),
2271            _ => {}
2272        }
2273    }
2274    merge_adjacent_text(nodes);
2275}
2276
2277/// The open brackets of one inline pass and the runs and nodes they enclose.
2278struct Brackets {
2279    openers: Vec<BracketOpener>,
2280    /// Openers below this stack index can no longer form links: a link formed
2281    /// after them.
2282    link_floor: usize,
2283    /// How many `[` were left as text for want of nesting room and are still
2284    /// open: the next that many `]` close them, as text.
2285    overflow: usize,
2286    /// Formed link, image, footnote, and directive nodes with their nesting
2287    /// depth, by node index, so spans around them count it.
2288    formed: Vec<(usize, usize)>,
2289}
2290
2291impl Brackets {
2292    fn new() -> Self {
2293        Self {
2294            openers: Vec::new(),
2295            link_floor: 0,
2296            overflow: 0,
2297            formed: Vec::new(),
2298        }
2299    }
2300
2301    /// Whether a `[` is open: pushed and not yet closed by a `]`, as a link or
2302    /// image opener or as text left for want of nesting room. An inline
2303    /// footnote's `^[` opens no link text. The stack is at most the nesting
2304    /// limit deep.
2305    fn open(&self) -> bool {
2306        self.overflow > 0
2307            || self
2308                .openers
2309                .iter()
2310                .any(|opener| !matches!(opener.kind, BracketKind::InlineFootnote))
2311    }
2312
2313    /// Keeps every open `[` from forming a link around what follows.
2314    fn close_links(&mut self) {
2315        self.link_floor = self.openers.len();
2316    }
2317}
2318
2319/// Takes the nodes and runs after `opener` out of the pass, pairs the runs
2320/// among themselves, and returns the resolved label content with its nesting
2321/// depth.
2322fn take_label(
2323    input: &str,
2324    nodes: &mut Vec<Inline>,
2325    delimiters: &mut Vec<DelimMarker>,
2326    brackets: &mut Brackets,
2327    opener: &BracketOpener,
2328    depth: usize,
2329) -> (Vec<Inline>, usize) {
2330    let first = opener.node_index + 1;
2331    let children: Vec<Inline> = nodes.drain(first..).collect();
2332    let mut runs = delimiters.split_off(opener.delimiter_bottom);
2333    for run in &mut runs {
2334        run.node_index -= first;
2335    }
2336    let split = brackets.formed.partition_point(|&(node, _)| node < first);
2337    let formed: Vec<(usize, usize)> = brackets
2338        .formed
2339        .split_off(split)
2340        .into_iter()
2341        .map(|(node, marks)| (node - first, marks))
2342        .collect();
2343    assign_emphasis_roles(input, &mut runs);
2344    process_emphasis(children, runs, depth, &formed)
2345}
2346
2347/// Removes `count` trailing delimiter characters from a placeholder text node.
2348fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
2349    if let Inline::Text(text) = node {
2350        let new_len = text.value.len().saturating_sub(count);
2351        text.value.truncate(new_len);
2352        if let Some(span) = text.meta.span.as_mut() {
2353            span.end = span.end.saturating_sub(count);
2354        }
2355    }
2356}
2357
2358/// Removes `count` leading delimiter characters from a placeholder text node.
2359/// The node holds one repeated marker byte, so dropping from the front leaves
2360/// the same text as truncating the back.
2361fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
2362    if let Inline::Text(text) = node {
2363        let count = count.min(text.value.len());
2364        text.value.truncate(text.value.len() - count);
2365        if let Some(span) = text.meta.span.as_mut() {
2366            span.start += count;
2367        }
2368    }
2369}
2370
2371/// Forward lookups the inline scanners ask of one input. `DirectLookups`
2372/// answers each with a fresh scan, for callers that ask once; `InlineLookups`
2373/// memoizes the answers for the many questions one inline pass asks. Both
2374/// answer identically.
2375trait Lookups {
2376    /// The first occurrence of `pattern` that starts at or after `from`.
2377    fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize>;
2378    /// The first run of exactly `len` backticks at or after `start`, which is
2379    /// not inside a backtick run.
2380    fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize>;
2381    /// The first unescaped `<` or `>`, or line break, at or after `from`.
2382    fn angle_destination_stop(&mut self, from: usize) -> Option<usize>;
2383    /// The first `$` not right after a `\` at or after `from`.
2384    fn unescaped_dollar(&mut self, from: usize) -> Option<usize>;
2385}
2386
2387struct DirectLookups<'a> {
2388    input: &'a str,
2389}
2390
2391impl Lookups for DirectLookups<'_> {
2392    fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2393        self.input[from..].find(pattern).map(|offset| from + offset)
2394    }
2395
2396    fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2397        find_code_span_close(self.input, start, len)
2398    }
2399
2400    fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2401        (from..self.input.len()).find(|index| is_angle_destination_stop(self.input, *index))
2402    }
2403
2404    fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2405        (from..self.input.len()).find(|index| is_unescaped_dollar(self.input, *index))
2406    }
2407}
2408
2409/// The patterns `InlineLookups::find` keeps position tables for.
2410const CACHED_PATTERNS: [&str; 8] = [">", "-->", "?>", "]]>", "\"", "'", "`$", "$$"];
2411
2412/// `Lookups` over one inline input, each answered from a table built on first
2413/// use.
2414struct InlineLookups<'a> {
2415    input: &'a str,
2416    patterns: [Positions; CACHED_PATTERNS.len()],
2417    backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
2418    angle_destination_stops: Positions,
2419    unescaped_dollars: Positions,
2420}
2421
2422impl<'a> InlineLookups<'a> {
2423    fn new(input: &'a str) -> Self {
2424        Self {
2425            input,
2426            patterns: Default::default(),
2427            backtick_runs: None,
2428            angle_destination_stops: Positions::default(),
2429            unescaped_dollars: Positions::default(),
2430        }
2431    }
2432}
2433
2434impl Lookups for InlineLookups<'_> {
2435    fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2436        let input = self.input;
2437        let Some(slot) = CACHED_PATTERNS.iter().position(|cached| *cached == pattern) else {
2438            return DirectLookups { input }.find(pattern, from);
2439        };
2440        self.patterns[slot].first_at_or_after(from, || pattern_starts(input, pattern))
2441    }
2442
2443    fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2444        let input = self.input;
2445        let runs = self
2446            .backtick_runs
2447            .get_or_insert_with(|| backtick_runs_by_length(input));
2448        let starts = runs.get(&len)?;
2449        starts
2450            .get(starts.partition_point(|run| *run < start))
2451            .copied()
2452    }
2453
2454    fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2455        let input = self.input;
2456        self.angle_destination_stops.first_at_or_after(from, || {
2457            (0..input.len())
2458                .filter(|index| is_angle_destination_stop(input, *index))
2459                .collect()
2460        })
2461    }
2462
2463    fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2464        let input = self.input;
2465        self.unescaped_dollars.first_at_or_after(from, || {
2466            (0..input.len())
2467                .filter(|index| is_unescaped_dollar(input, *index))
2468                .collect()
2469        })
2470    }
2471}
2472
2473/// Start offsets of every maximal backtick run, grouped by run length.
2474fn backtick_runs_by_length(input: &str) -> BTreeMap<usize, Vec<usize>> {
2475    let mut runs: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
2476    let mut index = 0;
2477    while index < input.len() {
2478        if input.as_bytes()[index] == b'`' {
2479            let len = backtick_run_len(input, index);
2480            runs.entry(len).or_default().push(index);
2481            index += len;
2482        } else {
2483            index += 1;
2484        }
2485    }
2486    runs
2487}
2488
2489fn backtick_run_len(input: &str, index: usize) -> usize {
2490    input.as_bytes()[index..]
2491        .iter()
2492        .take_while(|byte| **byte == b'`')
2493        .count()
2494}
2495
2496/// A `$` that single-dollar math reads as a delimiter: one right after a `\`
2497/// is content, whether or not that `\` is itself escaped.
2498fn is_unescaped_dollar(input: &str, index: usize) -> bool {
2499    let bytes = input.as_bytes();
2500    bytes[index] == b'$' && (index == 0 || bytes[index - 1] != b'\\')
2501}
2502
2503fn is_angle_destination_stop(input: &str, index: usize) -> bool {
2504    match input.as_bytes()[index] {
2505        b'<' | b'>' => !is_escaped_at(input, index),
2506        b'\n' | b'\r' => true,
2507        _ => false,
2508    }
2509}
2510
2511/// Everything one `parse_inline_content` pass memoizes about its input, so its
2512/// questions about closing delimiters cost amortized linear time in total.
2513struct InlineScan<'a> {
2514    input: &'a str,
2515    lookups: InlineLookups<'a>,
2516    label_ends: BracketMemo,
2517    reference_label_ends: PathMemo,
2518    wikilink_closes: PathMemo,
2519    directive_attribute_closes: PathMemo,
2520    literal_autolinks: LiteralAutolinkScan,
2521}
2522
2523impl<'a> InlineScan<'a> {
2524    fn new(input: &'a str) -> Self {
2525        Self {
2526            input,
2527            lookups: InlineLookups::new(input),
2528            label_ends: BracketMemo::default(),
2529            reference_label_ends: PathMemo::default(),
2530            wikilink_closes: PathMemo::default(),
2531            directive_attribute_closes: PathMemo::default(),
2532            literal_autolinks: LiteralAutolinkScan::default(),
2533        }
2534    }
2535
2536    /// `find_link_label_end`, memoized.
2537    fn link_label_end(&mut self, open: usize) -> Option<usize> {
2538        let input = self.input;
2539        if input.as_bytes().get(open) != Some(&b'[') {
2540            return None;
2541        }
2542        let lookups = &mut self.lookups;
2543        self.label_ends
2544            .resolve(input.len() + 1, open + 1, |cursor| {
2545                link_label_step(lookups, input, cursor)
2546            })
2547    }
2548
2549    /// `find_reference_label_end`, memoized.
2550    fn reference_label_end(&mut self, open: usize) -> Option<usize> {
2551        let input = self.input;
2552        if input.as_bytes().get(open) != Some(&b'[') {
2553            return None;
2554        }
2555        let close = self
2556            .reference_label_ends
2557            .resolve(input.len() + 1, open + 1, |cursor| {
2558                reference_label_step(input, cursor)
2559            })?;
2560        reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
2561    }
2562
2563    /// The `]]` closing the wikilink whose content starts at `start`; it must
2564    /// be on the same line.
2565    fn wikilink_close(&mut self, start: usize) -> Option<usize> {
2566        let input = self.input;
2567        self.wikilink_closes
2568            .resolve(input.len() + 1, start, |cursor| {
2569                wikilink_close_step(input, cursor)
2570            })
2571    }
2572
2573    /// `find_directive_attributes_close`, memoized.
2574    fn directive_attributes_close(&mut self, open: usize) -> Option<usize> {
2575        let input = self.input;
2576        if input.as_bytes().get(open) != Some(&b'{') {
2577            return None;
2578        }
2579        self.directive_attribute_closes.resolve(
2580            (input.len() + 1) * DIRECTIVE_QUOTE_STATES,
2581            (open + 1) * DIRECTIVE_QUOTE_STATES,
2582            |node| directive_attributes_step(input, node),
2583        )
2584    }
2585}
2586
2587/// Parses the inline content `input`, whose source map is `map`. The inline
2588/// parser works in `input`'s coordinates; the spans it produces, on nodes and
2589/// on the diagnostics it pushes, are translated to the input afterwards.
2590fn parse_inlines(
2591    input: &str,
2592    map: &SourceMap,
2593    definitions: Option<Labels<'_>>,
2594    diagnostics: &mut Vec<Diagnostic>,
2595    tokens: Option<&mut Vec<InlineToken>>,
2596) -> Vec<Inline> {
2597    // Definitions are collected from the block structure alone.
2598    if definitions.is_none() {
2599        return Vec::new();
2600    }
2601    let first_diagnostic = diagnostics.len();
2602    let mut nodes = parse_inlines_with_context(
2603        input,
2604        0,
2605        definitions,
2606        diagnostics,
2607        &mut InlineState::default(),
2608    );
2609    tags::split_tags(input, &mut nodes);
2610    if let Some(tokens) = tokens {
2611        *tokens = tokens::tokens(input, &nodes);
2612        source_map::translate_tokens(map, tokens);
2613    }
2614    source_map::translate_inlines(map, &mut nodes, &mut diagnostics[first_diagnostic..]);
2615    nodes
2616}
2617
2618/// The deepest inline nesting the parser builds. Directive labels parse their
2619/// content one inline pass deeper; within a pass, open brackets and every
2620/// emphasis or mark span each add a level. Content past this depth stays
2621/// literal text, so recursion and the native stack it uses stay bounded.
2622const MAX_INLINE_NESTING: usize = 32;
2623
2624/// State shared by every inline parse nested under one block-level inline
2625/// parse. All nested inputs are slices of that one input, addressed by their
2626/// start offset in it; `parse_inlines` maps the result to the original input.
2627#[derive(Default)]
2628struct InlineState {
2629    /// How many inline parses enclose the current one.
2630    depth: usize,
2631}
2632
2633/// What one inline pass threads through its construct parsers: the state it
2634/// shares with enclosing passes and the memoized scans of its own input.
2635struct InlinePass<'p, 'a> {
2636    state: &'p mut InlineState,
2637    scan: InlineScan<'a>,
2638}
2639
2640fn parse_inlines_with_context(
2641    input: &str,
2642    base_offset: usize,
2643    definitions: Option<Labels<'_>>,
2644    diagnostics: &mut Vec<Diagnostic>,
2645    state: &mut InlineState,
2646) -> Vec<Inline> {
2647    if state.depth >= MAX_INLINE_NESTING {
2648        if input.is_empty() {
2649            return Vec::new();
2650        }
2651        return vec![Inline::Text(Text {
2652            meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + input.len()))),
2653            value: input.into(),
2654        })];
2655    }
2656    state.depth += 1;
2657    let nodes = parse_inline_content(input, base_offset, definitions, diagnostics, state);
2658    state.depth -= 1;
2659    nodes
2660}
2661
2662fn parse_inline_content(
2663    input: &str,
2664    base_offset: usize,
2665    definitions: Option<Labels<'_>>,
2666    diagnostics: &mut Vec<Diagnostic>,
2667    state: &mut InlineState,
2668) -> Vec<Inline> {
2669    let bytes = input.as_bytes();
2670    let mut nodes = Vec::new();
2671    let mut text_start = 0;
2672    let mut text = String::new();
2673    let mut index = 0;
2674    // Emphasis and mark delimiters are resolved with a delimiter stack after the
2675    // scan completes. During the scan we emit each candidate delimiter run as a
2676    // literal text node and record it here so `process_emphasis` can rewrite the
2677    // flat node list into emphasis and mark nodes (or leave it as text).
2678    let mut delimiters: Vec<DelimMarker> = Vec::new();
2679    let mut brackets = Brackets::new();
2680    let mut pass = InlinePass {
2681        state,
2682        scan: InlineScan::new(input),
2683    };
2684    // A literal autolink holds `://`, `www.`, or an email's `@`; content
2685    // without any of them is not scanned for one at each position.
2686    let literal_autolinks = bytes.contains(&b'@')
2687        || input.contains("://")
2688        || bytes
2689            .windows(4)
2690            .any(|window| window.eq_ignore_ascii_case(b"www."));
2691
2692    while index < bytes.len() {
2693        if bytes[index] == b'\\' {
2694            if let Some((next_index, char)) = next_char(input, index + 1) {
2695                if char.is_ascii_punctuation() {
2696                    flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2697                    nodes.push(Inline::Escape(Escape {
2698                        meta: NodeMeta::new(Some(Span::new(
2699                            base_offset + index,
2700                            base_offset + next_index,
2701                        ))),
2702                        value: char,
2703                    }));
2704                    index = next_index;
2705                    text_start = index;
2706                    continue;
2707                }
2708            }
2709        }
2710
2711        if bytes[index] == b'&' {
2712            if let Some((end, _)) = parse_character_reference(input, index) {
2713                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2714                nodes.push(Inline::CharacterReference(CharacterReference {
2715                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2716                    reference: input[index..end].into(),
2717                }));
2718                index = end;
2719                text_start = index;
2720                continue;
2721            }
2722        }
2723
2724        if bytes[index] == b'\n' {
2725            // Only a literal backslash, not one an escape or a reference wrote,
2726            // makes the line ending a hard break.
2727            if index > 0
2728                && bytes[index - 1] == b'\\'
2729                && !is_escaped_at(input, index - 1)
2730                && text.ends_with('\\')
2731            {
2732                text.pop();
2733                flush_text(
2734                    &mut nodes,
2735                    &mut text,
2736                    text_start,
2737                    base_offset + index.saturating_sub(1),
2738                );
2739                nodes.push(Inline::LineBreak(LineBreak {
2740                    meta: NodeMeta::new(Some(Span::new(
2741                        base_offset + index.saturating_sub(1),
2742                        base_offset + index + 1,
2743                    ))),
2744                    kind: LineBreakKind::Backslash,
2745                }));
2746                index += 1;
2747                text_start = index;
2748                continue;
2749            }
2750            // The spaces and tabs ending the line: a hard break or stripped.
2751            let trailing_spaces = trailing_space_count(&text);
2752            if is_hard_break_suffix(&text, trailing_spaces) {
2753                text.truncate(text.len() - trailing_spaces);
2754                flush_text(
2755                    &mut nodes,
2756                    &mut text,
2757                    text_start,
2758                    base_offset + index.saturating_sub(trailing_spaces),
2759                );
2760                nodes.push(Inline::LineBreak(LineBreak {
2761                    meta: NodeMeta::new(Some(Span::new(
2762                        base_offset + index.saturating_sub(trailing_spaces),
2763                        base_offset + index + 1,
2764                    ))),
2765                    kind: LineBreakKind::Spaces,
2766                }));
2767                index += 1;
2768                text_start = index;
2769                continue;
2770            }
2771            // Stripped spaces and tabs that no text holds, as after another
2772            // inline, are read as part of the soft break.
2773            let stripped_alone = trailing_spaces > 0 && text.len() == trailing_spaces;
2774            if trailing_spaces > 0 {
2775                text.truncate(text.len() - trailing_spaces);
2776            }
2777            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2778            let start = if stripped_alone {
2779                index - trailing_spaces
2780            } else {
2781                index
2782            };
2783            nodes.push(Inline::SoftBreak(SoftBreak {
2784                meta: NodeMeta::new(Some(Span::new(
2785                    base_offset + start,
2786                    base_offset + index + 1,
2787                ))),
2788            }));
2789            index += 1;
2790            text_start = index;
2791            continue;
2792        }
2793
2794        if bytes[index] == b'`' {
2795            if let Some((end, value)) = parse_code_span_with(&mut pass.scan.lookups, input, index) {
2796                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2797                nodes.push(Inline::Code(CodeInline {
2798                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2799                    value,
2800                }));
2801                index = end;
2802                text_start = index;
2803                continue;
2804            } else {
2805                // No matching-length close for this opening backtick run:
2806                // CommonMark renders the whole run as literal text. Consume the
2807                // entire run here so the loop does not advance one byte and retry
2808                // a shorter sub-run that could spuriously match a shorter close
2809                // (```foo`` stayed a phantom 2-backtick code span).
2810                let run = bytes[index..]
2811                    .iter()
2812                    .take_while(|byte| **byte == b'`')
2813                    .count();
2814                if text.is_empty() {
2815                    text_start = base_offset + index;
2816                }
2817                for _ in 0..run {
2818                    text.push('`');
2819                }
2820                index += run;
2821                continue;
2822            }
2823        }
2824
2825        if bytes[index] == b'*' && starts_delimiter_run(input, index, b'*') {
2826            let run_len = delimiter_byte_run_len(input, index, b'*');
2827            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2828            push_delimiter(
2829                &mut nodes,
2830                &mut delimiters,
2831                index,
2832                base_offset,
2833                b'*',
2834                run_len,
2835            );
2836            index += run_len;
2837            text_start = index;
2838            continue;
2839        }
2840
2841        // Core `_` emphasis/strong is resolved by the delimiter stack, just like
2842        // `*`.
2843        if bytes[index] == b'_' && starts_delimiter_run(input, index, b'_') {
2844            // A leading `_` can begin a GFM email local part (`_a@b.c`); try the
2845            // literal autolink before recording the `_` as an emphasis
2846            // delimiter, otherwise the `_` would be consumed and the email would
2847            // wrongly start one char later (where its left boundary fails).
2848            if literal_autolinks {
2849                if let Some((end, _)) = parse_literal_autolink(
2850                    input,
2851                    index,
2852                    &mut pass.scan.literal_autolinks,
2853                    !brackets.open(),
2854                ) {
2855                    flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2856                    nodes.push(autolink_node(
2857                        AutolinkForm::Literal,
2858                        Span::new(base_offset + index, base_offset + end),
2859                        &input[index..end],
2860                    ));
2861                    index = end;
2862                    text_start = index;
2863                    continue;
2864                }
2865            }
2866            let run_len = delimiter_byte_run_len(input, index, b'_');
2867            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2868            push_delimiter(
2869                &mut nodes,
2870                &mut delimiters,
2871                index,
2872                base_offset,
2873                b'_',
2874                run_len,
2875            );
2876            index += run_len;
2877            text_start = index;
2878            continue;
2879        }
2880
2881        if bytes[index] == b'=' {
2882            let run_len = delimiter_byte_run_len(input, index, b'=');
2883            if run_len >= 2 {
2884                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2885                push_delimiter(
2886                    &mut nodes,
2887                    &mut delimiters,
2888                    index,
2889                    base_offset,
2890                    b'=',
2891                    run_len,
2892                );
2893                index += run_len;
2894                text_start = index;
2895                continue;
2896            }
2897        }
2898
2899        let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
2900
2901        // `^[` opens an inline footnote.
2902        if bracket_room && bytes[index] == b'^' && bytes.get(index + 1) == Some(&b'[') {
2903            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2904            push_text(&mut nodes, base_offset + index, "^");
2905            push_text(&mut nodes, base_offset + index + 1, "[");
2906            brackets.openers.push(BracketOpener {
2907                kind: BracketKind::InlineFootnote,
2908                node_index: nodes.len() - 1,
2909                position: index,
2910                delimiter_bottom: delimiters.len(),
2911            });
2912            index += 2;
2913            text_start = index;
2914            continue;
2915        }
2916
2917        // A run of exactly two `~` may open or close GFM strikethrough.
2918        // The run is measured only from its start, so the bytes of a longer
2919        // run, which go on as text one at a time, are not measured again.
2920        if bytes[index] == b'~' && starts_delimiter_run(input, index, b'~') {
2921            let run_len = delimiter_byte_run_len(input, index, b'~');
2922            if run_len == 2 {
2923                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2924                push_delimiter(
2925                    &mut nodes,
2926                    &mut delimiters,
2927                    index,
2928                    base_offset,
2929                    b'~',
2930                    run_len,
2931                );
2932                index += run_len;
2933                text_start = index;
2934                continue;
2935            }
2936        }
2937
2938        if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') {
2939            // A wikilink at the `[` wins over the image, as it wins over a
2940            // link at a lone `[`, and the `!` makes it an embed; it takes no
2941            // bracket nesting, so it forms past the limit too.
2942            if let Some((end, wikilink)) =
2943                parse_wikilink(input, index + 1, base_offset, &mut pass.scan)
2944            {
2945                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2946                nodes.push(embed(wikilink, base_offset + index));
2947                // A wikilink is a link: no open bracket forms a link around it.
2948                brackets.close_links();
2949                index = end;
2950                text_start = index;
2951                continue;
2952            }
2953        }
2954
2955        if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') && bracket_room {
2956            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2957            push_text(&mut nodes, base_offset + index, "!");
2958            push_text(&mut nodes, base_offset + index + 1, "[");
2959            brackets.openers.push(BracketOpener {
2960                kind: BracketKind::Image,
2961                node_index: nodes.len() - 1,
2962                position: index,
2963                delimiter_bottom: delimiters.len(),
2964            });
2965            index += 2;
2966            text_start = index;
2967            continue;
2968        }
2969
2970        if bytes[index] == b'[' {
2971            if let Some((end, wikilink)) = parse_wikilink(input, index, base_offset, &mut pass.scan)
2972            {
2973                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2974                nodes.push(wikilink);
2975                brackets.close_links();
2976                index = end;
2977                text_start = index;
2978                continue;
2979            }
2980            if bracket_room {
2981                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2982                push_text(&mut nodes, base_offset + index, "[");
2983                brackets.openers.push(BracketOpener {
2984                    kind: BracketKind::Link,
2985                    node_index: nodes.len() - 1,
2986                    position: index,
2987                    delimiter_bottom: delimiters.len(),
2988                });
2989                index += 1;
2990                text_start = index;
2991                continue;
2992            }
2993            brackets.overflow += 1;
2994        }
2995
2996        if bytes[index] == b']' && brackets.overflow > 0 {
2997            // Closes a `[` left as text past the nesting limit.
2998            brackets.overflow -= 1;
2999        } else if bytes[index] == b']' && !brackets.openers.is_empty() {
3000            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3001            if let Some(end) = close_bracket(
3002                input,
3003                base_offset,
3004                index,
3005                definitions,
3006                &mut nodes,
3007                &mut delimiters,
3008                &mut brackets,
3009                &mut pass,
3010            ) {
3011                index = end;
3012                text_start = index;
3013                continue;
3014            }
3015        }
3016
3017        if bytes[index] == b'$' {
3018            if let Some((end, value, kind)) =
3019                parse_math_inline(&mut pass.scan.lookups, input, index)
3020            {
3021                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3022                nodes.push(Inline::Math(MathInline {
3023                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3024                    value,
3025                    kind,
3026                }));
3027                index = end;
3028                text_start = index;
3029                continue;
3030            }
3031            // A dollar run that opens but finds no exact-length close is emitted
3032            // as literal text in one piece (like a code-span). Skipping the
3033            // whole run prevents re-opening with a shorter marker inside it, so
3034            // `$$$foo$$` stays literal rather than matching `$$foo$$`. A lone
3035            // `$` before a backtick (the code-math form) is a run of 1, so this
3036            // still advances correctly when that form fails.
3037            let run = bytes[index..]
3038                .iter()
3039                .take_while(|byte| **byte == b'$')
3040                .count();
3041            if run > 1 {
3042                if text.is_empty() {
3043                    text_start = base_offset + index;
3044                }
3045                text.push_str(&input[index..index + run]);
3046                index += run;
3047                continue;
3048            }
3049        }
3050
3051        // Inside an open `[` only an email address links; if the bracket
3052        // forms a link, `demote_links` turns it back into text.
3053        if literal_autolinks {
3054            if let Some((end, _)) = parse_literal_autolink(
3055                input,
3056                index,
3057                &mut pass.scan.literal_autolinks,
3058                !brackets.open(),
3059            ) {
3060                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3061                nodes.push(autolink_node(
3062                    AutolinkForm::Literal,
3063                    Span::new(base_offset + index, base_offset + end),
3064                    &input[index..end],
3065                ));
3066                index = end;
3067                text_start = index;
3068                continue;
3069            }
3070        }
3071
3072        if bytes[index] == b'<' {
3073            if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
3074                let uri = &input[index + 1..end - 1];
3075                if is_angle_autolink(uri) {
3076                    flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3077                    nodes.push(autolink_node(
3078                        AutolinkForm::Angle,
3079                        Span::new(base_offset + index, base_offset + end),
3080                        uri,
3081                    ));
3082                    index = end;
3083                    text_start = index;
3084                    continue;
3085                }
3086            }
3087            if let Some(end) = html_inline_end(&mut pass.scan.lookups, input, index) {
3088                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3089                nodes.push(Inline::Html(HtmlInline {
3090                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3091                    value: input[index..end].into(),
3092                }));
3093                index = end;
3094                text_start = index;
3095                continue;
3096            }
3097        }
3098
3099        if bytes[index] == b':' {
3100            if let Some((end, name)) = parse_shortcode(input, index) {
3101                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3102                nodes.push(Inline::Shortcode(Shortcode {
3103                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3104                    name,
3105                }));
3106                index = end;
3107                text_start = index;
3108                continue;
3109            }
3110        }
3111
3112        // A directive counts one nesting level for itself, on top of the
3113        // enclosing passes and open brackets.
3114        if bytes[index] == b':' && pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING {
3115            // The label parses one pass deeper than the open brackets around
3116            // it.
3117            pass.state.depth += brackets.openers.len();
3118            let directive = parse_text_directive(
3119                input,
3120                index,
3121                base_offset,
3122                definitions,
3123                diagnostics,
3124                &mut pass,
3125            );
3126            pass.state.depth -= brackets.openers.len();
3127            if let Some((end, directive)) = directive {
3128                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3129                if let Inline::TextDirective(node) = &directive {
3130                    // A link inside the directive label keeps open brackets
3131                    // from forming links around it.
3132                    if contains_link_inline(&node.label) {
3133                        brackets.close_links();
3134                    }
3135                    let depth = inline_nesting(&node.label) + 1;
3136                    brackets.formed.push((nodes.len(), depth));
3137                }
3138                nodes.push(directive);
3139                index = end;
3140                text_start = index;
3141                continue;
3142            }
3143        }
3144
3145        let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
3146        if text.is_empty() {
3147            text_start = base_offset + index;
3148        }
3149        text.push(char);
3150        index = next_index;
3151    }
3152
3153    flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
3154    assign_emphasis_roles(input, &mut delimiters);
3155    process_emphasis(nodes, delimiters, pass.state.depth - 1, &brackets.formed).0
3156}
3157
3158/// A `:name:` shortcode at `index`: `name` is in the gemoji table, and the
3159/// source chars directly outside the colons are no Unicode letter or digit,
3160/// so clock times and words joined by a colon stay text.
3161fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
3162    if input[index..].starts_with("::") {
3163        return None;
3164    }
3165    if input[..index]
3166        .chars()
3167        .next_back()
3168        .is_some_and(char::is_alphanumeric)
3169    {
3170        return None;
3171    }
3172
3173    let mut cursor = index + 1;
3174    while let Some((next, char)) = next_char(input, cursor) {
3175        if char == ':' {
3176            let name = &input[index + 1..cursor];
3177            if crate::gemoji::glyph(name).is_none()
3178                || input[next..]
3179                    .chars()
3180                    .next()
3181                    .is_some_and(char::is_alphanumeric)
3182            {
3183                return None;
3184            }
3185            return Some((next, name.into()));
3186        }
3187        if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
3188            return None;
3189        }
3190        cursor = next;
3191    }
3192    None
3193}
3194
3195fn parse_wikilink(
3196    input: &str,
3197    index: usize,
3198    base_offset: usize,
3199    scan: &mut InlineScan,
3200) -> Option<(usize, Inline)> {
3201    if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
3202    {
3203        return None;
3204    }
3205
3206    let close = scan.wikilink_close(index + 2)?;
3207    let source = &input[index + 2..close];
3208    if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
3209        return None;
3210    }
3211
3212    let (target_source, label_source) = match find_wikilink_separator(source) {
3213        Some(separator) => (&source[..separator], &source[separator + 1..]),
3214        None => (source, source),
3215    };
3216
3217    // Target and label keep their escapes and character references as
3218    // written.
3219    if target_source.is_empty() {
3220        return None;
3221    }
3222    let target = String::from(target_source);
3223    let label = String::from(label_source);
3224    let end = close + 2;
3225    Some((
3226        end,
3227        Inline::WikiLink(WikiLink {
3228            meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3229            target,
3230            label,
3231            embed: false,
3232        }),
3233    ))
3234}
3235
3236/// `wikilink` marked as an embed by the `!` at `bang`, where its span starts.
3237fn embed(mut wikilink: Inline, bang: usize) -> Inline {
3238    if let Inline::WikiLink(node) = &mut wikilink {
3239        node.embed = true;
3240        node.meta.span = node.meta.span.map(|span| Span::new(bang, span.end));
3241    }
3242    wikilink
3243}
3244
3245/// One step of the walk to a wikilink's closing `]]`, which must be on the
3246/// opener's line. The content holds no unescaped `[` or `]`.
3247fn wikilink_close_step(input: &str, cursor: usize) -> Step {
3248    let bytes = input.as_bytes();
3249    match bytes.get(cursor) {
3250        None | Some(b'\n' | b'\r' | b'[') => Step::Done(None),
3251        Some(b'\\') => {
3252            let escaped = cursor + 1;
3253            Step::Next(next_char(input, escaped).map_or(escaped, |(after_escape, _)| after_escape))
3254        }
3255        Some(b']') if bytes.get(cursor + 1) == Some(&b']') => Step::Done(Some(cursor)),
3256        Some(b']') => Step::Done(None),
3257        Some(_) => Step::Next(next_char(input, cursor).map_or(input.len(), |(next, _)| next)),
3258    }
3259}
3260
3261fn find_wikilink_separator(input: &str) -> Option<usize> {
3262    let bytes = input.as_bytes();
3263    let mut cursor = 0;
3264    while cursor < input.len() {
3265        match bytes[cursor] {
3266            b'\\' => {
3267                cursor += 1;
3268                if cursor < input.len() {
3269                    cursor = next_char(input, cursor)?.0;
3270                }
3271            }
3272            b'|' => return Some(cursor),
3273            _ => cursor = next_char(input, cursor)?.0,
3274        }
3275    }
3276    None
3277}
3278
3279fn trailing_space_count(input: &str) -> usize {
3280    input
3281        .as_bytes()
3282        .iter()
3283        .rev()
3284        .take_while(|byte| matches!(**byte, b' ' | b'\t'))
3285        .count()
3286}
3287
3288fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
3289    // A hard line break is two or more spaces immediately before the newline
3290    // with no intervening tab; a tab anywhere in the trailing whitespace run
3291    // demotes it to a soft break.
3292    let bytes = input.as_bytes();
3293    trailing >= 2
3294        && bytes[bytes.len() - trailing..]
3295            .iter()
3296            .all(|byte| *byte == b' ')
3297}
3298
3299/// The walk `InlineScan::reference_label_end` and `definition_label` take in
3300/// steps, run once from the start; the scan tests hold them to it.
3301#[cfg(test)]
3302fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
3303    // A reference/definition link label does not nest: it ends at the first
3304    // unescaped `]`, and an unescaped interior `[` disqualifies it.
3305    if input.as_bytes().get(open) != Some(&b'[') {
3306        return None;
3307    }
3308    let close = path_walk(open + 1, |cursor| reference_label_step(input, cursor))?;
3309    reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
3310}
3311
3312/// One step of the walk to a reference label's closing `]`.
3313fn reference_label_step(input: &str, cursor: usize) -> Step {
3314    let Some((next, char)) = next_char(input, cursor) else {
3315        return Step::Done(None);
3316    };
3317    match char {
3318        '\\' => Step::Next(next_char(input, next).map_or(next, |(after_escape, _)| after_escape)),
3319        '[' => Step::Done(None),
3320        ']' => Step::Done(Some(cursor)),
3321        _ => Step::Next(next),
3322    }
3323}
3324
3325/// How many container levels `inlines` nest, counting empty containers.
3326fn inline_nesting(inlines: &[Inline]) -> usize {
3327    let mut deepest = 0;
3328    let mut pending = vec![(inlines, 0)];
3329    while let Some((nodes, depth)) = pending.pop() {
3330        for node in nodes {
3331            if is_inline_container(node) {
3332                deepest = deepest.max(depth + 1);
3333                pending.push((node.children(), depth + 1));
3334            }
3335        }
3336    }
3337    deepest
3338}
3339
3340fn is_inline_container(inline: &Inline) -> bool {
3341    matches!(
3342        inline,
3343        Inline::Emphasis(_)
3344            | Inline::Strong(_)
3345            | Inline::Delete(_)
3346            | Inline::Mark(_)
3347            | Inline::Link(_)
3348            | Inline::Image(_)
3349            | Inline::LinkReference(_)
3350            | Inline::ImageReference(_)
3351            | Inline::InlineFootnote(_)
3352            | Inline::TextDirective(_)
3353    )
3354}
3355
3356/// Whether `inlines` hold a link formed from brackets, at any depth: an
3357/// autolink, which keeps open brackets from nothing, does not count.
3358fn contains_link_inline(inlines: &[Inline]) -> bool {
3359    inlines.iter().any(|inline| {
3360        matches!(
3361            inline,
3362            Inline::Link(_) | Inline::LinkReference(_) | Inline::WikiLink(_)
3363        ) || contains_link_inline(inline.children())
3364    })
3365}
3366
3367fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
3368    if input.as_bytes().get(open) != Some(&b'[') {
3369        return None;
3370    }
3371    let mut lookups = DirectLookups { input };
3372    bracket_walk(open + 1, |cursor| {
3373        link_label_step(&mut lookups, input, cursor)
3374    })
3375}
3376
3377/// One step of the walk to a link label's closing `]`: escapes, code spans,
3378/// autolinks, and inline HTML are stepped over whole, so brackets inside them
3379/// do not count; other brackets nest.
3380fn link_label_step(lookups: &mut impl Lookups, input: &str, cursor: usize) -> BracketStep {
3381    let Some((next, char)) = next_char(input, cursor) else {
3382        return BracketStep::End;
3383    };
3384    match char {
3385        '\\' => {
3386            BracketStep::Pass(next_char(input, next).map_or(next, |(after_escape, _)| after_escape))
3387        }
3388        '`' => BracketStep::Pass(code_span_end(lookups, input, cursor).unwrap_or(next)),
3389        '<' => BracketStep::Pass(
3390            autolink_end(lookups, input, cursor)
3391                .or_else(|| html_inline_end(lookups, input, cursor))
3392                .unwrap_or(next),
3393        ),
3394        '[' => BracketStep::Open(next),
3395        ']' => BracketStep::Close(next),
3396        _ => BracketStep::Pass(next),
3397    }
3398}
3399
3400fn parse_text_directive(
3401    input: &str,
3402    index: usize,
3403    base_offset: usize,
3404    definitions: Option<Labels<'_>>,
3405    diagnostics: &mut Vec<Diagnostic>,
3406    pass: &mut InlinePass,
3407) -> Option<(usize, Inline)> {
3408    if input[index..].starts_with("::") {
3409        return None;
3410    }
3411    if index > 0 {
3412        let previous = input[..index].chars().next_back()?;
3413        if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
3414            return None;
3415        }
3416    }
3417    let opener_offset = index + 1;
3418    let opener = parse_directive_opener_with(
3419        &input[opener_offset..],
3420        |close, open| {
3421            let found = match close {
3422                DirectiveClose::Label => pass.scan.link_label_end(opener_offset + open),
3423                DirectiveClose::Attributes => {
3424                    pass.scan.directive_attributes_close(opener_offset + open)
3425                }
3426            };
3427            found.map(|position| position - opener_offset)
3428        },
3429        |label, attributes, rest| text_directive_ends(label, attributes, rest.bytes().next()),
3430    );
3431    let opener = match opener {
3432        Ok(opener) => opener,
3433        Err(refused) => {
3434            // A `[` or `{` left open after a valid name is malformed; a bad
3435            // name, or an opener refused for what follows it, is plain text.
3436            if refused == Refused::Unclosed {
3437                diagnostics.push(Diagnostic::new(
3438                    DiagnosticSeverity::Error,
3439                    DiagnosticCode::InvalidDirectiveName,
3440                    Span::new(base_offset + index, base_offset + input.len()),
3441                    "text directive opener is malformed",
3442                ));
3443            }
3444            return None;
3445        }
3446    };
3447    let label = opener
3448        .label
3449        .map(|source| {
3450            parse_inlines_with_context(
3451                source,
3452                base_offset + index + 1 + opener.name.len() + 1,
3453                definitions,
3454                diagnostics,
3455                pass.state,
3456            )
3457        })
3458        .unwrap_or_default();
3459    let at = base_offset + opener_offset;
3460    opener.report_dropped(diagnostics, |start, end| Span::new(at + start, at + end));
3461    let DirectiveOpener {
3462        name,
3463        attributes,
3464        consumed,
3465        ..
3466    } = opener;
3467    Some((
3468        index + 1 + consumed,
3469        Inline::TextDirective(TextDirective {
3470            meta: NodeMeta::new(Some(Span::new(
3471                base_offset + index,
3472                base_offset + index + 1 + consumed,
3473            ))),
3474            name,
3475            label,
3476            attributes,
3477        }),
3478    ))
3479}
3480
3481/// Whether a text directive with this label and attribute source ends before
3482/// `next`: whitespace or the end of the content, or ASCII punctuation when the
3483/// label is not empty or the attributes hold a non-space char. So `:e[a].` is
3484/// a directive, while `:word:`, `:a@b.c`, `(:note)`, `:e{}x`, `:e{}.`, and
3485/// `:e[a]b` stay text. Deciding from the sources keeps a refused opener from
3486/// parsing its attributes.
3487fn text_directive_ends(label: Option<&str>, attributes: Option<&str>, next: Option<u8>) -> bool {
3488    match next {
3489        None | Some(b' ' | b'\t' | b'\n' | b'\r') => true,
3490        Some(byte) if byte.is_ascii_punctuation() => {
3491            label.is_some_and(|label| !label.is_empty())
3492                || attributes
3493                    .is_some_and(|source| source.bytes().any(|byte| !byte.is_ascii_whitespace()))
3494        }
3495        Some(_) => false,
3496    }
3497}
3498
3499/// A directive's name, label, and attributes, read from the text after its
3500/// colons.
3501struct DirectiveOpener<'a> {
3502    name: String,
3503    label: Option<&'a str>,
3504    attributes: Vec<DirectiveAttribute>,
3505    /// The ranges of the attributes dropped for having no valid name.
3506    dropped: Vec<(usize, usize)>,
3507    /// The bytes the opener takes.
3508    consumed: usize,
3509}
3510
3511impl DirectiveOpener<'_> {
3512    /// A warning for each dropped attribute, spanning `span(start, end)`.
3513    fn report_dropped(
3514        &self,
3515        diagnostics: &mut Vec<Diagnostic>,
3516        span: impl Fn(usize, usize) -> Span,
3517    ) {
3518        for &(start, end) in &self.dropped {
3519            diagnostics.push(Diagnostic::new(
3520                DiagnosticSeverity::Warning,
3521                DiagnosticCode::InvalidDirectiveAttribute,
3522                span(start, end),
3523                "directive attribute without a valid name is dropped",
3524            ));
3525        }
3526    }
3527}
3528
3529/// Why `parse_directive_opener_with` read no opener.
3530#[derive(Clone, Copy, PartialEq, Eq)]
3531enum Refused {
3532    /// The text after the colons does not start with a valid directive name.
3533    Name,
3534    /// The `[` or `{` after the name has no closing `]` or `}`.
3535    Unclosed,
3536    /// `accept` refused the opener for its label, attributes, or what follows.
3537    Follow,
3538}
3539
3540/// `parse_directive_opener_with` for a block directive line, finding closing
3541/// brackets in `input` alone.
3542fn parse_directive_opener<'a>(
3543    input: &'a str,
3544    accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3545) -> Result<DirectiveOpener<'a>, Refused> {
3546    parse_directive_opener_with(
3547        input,
3548        |close, open| match close {
3549            DirectiveClose::Label => find_link_label_end(input, open),
3550            DirectiveClose::Attributes => find_directive_attributes_close(input, open),
3551        },
3552        accept,
3553    )
3554}
3555
3556/// Which closing position `parse_directive_opener_with` asks its finder for.
3557#[derive(Clone, Copy)]
3558enum DirectiveClose {
3559    /// The `]` closing the `[label]` opening at the given position.
3560    Label,
3561    /// The `}` closing the `{attributes}` opening at the given position.
3562    Attributes,
3563}
3564
3565/// Reads the directive opener at the start of `input`, the text after the
3566/// colons: a name, then an optional `[label]`, then optional `{attributes}`.
3567/// `find_close` gives the `]` or `}` closing the bracket at a position.
3568///
3569/// `accept` sees the label source, the attribute source, and the text after
3570/// the opener, and runs before the attributes are parsed. That order keeps
3571/// refused openers linear-time: `:a{` repeated, then `}x`, refuses every
3572/// opener without parsing the attribute source each one spans to the end.
3573/// The cost is that acceptance reads the attribute source, not the parsed
3574/// attributes: `:e{!}.` is a text directive, since its attribute source is not
3575/// blank, whose invalid attribute is dropped, so it is written `:e.`, which
3576/// reads back as text.
3577fn parse_directive_opener_with<'a>(
3578    input: &'a str,
3579    mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
3580    accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3581) -> Result<DirectiveOpener<'a>, Refused> {
3582    let mut index = 0;
3583    while let Some((next, char)) = next_char(input, index) {
3584        if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
3585            index = next;
3586        } else {
3587            break;
3588        }
3589    }
3590    let name = &input[..index];
3591    if !is_directive_name(name) {
3592        return Err(Refused::Name);
3593    }
3594
3595    let mut label = None;
3596    let mut attribute_source = None;
3597    let mut consumed = index;
3598    if input.as_bytes().get(consumed) == Some(&b'[') {
3599        let close = find_close(DirectiveClose::Label, consumed).ok_or(Refused::Unclosed)?;
3600        label = Some(&input[consumed + 1..close]);
3601        consumed = close + 1;
3602    }
3603    if input.as_bytes().get(consumed) == Some(&b'{') {
3604        let close = find_close(DirectiveClose::Attributes, consumed).ok_or(Refused::Unclosed)?;
3605        attribute_source = Some((consumed + 1, &input[consumed + 1..close]));
3606        consumed = close + 1;
3607    }
3608    if !accept(
3609        label,
3610        attribute_source.map(|(_, source)| source),
3611        &input[consumed..],
3612    ) {
3613        return Err(Refused::Follow);
3614    }
3615    let (attributes, dropped) = match attribute_source {
3616        Some((start, source)) => {
3617            let (attributes, mut dropped) = parse_attributes(source);
3618            for range in &mut dropped {
3619                *range = (range.0 + start, range.1 + start);
3620            }
3621            (attributes, dropped)
3622        }
3623        None => (Vec::new(), Vec::new()),
3624    };
3625
3626    Ok(DirectiveOpener {
3627        name: name.into(),
3628        label,
3629        attributes,
3630        dropped,
3631        consumed,
3632    })
3633}
3634
3635/// Quote states of the walk to a directive's closing `}`: outside quotes,
3636/// inside `"…"`, inside `'…'`.
3637const DIRECTIVE_QUOTE_STATES: usize = 3;
3638
3639fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
3640    if input.as_bytes().get(open) != Some(&b'{') {
3641        return None;
3642    }
3643    path_walk((open + 1) * DIRECTIVE_QUOTE_STATES, |node| {
3644        directive_attributes_step(input, node)
3645    })
3646}
3647
3648/// One step of the walk to a directive's closing `}`, over nodes
3649/// `byte position * DIRECTIVE_QUOTE_STATES + quote state`: a `\` escapes the
3650/// next byte, and a `}` inside quotes does not close.
3651fn directive_attributes_step(input: &str, node: usize) -> Step {
3652    let bytes = input.as_bytes();
3653    let (cursor, quote) = (node / DIRECTIVE_QUOTE_STATES, node % DIRECTIVE_QUOTE_STATES);
3654    let at = |position: usize, quote: usize| Step::Next(position * DIRECTIVE_QUOTE_STATES + quote);
3655    let Some(&byte) = bytes.get(cursor) else {
3656        return Step::Done(None);
3657    };
3658    if byte == b'\\' {
3659        return at((cursor + 2).min(bytes.len()), quote);
3660    }
3661    match (quote, byte) {
3662        (0, b'"') => at(cursor + 1, 1),
3663        (0, b'\'') => at(cursor + 1, 2),
3664        (0, b'}') => Step::Done(Some(cursor)),
3665        (1, b'"') | (2, b'\'') => at(cursor + 1, 0),
3666        _ => at(cursor + 1, quote),
3667    }
3668}
3669
3670/// The attributes in `input`, the text between a directive's braces, and the
3671/// ranges of `input` holding attributes dropped for having no valid name.
3672fn parse_attributes(input: &str) -> (Vec<DirectiveAttribute>, Vec<(usize, usize)>) {
3673    let mut attributes = Vec::new();
3674    let mut dropped = Vec::new();
3675    let mut cursor = 0;
3676    while cursor < input.len() {
3677        cursor = skip_spaces(input, cursor);
3678        if cursor >= input.len() {
3679            break;
3680        }
3681
3682        if input.as_bytes().get(cursor) == Some(&b'#') {
3683            let (id, next) = parse_attribute_token(input, cursor + 1);
3684            if !id.is_empty() {
3685                attributes.push(DirectiveAttribute {
3686                    name: "id".into(),
3687                    value: Some(id.into()),
3688                });
3689            }
3690            cursor = next;
3691            continue;
3692        }
3693
3694        if input.as_bytes().get(cursor) == Some(&b'.') {
3695            let (class, next) = parse_attribute_token(input, cursor + 1);
3696            if !class.is_empty() {
3697                attributes.push(DirectiveAttribute {
3698                    name: "class".into(),
3699                    value: Some(class.into()),
3700                });
3701            }
3702            cursor = next;
3703            continue;
3704        }
3705
3706        let token_start = cursor;
3707        let (name, next) = parse_attribute_name(input, cursor);
3708        if name.is_empty() {
3709            dropped.push((token_start, input.trim_end().len().max(token_start)));
3710            break;
3711        }
3712        let mut token_end = next;
3713        cursor = skip_spaces(input, next);
3714        let value = if input.as_bytes().get(cursor) == Some(&b'=') {
3715            cursor = skip_spaces(input, cursor + 1);
3716            token_end = cursor;
3717            if let Some((value, next)) = parse_attribute_value(input, cursor) {
3718                cursor = next;
3719                token_end = next;
3720                Some(value)
3721            } else {
3722                Some(String::new())
3723            }
3724        } else {
3725            None
3726        };
3727        // A token that is no attribute name is dropped, as a malformed value
3728        // is read leniently: the document keeps only attributes it can write.
3729        if crate::validate::is_attribute_name(name) {
3730            attributes.push(DirectiveAttribute {
3731                name: name.into(),
3732                value,
3733            });
3734        } else {
3735            dropped.push((token_start, token_end));
3736        }
3737    }
3738    (attributes, dropped)
3739}
3740
3741fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
3742    let mut cursor = index;
3743    while let Some((next, char)) = next_char(input, cursor) {
3744        if char.is_whitespace() {
3745            break;
3746        }
3747        cursor = next;
3748    }
3749    (&input[index..cursor], cursor)
3750}
3751
3752fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
3753    let mut cursor = index;
3754    while let Some((next, char)) = next_char(input, cursor) {
3755        if char.is_whitespace() || char == '=' {
3756            break;
3757        }
3758        cursor = next;
3759    }
3760    (&input[index..cursor], cursor)
3761}
3762
3763fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
3764    let quote = input.as_bytes().get(index).copied();
3765    if matches!(quote, Some(b'"' | b'\'')) {
3766        let quote = quote?;
3767        let mut cursor = index + 1;
3768        while cursor < input.len() {
3769            let (next, char) = next_char(input, cursor)?;
3770            if char as u8 == quote && !is_escaped_at(input, cursor) {
3771                return Some((
3772                    decode_escapes_and_references(&input[index + 1..cursor]),
3773                    next,
3774                ));
3775            }
3776            cursor = next;
3777        }
3778        return None;
3779    }
3780
3781    let (value, next) = parse_attribute_token(input, index);
3782    Some((
3783        decode_selected_escapes_and_references(value, |char| matches!(char, '\\' | '&')),
3784        next,
3785    ))
3786}
3787
3788/// The end and the normalized value of the code span opening at `index`.
3789fn parse_code_span_with(
3790    lookups: &mut impl Lookups,
3791    input: &str,
3792    index: usize,
3793) -> Option<(usize, String)> {
3794    let len = backtick_run_len(input, index);
3795    let search_start = index + len;
3796    let close = lookups.code_span_close(search_start, len)?;
3797    Some((
3798        close + len,
3799        normalize_code_span(&input[search_start..close]),
3800    ))
3801}
3802
3803/// The end of the code span opening at `index`, as `parse_code_span` finds it.
3804fn code_span_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
3805    let len = backtick_run_len(input, index);
3806    Some(lookups.code_span_close(index + len, len)? + len)
3807}
3808
3809fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
3810    let bytes = input.as_bytes();
3811    let mut cursor = start;
3812    while cursor < bytes.len() {
3813        if bytes[cursor] != b'`' {
3814            cursor = next_char(input, cursor)
3815                .map(|(next, _)| next)
3816                .unwrap_or(bytes.len());
3817            continue;
3818        }
3819        let run_len = bytes[cursor..]
3820            .iter()
3821            .take_while(|byte| **byte == b'`')
3822            .count();
3823        if run_len == marker_len {
3824            return Some(cursor);
3825        }
3826        cursor += run_len;
3827    }
3828    None
3829}
3830
3831fn normalize_code_span(input: &str) -> String {
3832    let (before, after) = code_padding(input);
3833    let mut normalized = String::new();
3834    let content = &input[before..input.len() - after];
3835    let mut cursor = 0;
3836    while cursor < content.len() {
3837        let (next, char) = next_char(content, cursor).expect("valid UTF-8 byte index");
3838        if char == '\r' {
3839            if content.as_bytes().get(next) == Some(&b'\n') {
3840                cursor = next + 1;
3841            } else {
3842                cursor = next;
3843            }
3844            normalized.push(' ');
3845            continue;
3846        }
3847        if char == '\n' {
3848            normalized.push(' ');
3849            cursor = next;
3850            continue;
3851        }
3852        normalized.push(char);
3853        cursor = next;
3854    }
3855    normalized
3856}
3857
3858/// The bytes of padding a code span or `$` math drops from each end of
3859/// `input`, its content: one space or line ending at each end, when both ends
3860/// hold one and the content is not all spaces and line endings.
3861pub(crate) fn code_padding(input: &str) -> (usize, usize) {
3862    let space = |text: &str| -> Option<usize> {
3863        match text.as_bytes().first()? {
3864            b' ' | b'\n' => Some(1),
3865            b'\r' if text.as_bytes().get(1) == Some(&b'\n') => Some(2),
3866            b'\r' => Some(1),
3867            _ => None,
3868        }
3869    };
3870    let end_space = |text: &str| -> Option<usize> {
3871        match text.as_bytes().last()? {
3872            b' ' | b'\r' => Some(1),
3873            b'\n' if text.len() >= 2 && text.as_bytes()[text.len() - 2] == b'\r' => Some(2),
3874            b'\n' => Some(1),
3875            _ => None,
3876        }
3877    };
3878    match (space(input), end_space(input)) {
3879        (Some(before), Some(after))
3880            if input
3881                .bytes()
3882                .any(|byte| !matches!(byte, b' ' | b'\n' | b'\r')) =>
3883        {
3884            (before, after)
3885        }
3886        _ => (0, 0),
3887    }
3888}
3889
3890fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3891    delimiter_flanking(input, index, marker_len).left
3892}
3893
3894fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3895    delimiter_flanking(input, index, marker_len).right
3896}
3897
3898/// Whether a run of `marker` bytes starts at `index`: the byte before it is
3899/// not an unescaped `marker`. Only the one byte is looked at, so asking at
3900/// every byte of a long run stays linear.
3901fn starts_delimiter_run(input: &str, index: usize, marker: u8) -> bool {
3902    index == 0 || input.as_bytes()[index - 1] != marker || is_escaped_at(input, index - 1)
3903}
3904
3905fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
3906    let bytes = input.as_bytes();
3907    let mut cursor = index;
3908    while bytes.get(cursor) == Some(&marker) {
3909        cursor += 1;
3910    }
3911    cursor - index
3912}
3913
3914#[derive(Clone, Copy)]
3915struct DelimiterFlanking {
3916    left: bool,
3917    right: bool,
3918    previous: Option<char>,
3919    next: Option<char>,
3920}
3921
3922fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
3923    delimiter_flanking_within(input, index, marker_len, false, false)
3924}
3925
3926/// `delimiter_flanking` for a run that may sit right inside a mark span: a
3927/// bounded side flanks as if the input ended there, the way the span's content
3928/// reads on its own.
3929fn delimiter_flanking_within(
3930    input: &str,
3931    index: usize,
3932    marker_len: usize,
3933    bounded_before: bool,
3934    bounded_after: bool,
3935) -> DelimiterFlanking {
3936    let previous = if bounded_before {
3937        None
3938    } else {
3939        input[..index].chars().next_back().map(source_char)
3940    };
3941    let next = if bounded_after {
3942        None
3943    } else {
3944        input[index + marker_len..].chars().next().map(source_char)
3945    };
3946
3947    let previous_whitespace = previous.is_none_or(char::is_whitespace);
3948    let next_whitespace = next.is_none_or(char::is_whitespace);
3949    let previous_punctuation = previous.is_some_and(is_flanking_punctuation);
3950    let next_punctuation = next.is_some_and(is_flanking_punctuation);
3951
3952    let left = next.is_some()
3953        && !next_whitespace
3954        && !(next_punctuation && !previous_whitespace && !previous_punctuation);
3955    let right = previous.is_some()
3956        && !previous_whitespace
3957        && !(previous_punctuation && !next_whitespace && !next_punctuation);
3958
3959    DelimiterFlanking {
3960        left,
3961        right,
3962        previous,
3963        next,
3964    }
3965}
3966
3967/// Dollar-fenced inline math, GitHub Flavored Markdown dialect.
3968///
3969/// A `$` is a flanking delimiter resolved at scan time (math is not pushed onto
3970/// the emphasis delimiter stack). An opening run of one or two `$` (runs of
3971/// three or more never form math) scans forward for a matching closing run:
3972///
3973/// * single `$`: cannot open if the next char is ASCII whitespace; the closing
3974///   `$` cannot be preceded by ASCII whitespace nor followed by an ASCII digit;
3975///   a `\$` inside is skipped (the backslash is kept verbatim, never a
3976///   delimiter); the close must be a run of exactly one `$`.
3977/// * double `$$`: no flanking and no digit guard; closes on the next run of two
3978///   `$`; content is kept verbatim and may span newlines (this is still an
3979///   inline display span — `$$` flow blocks are handled by `parse_math_block`).
3980///
3981/// The closing run is matched greedily (the nearest valid close wins), which is
3982/// equivalent to emphasis-style "nearest preceding open" because a failed open
3983/// emits a literal `$`/`$$` and the scan resumes after it. Content for the
3984/// single-`$` form is normalized like a code span (line endings → spaces, one
3985/// edge-space strip); the `$$` display form is verbatim. The `` $`…`$ `` code
3986/// form takes precedence.
3987fn parse_math_inline(
3988    lookups: &mut impl Lookups,
3989    input: &str,
3990    index: usize,
3991) -> Option<(usize, String, MathInlineKind)> {
3992    if let Some((end, value)) = parse_math_code_inline(lookups, input, index) {
3993        return Some((end, value, MathInlineKind::Code));
3994    }
3995
3996    let bytes = input.as_bytes();
3997    let open_dollars = bytes[index..]
3998        .iter()
3999        .take_while(|byte| **byte == b'$')
4000        .count();
4001    // The maximum math fence length is 2 dollars: a run of three or more never
4002    // opens math.
4003    if open_dollars == 0 || open_dollars > 2 {
4004        return None;
4005    }
4006
4007    let content_start = index + open_dollars;
4008    let close = scan_to_closing_dollar(lookups, input, content_start, open_dollars)?;
4009    let content_end = close - open_dollars;
4010    // The span requires `endpos - startpos >= fence_length * 2 + 1`, i.e. at
4011    // least one content byte between the open and close fences.
4012    if content_end <= content_start {
4013        return None;
4014    }
4015
4016    let raw = &input[content_start..content_end];
4017    let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
4018    let kind = MathInlineKind::Dollar { dollars };
4019    let value = if math_is_padded(kind) {
4020        normalize_math_text(raw)
4021    } else {
4022        raw.into()
4023    };
4024    Some((close, value, kind))
4025}
4026
4027/// Whether math of `kind` follows the code-span content rules: line endings
4028/// read as spaces, and padding `code_padding` finds is dropped. Only
4029/// single-dollar math does.
4030pub(crate) fn math_is_padded(kind: MathInlineKind) -> bool {
4031    matches!(kind, MathInlineKind::Dollar { dollars: 1 })
4032}
4033
4034/// Whether `written`, read alone, is exactly one inline math holding `value`
4035/// as `kind`: validation's check that dollar math reads back as written.
4036pub(crate) fn reads_as_inline_math(written: &str, value: &str, kind: MathInlineKind) -> bool {
4037    parse_math_inline(&mut DirectLookups { input: written }, written, 0).is_some_and(
4038        |(end, read, read_kind)| end == written.len() && read == value && read_kind == kind,
4039    )
4040}
4041
4042/// Scans for the closing dollar run. `start` is the first content byte
4043/// (just past the opening run); returns the byte offset just past a matching
4044/// closing run of exactly `open_dollars` `$`.
4045fn scan_to_closing_dollar(
4046    lookups: &mut impl Lookups,
4047    input: &str,
4048    start: usize,
4049    open_dollars: usize,
4050) -> Option<usize> {
4051    let bytes = input.as_bytes();
4052    if open_dollars == 1 {
4053        // A space immediately after a single opening `$` forbids the open.
4054        if bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
4055            return None;
4056        }
4057        // An escaped `\$` is content, not a delimiter (the backslash stays in
4058        // the content verbatim); the first other `$` closes, unless a space
4059        // comes before it or an ASCII digit after it. The lookup keeps a run
4060        // of `\$` from being walked again from each `$` that opens before it.
4061        let close = lookups.unescaped_dollar(start)?;
4062        if is_math_space(bytes[close - 1]) || bytes.get(close + 1).is_some_and(u8::is_ascii_digit) {
4063            return None;
4064        }
4065        return Some(close + 1);
4066    }
4067    // The display form closes at the first `$$`, even the first two `$` of a
4068    // longer run.
4069    lookups.find("$$", start).map(|close| close + 2)
4070}
4071
4072/// Math whitespace: ASCII tab, line feed, carriage return, and space.
4073fn is_math_space(byte: u8) -> bool {
4074    matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
4075}
4076
4077/// Applies the code-span content rules to dollar-fenced math: line endings
4078/// become single spaces, and the padding `code_padding` finds is dropped.
4079fn normalize_math_text(input: &str) -> String {
4080    normalize_code_span(input)
4081}
4082
4083fn parse_math_code_inline(
4084    lookups: &mut impl Lookups,
4085    input: &str,
4086    index: usize,
4087) -> Option<(usize, String)> {
4088    if !input[index..].starts_with("$`") {
4089        return None;
4090    }
4091
4092    let search_start = index + 2;
4093    let close = lookups.find("`$", search_start)?;
4094    if close == search_start {
4095        return None;
4096    }
4097
4098    Some((close + 2, input[search_start..close].into()))
4099}
4100
4101fn parse_link_resource(
4102    lookups: &mut impl Lookups,
4103    input: &str,
4104    open: usize,
4105) -> Option<(usize, ParsedLinkResource, LinkTailRanges)> {
4106    let bytes = input.as_bytes();
4107    if bytes.get(open) != Some(&b'(') {
4108        return None;
4109    }
4110    let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
4111    if bytes.get(cursor) == Some(&b')') {
4112        return Some((
4113            cursor + 1,
4114            ParsedLinkResource {
4115                destination: String::new(),
4116                destination_kind: LinkDestinationKind::Omitted,
4117                title: None,
4118            },
4119            LinkTailRanges {
4120                destination: (cursor, cursor),
4121                title: None,
4122            },
4123        ));
4124    }
4125    if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
4126        let omitted = cursor;
4127        let (title, next) = parse_link_title(input, cursor)?;
4128        let title_range = (cursor + 1, next - 1);
4129        cursor = skip_link_resource_space(input, next)?;
4130        if bytes.get(cursor) == Some(&b')') {
4131            return Some((
4132                cursor + 1,
4133                ParsedLinkResource {
4134                    destination: String::new(),
4135                    destination_kind: LinkDestinationKind::Omitted,
4136                    title: Some(title),
4137                },
4138                LinkTailRanges {
4139                    destination: (omitted, omitted),
4140                    title: Some(title_range),
4141                },
4142            ));
4143        }
4144        return None;
4145    }
4146    let destination_start = cursor;
4147    let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
4148    let destination_range = match destination_kind {
4149        LinkDestinationKind::Angle => (destination_start + 1, next - 1),
4150        _ => (destination_start, next),
4151    };
4152    let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
4153    cursor = after_destination;
4154    if bytes.get(cursor) == Some(&b')') {
4155        return Some((
4156            cursor + 1,
4157            ParsedLinkResource {
4158                destination,
4159                destination_kind,
4160                title: None,
4161            },
4162            LinkTailRanges {
4163                destination: destination_range,
4164                title: None,
4165            },
4166        ));
4167    }
4168    if !had_space {
4169        return None;
4170    }
4171
4172    let (title, next) = parse_link_title(input, cursor)?;
4173    let title_range = (cursor + 1, next - 1);
4174    cursor = skip_link_resource_space(input, next)?;
4175    if bytes.get(cursor) == Some(&b')') {
4176        Some((
4177            cursor + 1,
4178            ParsedLinkResource {
4179                destination,
4180                destination_kind,
4181                title: Some(title),
4182            },
4183            LinkTailRanges {
4184                destination: destination_range,
4185                title: Some(title_range),
4186            },
4187        ))
4188    } else {
4189        None
4190    }
4191}
4192
4193fn parse_link_destination(
4194    lookups: &mut impl Lookups,
4195    input: &str,
4196    index: usize,
4197) -> Option<(String, LinkDestinationKind, usize)> {
4198    if input.as_bytes().get(index) == Some(&b'<') {
4199        let stop = lookups.angle_destination_stop(index + 1)?;
4200        if input.as_bytes()[stop] != b'>' {
4201            return None;
4202        }
4203        return Some((
4204            decode_escapes_and_references(&input[index + 1..stop]),
4205            LinkDestinationKind::Angle,
4206            stop + 1,
4207        ));
4208    }
4209
4210    let mut cursor = index;
4211    let mut depth = 0usize;
4212    while cursor < input.len() {
4213        let (next, char) = next_char(input, cursor)?;
4214        // A bare destination terminates on ASCII space or an ASCII control
4215        // character; Unicode whitespace (e.g. U+00A0) is ordinary. A backslash
4216        // before a space is NOT an escape (only ASCII punctuation is escapable),
4217        // so `\ ` still terminates the destination → `[a](\ b)` is not a link.
4218        if char == ' ' || source_char(char).is_ascii_control() {
4219            break;
4220        }
4221        if char == '(' && !is_escaped_at(input, cursor) {
4222            depth += 1;
4223            // CommonMark caps balanced parens in a bare destination at depth 32.
4224            if depth > 32 {
4225                return None;
4226            }
4227        } else if char == ')' && !is_escaped_at(input, cursor) {
4228            if depth == 0 {
4229                break;
4230            }
4231            depth -= 1;
4232        }
4233        cursor = next;
4234    }
4235
4236    if cursor == index || depth > 0 {
4237        None
4238    } else {
4239        Some((
4240            decode_escapes_and_references(&input[index..cursor]),
4241            LinkDestinationKind::Bare,
4242            cursor,
4243        ))
4244    }
4245}
4246
4247fn parse_link_title(input: &str, index: usize) -> Option<(Title, usize)> {
4248    let opener = input.as_bytes().get(index).copied()?;
4249    let (closer, title_kind) = match opener {
4250        b'"' => ('"', LinkTitleKind::DoubleQuote),
4251        b'\'' => ('\'', LinkTitleKind::SingleQuote),
4252        b'(' => (')', LinkTitleKind::Paren),
4253        _ => return None,
4254    };
4255    let mut cursor = index + 1;
4256    while cursor < input.len() {
4257        let (next, char) = next_char(input, cursor)?;
4258        if char == closer && !is_escaped_at(input, cursor) {
4259            if contains_blank_line(&input[index + 1..cursor]) {
4260                return None;
4261            }
4262            let value = decode_escapes_and_references(&input[index + 1..cursor]);
4263            return Some((Title::new(value, title_kind), next));
4264        }
4265        if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
4266            return None;
4267        }
4268        cursor = next;
4269    }
4270    None
4271}
4272
4273fn contains_blank_line(input: &str) -> bool {
4274    if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
4275        return false;
4276    }
4277    // A title that merely begins or ends with an EOL is allowed; only an INTERIOR
4278    // blank line (a blank line bounded by content on both sides) is rejected. The
4279    // empty first/last line entries that a leading/trailing newline produces are
4280    // boundary artifacts, not blank lines in the title content.
4281    let map = SourceMap::verbatim(input.len(), 0);
4282    let lines = collect_lines(input, &map);
4283    let interior = lines.len().saturating_sub(1);
4284    lines
4285        .iter()
4286        .take(interior)
4287        .skip(1)
4288        .any(|line| is_blank(line.text))
4289}
4290
4291fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
4292    skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
4293}
4294
4295fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
4296    let mut line_breaks = 0usize;
4297    let mut had_space = false;
4298    while input
4299        .as_bytes()
4300        .get(index)
4301        .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
4302    {
4303        had_space = true;
4304        match input.as_bytes()[index] {
4305            b'\n' => {
4306                line_breaks += 1;
4307                if line_breaks > 1 {
4308                    return None;
4309                }
4310                index += 1;
4311            }
4312            b'\r' => {
4313                line_breaks += 1;
4314                if line_breaks > 1 {
4315                    return None;
4316                }
4317                if input.as_bytes().get(index + 1) == Some(&b'\n') {
4318                    index += 2;
4319                } else {
4320                    index += 1;
4321                }
4322            }
4323            _ => index += 1,
4324        }
4325    }
4326    Some((index, had_space))
4327}
4328
4329pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
4330    let bytes = input.as_bytes();
4331    let mut cursor = index;
4332    let mut count = 0;
4333    while cursor > 0 && bytes[cursor - 1] == b'\\' {
4334        count += 1;
4335        cursor -= 1;
4336    }
4337    count % 2 == 1
4338}
4339
4340fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
4341    let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
4342    let (destination, destination_kind, next) =
4343        parse_link_destination(&mut DirectLookups { input }, input, cursor)?;
4344    cursor = next;
4345
4346    let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
4347    cursor = next;
4348    if cursor >= input.len() {
4349        return Some(ParsedLinkResource {
4350            destination,
4351            destination_kind,
4352            title: None,
4353        });
4354    }
4355    if !had_space {
4356        return None;
4357    }
4358
4359    let (title, next) = parse_link_title(input, cursor)?;
4360    let after_title = skip_link_resource_space(input, next)?;
4361    (after_title == input.len()).then_some(ParsedLinkResource {
4362        destination,
4363        destination_kind,
4364        title: Some(title),
4365    })
4366}
4367
4368/// The char that closes the title `source` opens after its destination, when
4369/// no unescaped one follows the opener yet.
4370fn open_definition_title(source: &str) -> Option<char> {
4371    let (cursor, _) = skip_link_resource_space_with_info(source, 0)?;
4372    let (_, _, next) =
4373        parse_link_destination(&mut DirectLookups { input: source }, source, cursor)?;
4374    let (cursor, had_space) = skip_link_resource_space_with_info(source, next)?;
4375    if !had_space {
4376        return None;
4377    }
4378    let closer = match source.as_bytes().get(cursor)? {
4379        b'"' => '"',
4380        b'\'' => '\'',
4381        b'(' => ')',
4382        _ => return None,
4383    };
4384    (!line_may_close_title(&source[cursor + 1..], closer)).then_some(closer)
4385}
4386
4387/// Whether `line` holds an unescaped `closer`, or an unescaped `(` inside a
4388/// parenthesized title, either of which settles the title. A backslash
4389/// escape never spans a line ending, so each line is read alone.
4390fn line_may_close_title(line: &str, closer: char) -> bool {
4391    line.char_indices().any(|(index, char)| {
4392        (char == closer || (closer == ')' && char == '(')) && !is_escaped_at(line, index)
4393    })
4394}
4395
4396fn line_can_start_definition_title(input: &str) -> bool {
4397    let trimmed = trim_ascii_start(input);
4398    matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
4399}
4400
4401fn push_line(output: &mut String, line: &str) {
4402    if !output.is_empty() {
4403        output.push('\n');
4404    }
4405    output.push_str(line);
4406}
4407
4408/// Ends a code or math block's last line when the input ended without a line
4409/// ending: every line of such a block's value ends with one. The added ending
4410/// repeats the value's first line ending, `\n` when it has none.
4411fn end_last_line(value: &mut String) {
4412    if value.is_empty() || ends_with_line_ending(value) {
4413        return;
4414    }
4415    value.push_str(value_line_ending(value));
4416}
4417
4418/// The line ending a code or math value's lines take: its first one, `\n`
4419/// when it has none.
4420fn value_line_ending(value: &str) -> &'static str {
4421    match value.find(['\r', '\n']) {
4422        Some(index) if value[index..].starts_with("\r\n") => "\r\n",
4423        Some(index) if value[index..].starts_with('\r') => "\r",
4424        _ => "\n",
4425    }
4426}
4427
4428fn ends_with_line_ending(input: &str) -> bool {
4429    input.ends_with('\n') || input.ends_with('\r')
4430}
4431
4432fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
4433    if !text.is_empty() {
4434        nodes.push(Inline::Text(Text {
4435            meta: NodeMeta::new(Some(Span::new(text_start, end))),
4436            value: core::mem::take(text),
4437        }));
4438    }
4439}
4440
4441fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
4442    let char = input[index..].chars().next()?;
4443    Some((index + char.len_utf8(), char))
4444}
4445
4446/// A CommonMark "Unicode punctuation character" for emphasis/strong flanking:
4447/// ASCII punctuation plus the non-ASCII Unicode `P*`/`S*` categories. Only the
4448/// flanking classification needs the Unicode set; escape/label logic stays
4449/// ASCII-only via `char::is_ascii_punctuation`.
4450fn is_flanking_punctuation(value: char) -> bool {
4451    value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
4452}
4453
4454/// Fold a reference label to its matching identifier. Per CommonMark, two
4455/// labels match when their RAW source (no backslash unescape, no entity decode)
4456/// agrees after collapsing internal spaces, tabs, and line endings to a single
4457/// space, trimming, and Unicode case-folding (`to_uppercase()` then
4458/// `to_lowercase()`). So `[foo\!]`
4459/// does NOT match `[foo!]`, and `[&copy;]` does NOT match `[©]`.
4460///
4461/// The serializer's `normalize_reference_label` delegates here so the
4462/// Shortcut/Collapsed omission oracle stays in lockstep with this matcher.
4463pub(crate) fn normalize_label(label: &str) -> String {
4464    label
4465        // Unicode full casefold maps capital sharp S (ẞ, U+1E9E) to "ss"; Rust's
4466        // `to_uppercase` leaves it unchanged (it is already uppercase), so without
4467        // this `[ẞ]` would not match a `[SS]: …` definition (links 540). This is
4468        // the only char where `to_uppercase().to_lowercase()` diverges from the
4469        // full casefold that matters for label matching.
4470        .replace('ẞ', "ss")
4471        .replace('\0', "\u{FFFD}")
4472        .split([' ', '\t', '\n', '\r'])
4473        .filter(|word| !word.is_empty())
4474        .collect::<Vec<_>>()
4475        .join(" ")
4476        .to_uppercase()
4477        .to_lowercase()
4478}
4479
4480/// The identifiers a document's references can resolve to: those of its own
4481/// definitions, sorted and deduplicated so that a lookup is a binary search.
4482#[derive(Clone, Copy)]
4483pub(crate) struct Labels<'a> {
4484    pub(crate) own: &'a [String],
4485}
4486
4487fn definition_exists(definitions: Option<Labels<'_>>, label: &str) -> bool {
4488    if label.is_empty() || !reference_label_is_within_limit(label) {
4489        return false;
4490    }
4491    let Some(definitions) = definitions else {
4492        return false;
4493    };
4494    definitions.own.binary_search(&identifier(label)).is_ok()
4495}
4496
4497/// The identifier a reference with `label` resolves to: its normalized form,
4498/// with U+0000 read as U+FFFD as in the definitions a document holds.
4499fn identifier(label: &str) -> String {
4500    let identifier = normalize_label(label);
4501    if identifier.contains('\0') {
4502        identifier.replace('\0', "\u{FFFD}")
4503    } else {
4504        identifier
4505    }
4506}
4507
4508fn reference_label_is_within_limit(label: &str) -> bool {
4509    label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
4510}
4511
4512/// Whether `[label]` reads back as a link reference label holding `label`:
4513/// the walk to its `]` ends at its own `]`, so the label holds no unescaped
4514/// bracket and does not escape its close; it is within the length limit, is
4515/// not blank, and holds no blank line, which would end its paragraph.
4516pub(crate) fn is_reference_label(label: &str) -> bool {
4517    let written = alloc::format!("[{label}]");
4518    path_walk(1, |cursor| reference_label_step(&written, cursor)) == Some(written.len() - 1)
4519        && reference_label_is_within_limit(label)
4520        && !normalize_label(label).is_empty()
4521        && !holds_blank_line(label)
4522}
4523
4524/// Whether `[^label]` reads back as a footnote label holding `label`: a
4525/// footnote label whose `]` it does not escape.
4526pub(crate) fn is_written_footnote_label(label: &str) -> bool {
4527    is_footnote_label(label) && !is_escaped_at(&alloc::format!("{label}]"), label.len())
4528}
4529
4530/// Whether a line of `text` other than its first and its last, which share
4531/// their lines with what is written around `text`, is blank.
4532fn holds_blank_line(text: &str) -> bool {
4533    let text = text.replace("\r\n", "\n");
4534    let lines: Vec<&str> = text.split(['\n', '\r']).collect();
4535    lines.len() > 2 && lines[1..lines.len() - 1].iter().any(|line| is_blank(line))
4536}
4537
4538fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
4539    let (columns, bytes) = leading_indent(input);
4540    if columns <= 3 {
4541        Some(&input[bytes..])
4542    } else {
4543        None
4544    }
4545}
4546
4547/// The marker and length of the code fence that `input` opens: three or more
4548/// backticks or tildes, where a backtick fence's info string holds no backtick.
4549fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
4550    let marker = match input.as_bytes().first()? {
4551        b'`' => FenceMarker::Backtick,
4552        b'~' => FenceMarker::Tilde,
4553        _ => return None,
4554    };
4555    let byte = match marker {
4556        FenceMarker::Backtick => b'`',
4557        FenceMarker::Tilde => b'~',
4558    };
4559    let length = input
4560        .as_bytes()
4561        .iter()
4562        .take_while(|item| **item == byte)
4563        .count();
4564    let opens = length >= 3 && (byte == b'~' || !input[length..].contains('`'));
4565    opens.then_some((marker, length))
4566}
4567
4568fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
4569    let byte = match marker {
4570        FenceMarker::Backtick => b'`',
4571        FenceMarker::Tilde => b'~',
4572    };
4573    let count = input
4574        .as_bytes()
4575        .iter()
4576        .take_while(|item| **item == byte)
4577        .count();
4578    count >= length && is_blank(&input[count..])
4579}
4580
4581fn trim_closing_hashes(input: &str) -> &str {
4582    let input = input.trim_end_matches([' ', '\t']);
4583    let hash_start = input.trim_end_matches('#').len();
4584    if hash_start == input.len() {
4585        return input;
4586    }
4587    if hash_start == 0 {
4588        return "";
4589    }
4590
4591    let before = &input[..hash_start];
4592    if before.ends_with(' ') || before.ends_with('\t') {
4593        before.trim_end_matches([' ', '\t'])
4594    } else {
4595        input
4596    }
4597}
4598
4599/// Whether `input` opens with a list item marker, up to three spaces in.
4600fn opens_list_item(input: &str) -> bool {
4601    trim_up_to_three_spaces(input).is_some_and(|rest| blocks::list_marker_head(rest).is_some())
4602}
4603
4604/// The offset of `slice` inside `text` when it is a borrowed sub-slice of it.
4605fn slice_offset_in(text: &str, slice: &str) -> Option<usize> {
4606    let offset = (slice.as_ptr() as usize).checked_sub(text.as_ptr() as usize)?;
4607    (offset + slice.len() <= text.len()).then_some(offset)
4608}
4609
4610fn leading_indent(input: &str) -> (usize, usize) {
4611    leading_indent_at(input, 0)
4612}
4613
4614/// The columns and bytes of `input`'s leading spaces and tabs, its first
4615/// char at source column `start`, from which tabs reach their stops.
4616fn leading_indent_at(input: &str, start: usize) -> (usize, usize) {
4617    let mut column = start;
4618    let mut bytes = 0usize;
4619    for byte in input.as_bytes() {
4620        match *byte {
4621            b' ' => column += 1,
4622            b'\t' => column += 4 - (column % 4),
4623            _ => break,
4624        }
4625        bytes += 1;
4626    }
4627    (column - start, bytes)
4628}
4629
4630fn task_marker_checked(input: &str) -> Option<bool> {
4631    if input.starts_with("[ ]") {
4632        Some(false)
4633    } else if input.starts_with("[x]") || input.starts_with("[X]") {
4634        Some(true)
4635    } else {
4636        None
4637    }
4638}
4639
4640/// Whether `text` holds only spaces and tabs, as a blank line does.
4641pub(crate) fn is_blank(text: &str) -> bool {
4642    text.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
4643}
4644
4645fn trim_ascii_start(input: &str) -> &str {
4646    input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
4647}
4648
4649fn leading_trim_bytes(input: &str) -> usize {
4650    input.len() - trim_ascii_start(input).len()
4651}
4652
4653fn parse_table_delimiter(input: &str) -> Option<Vec<TableAlignment>> {
4654    // Every cell trims to colons around dashes, so a row with any other char
4655    // is no delimiter row, whatever its cells.
4656    if !input.contains('-')
4657        || !input
4658            .chars()
4659            .all(|char| matches!(char, '|' | '-' | ':' | ' ' | '\t'))
4660    {
4661        return None;
4662    }
4663    let cells = table_row_cell_ranges(input);
4664    if cells.is_empty() {
4665        return None;
4666    }
4667    let mut alignments = Vec::new();
4668    for (start, end) in cells {
4669        alignments.push(table_delimiter_alignment(
4670            input[start..end].trim_matches([' ', '\t']),
4671        )?);
4672    }
4673    Some(alignments)
4674}
4675
4676// A delimiter cell is `:?` `-`+ `:?` once trimmed: colons only at the
4677// boundaries, the dashes contiguous, no interior space or colon.
4678fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
4679    let bytes = cell.as_bytes();
4680    let mut cursor = 0;
4681    let left = bytes.first() == Some(&b':');
4682    if left {
4683        cursor += 1;
4684    }
4685    let dash_start = cursor;
4686    while bytes.get(cursor) == Some(&b'-') {
4687        cursor += 1;
4688    }
4689    if cursor == dash_start {
4690        return None;
4691    }
4692    let right = bytes.get(cursor) == Some(&b':');
4693    if right {
4694        cursor += 1;
4695    }
4696    if cursor != bytes.len() {
4697        return None;
4698    }
4699    Some(match (left, right) {
4700        (true, true) => TableAlignment::Center,
4701        (true, false) => TableAlignment::Left,
4702        (false, true) => TableAlignment::Right,
4703        (false, false) => TableAlignment::None,
4704    })
4705}
4706
4707/// The cell-delimiter pipes of a table row, in order: every `|` that is not
4708/// escaped by an odd backslash run. The cell's inline content reads an
4709/// escaped pipe as `|`.
4710fn table_row_delimiters(row: &str) -> Vec<usize> {
4711    let bytes = row.as_bytes();
4712    let mut delimiters = Vec::new();
4713    let mut cursor = 0;
4714    while cursor < bytes.len() {
4715        match bytes[cursor] {
4716            b'\\' => {
4717                let backslashes = delimiter_byte_run_len(row, cursor, b'\\');
4718                let pipe = cursor + backslashes;
4719                cursor = if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
4720                    pipe + 1
4721                } else {
4722                    pipe
4723                };
4724            }
4725            b'|' => {
4726                delimiters.push(cursor);
4727                cursor += 1;
4728            }
4729            _ => cursor += 1,
4730        }
4731    }
4732    delimiters
4733}
4734
4735/// The byte ranges of `input`'s cells, between the pipes that delimit them.
4736fn table_row_cell_ranges(input: &str) -> Vec<(usize, usize)> {
4737    let trimmed = input.trim_matches([' ', '\t']);
4738    let offset = leading_trim_bytes(input);
4739    let delimiters = table_row_delimiters(trimmed);
4740    let mut cells = Vec::new();
4741    let mut start = 0;
4742    for &pipe in &delimiters {
4743        cells.push((offset + start, offset + pipe));
4744        start = pipe + 1;
4745    }
4746    cells.push((offset + start, offset + trimmed.len()));
4747
4748    // A delimiter at the very start or end (only whitespace after it) is a
4749    // border, not the edge of an empty cell.
4750    if delimiters.first() == Some(&0) {
4751        cells.remove(0);
4752    }
4753    if delimiters
4754        .last()
4755        .is_some_and(|&pipe| is_blank(&trimmed[pipe + 1..]))
4756    {
4757        cells.pop();
4758    }
4759    cells
4760}
4761
4762/// One table cell's inline input (whitespace trimmed, escaped pipes read as
4763/// `|`), its source map, and its span.
4764struct TableCellSource {
4765    text: DerivedText,
4766    span: Span,
4767}
4768
4769/// The cells of `row`, a slice of `line.text`, as `table_row_cell_ranges`
4770/// splits it.
4771fn table_row_cells(line: &Line<'_>, row: &str) -> Vec<TableCellSource> {
4772    let row_offset = slice_offset_in(line.text, row).unwrap_or(0);
4773    table_row_cell_ranges(row)
4774        .into_iter()
4775        .map(|(start, end)| {
4776            let raw = &row[start..end];
4777            let leading = leading_trim_bytes(raw);
4778            let content = raw.trim_matches([' ', '\t']);
4779            let offset = row_offset + start + leading;
4780            let span = Span::new(
4781                line.source_start(offset),
4782                line.source_end(offset + content.len()),
4783            );
4784            let mut text = DerivedText::default();
4785            // GitHub/cmark-gfm reads an odd backslash run before `|` as a
4786            // literal pipe; the run keeps its other backslashes, which the
4787            // inline parser resolves as written. The `|` read from `\|` maps
4788            // to both of its bytes.
4789            let bytes = content.as_bytes();
4790            let mut copied = 0;
4791            let mut cursor = 0;
4792            while cursor < bytes.len() {
4793                if bytes[cursor] == b'\\' {
4794                    let pipe = cursor + delimiter_byte_run_len(content, cursor, b'\\');
4795                    if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
4796                        text.append(line, &content[copied..pipe - 1]);
4797                        text.append_replacing(
4798                            "|",
4799                            line.source_start(offset + pipe - 1),
4800                            line.source_end(offset + pipe + 1),
4801                        );
4802                        copied = pipe + 1;
4803                    }
4804                    cursor = pipe;
4805                } else {
4806                    cursor += 1;
4807                }
4808            }
4809            text.append(line, &content[copied..]);
4810            TableCellSource { text, span }
4811        })
4812        .collect()
4813}
4814
4815fn table_has_separator(header: &str, delimiter: &str) -> bool {
4816    // GFM makes leading/trailing pipes optional, so `parse_table_delimiter` plus
4817    // the header/alignment column-count check usually suffice. The one exception
4818    // is a single resolved column with no disambiguating syntax: `a\n-\nb` has
4819    // matching one-column shapes yet no pipe and no alignment colon, so it is a
4820    // loose paragraph/setext, not a table. A single column still forms a table
4821    // when a pipe appears in the header/delimiter or the delimiter carries an
4822    // explicit alignment colon (`a\n-:`, `a\n:-:`, …).
4823    let Some(alignments) = parse_table_delimiter(delimiter) else {
4824        return true;
4825    };
4826    if alignments.len() == 1 {
4827        return contains_unescaped_pipe(header)
4828            || contains_unescaped_pipe(delimiter)
4829            || delimiter.contains(':');
4830    }
4831    true
4832}
4833
4834// Still used by `block_quote_table_body_row` to detect a table row appearing as
4835// a block-quote continuation line (which DOES require a pipe).
4836fn contains_unescaped_pipe(input: &str) -> bool {
4837    !table_row_delimiters(input).is_empty()
4838}
4839
4840// A GFM footnote definition `[^label]:` is a block boundary: it interrupts a
4841// paragraph and ends a prior footnote's lazy continuation.
4842/// The end of the `<…>` autolink opening at `index`.
4843fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4844    let end = lookups.find(">", index)? + 1;
4845    is_angle_autolink(&input[index + 1..end - 1]).then_some(end)
4846}
4847
4848/// The end of the inline HTML (comment, processing instruction, CDATA,
4849/// declaration, or tag) opening at `index`.
4850fn html_inline_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4851    let rest = &input[index..];
4852    if rest.starts_with("<!--") {
4853        return Some(lookups.find("-->", index)? + 3);
4854    }
4855    // The `?>` closing a processing instruction follows its `<?`.
4856    if rest.starts_with("<?") {
4857        return Some(lookups.find("?>", index + 2)? + 2);
4858    }
4859    if rest.starts_with("<![CDATA[") {
4860        return Some(lookups.find("]]>", index)? + 3);
4861    }
4862    if is_declaration_start(rest) {
4863        return Some(lookups.find(">", index)? + 1);
4864    }
4865    parse_html_tag_with(lookups, input, index).map(|(end, _)| end)
4866}
4867
4868fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
4869    parse_html_tag_with(&mut DirectLookups { input }, input, index)
4870}
4871
4872fn parse_html_tag_with<'a>(
4873    lookups: &mut impl Lookups,
4874    input: &'a str,
4875    index: usize,
4876) -> Option<(usize, &'a str)> {
4877    let bytes = input.as_bytes();
4878    if bytes.get(index) != Some(&b'<') {
4879        return None;
4880    }
4881
4882    let closing = bytes.get(index + 1) == Some(&b'/');
4883    let name_start = index + if closing { 2 } else { 1 };
4884    let first = *bytes.get(name_start)?;
4885    if !first.is_ascii_alphabetic() {
4886        return None;
4887    }
4888
4889    let mut cursor = name_start + 1;
4890    while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
4891        cursor += 1;
4892    }
4893    let name = &input[name_start..cursor];
4894
4895    if closing {
4896        cursor = skip_spaces(input, cursor);
4897        if bytes.get(cursor) == Some(&b'>') {
4898            return Some((cursor + 1, name));
4899        }
4900        return None;
4901    }
4902
4903    let mut needs_space = false;
4904    loop {
4905        let before_spaces = cursor;
4906        cursor = skip_spaces(input, cursor);
4907        let had_space = cursor > before_spaces;
4908        match bytes.get(cursor) {
4909            Some(b'>') => return Some((cursor + 1, name)),
4910            Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
4911            Some(byte) if had_space && html_attribute_name_start(*byte) => {
4912                cursor += 1;
4913                while bytes
4914                    .get(cursor)
4915                    .is_some_and(|byte| html_attribute_name_byte(*byte))
4916                {
4917                    cursor += 1;
4918                }
4919                let after_name = cursor;
4920                let after_spaces = skip_spaces(input, cursor);
4921                if bytes.get(after_spaces) == Some(&b'=') {
4922                    cursor = skip_spaces(input, after_spaces + 1);
4923                    cursor = parse_html_attribute_value(lookups, input, cursor)?;
4924                } else {
4925                    cursor = after_name;
4926                }
4927                needs_space = true;
4928            }
4929            Some(_) if needs_space => return None,
4930            _ => return None,
4931        }
4932    }
4933}
4934
4935fn parse_html_attribute_value(
4936    lookups: &mut impl Lookups,
4937    input: &str,
4938    index: usize,
4939) -> Option<usize> {
4940    let bytes = input.as_bytes();
4941    match bytes.get(index)? {
4942        b'"' => Some(lookups.find("\"", index + 1)? + 1),
4943        b'\'' => Some(lookups.find("'", index + 1)? + 1),
4944        b'=' | b'<' | b'>' | b'`' => None,
4945        _ => {
4946            let mut cursor = index;
4947            while bytes.get(cursor).is_some_and(|byte| {
4948                !byte.is_ascii_whitespace()
4949                    && !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
4950            }) {
4951                cursor += 1;
4952            }
4953            if cursor == index {
4954                None
4955            } else {
4956                Some(cursor)
4957            }
4958        }
4959    }
4960}
4961
4962fn html_name_byte(byte: u8) -> bool {
4963    byte.is_ascii_alphanumeric() || byte == b'-'
4964}
4965
4966fn html_attribute_name_start(byte: u8) -> bool {
4967    byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
4968}
4969
4970fn html_attribute_name_byte(byte: u8) -> bool {
4971    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
4972}
4973
4974fn skip_spaces(input: &str, mut index: usize) -> usize {
4975    while input
4976        .as_bytes()
4977        .get(index)
4978        .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
4979    {
4980        index += 1;
4981    }
4982    index
4983}
4984
4985/// The destination of the angle-bracket autolink `<uri>`, when `uri` writes
4986/// one: the URI itself, or `mailto:` and an email address.
4987pub(crate) fn angle_autolink_destination(uri: &str) -> Option<String> {
4988    if is_uri_autolink(uri) {
4989        Some(String::from(uri))
4990    } else if is_email_autolink(uri) {
4991        Some(alloc::format!("mailto:{uri}"))
4992    } else {
4993        None
4994    }
4995}
4996
4997/// Whether `<uri>` is an angle-bracket autolink.
4998fn is_angle_autolink(uri: &str) -> bool {
4999    is_uri_autolink(uri) || is_email_autolink(uri)
5000}
5001
5002fn is_uri_autolink(input: &str) -> bool {
5003    // A scheme is at most 32 bytes, so the `:` ending it is within the first 33.
5004    let Some(colon) = input
5005        .as_bytes()
5006        .iter()
5007        .take(33)
5008        .position(|byte| *byte == b':')
5009    else {
5010        return false;
5011    };
5012    let scheme = &input[..colon];
5013    if scheme.len() < 2 || scheme.len() > 32 {
5014        return false;
5015    }
5016    let mut bytes = scheme.bytes();
5017    if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
5018        return false;
5019    }
5020    if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
5021        return false;
5022    }
5023    input[colon + 1..]
5024        .chars()
5025        .map(source_char)
5026        .all(|char| !matches!(char, '<' | '>' | ' ') && !char.is_ascii_control())
5027}
5028
5029fn is_email_autolink(input: &str) -> bool {
5030    // No email address contains `<`, and stopping at one keeps a run of `<`
5031    // openers that share one later `>` linear.
5032    if input
5033        .chars()
5034        .any(|char| char.is_whitespace() || char == '<')
5035    {
5036        return false;
5037    }
5038    let Some(at) = input.find('@') else {
5039        return false;
5040    };
5041    if at == 0 || at + 1 >= input.len() {
5042        return false;
5043    }
5044    // Angle-bracket `<email>` autolinks use the strict CommonMark domain
5045    // grammar but, unlike the GFM bare form, allow a single (dotless) label.
5046    is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
5047}
5048
5049/// The destination of a literal autolink written as `text`, when `text` is
5050/// exactly one literal autolink the inline parser can read: the text after the
5051/// prefix `parse_literal_autolink` gives it.
5052///
5053/// Whether a literal autolink forms depends in part on what follows it:
5054/// cmark-gfm's domain check counts the `.` and `_` in the punctuation trimmed
5055/// off its end, and `www.` at a line start links `www`. So `text` is read at a
5056/// line start and followed by what lets the most through, `..` and a space:
5057/// every literal autolink the parser reads has a destination, and a text the
5058/// parser would trim never does.
5059pub(crate) fn literal_autolink_destination(text: &str) -> Option<String> {
5060    let mut input = String::from(text);
5061    input.push_str(".. ");
5062    let (end, prefix) =
5063        parse_literal_autolink(&input, 0, &mut LiteralAutolinkScan::default(), true)?;
5064    (end == text.len()).then(|| {
5065        let mut destination = String::from(prefix);
5066        destination.push_str(text);
5067        destination
5068    })
5069}
5070
5071/// A literal autolink starting at `index`, by cmark-gfm's dispatch: it tries,
5072/// in order, `http(s)://` URLs, `www.` URLs, extended-protocol
5073/// (`mailto:`/`xmpp:`) emails, and bare emails. Each branch enforces
5074/// cmark-gfm's per-scheme preceding-character guard and its domain/host
5075/// rules; the trailing trim is shared (`autolink_delim`). Returns the end and
5076/// the prefix the destination puts before `input[index..end]`, the visible
5077/// original: `http://` for a `www.` URL, `mailto:` for a bare email, and
5078/// nothing otherwise.
5079///
5080/// `urls` is false inside an open `[`: as in cmark-gfm, a `http://`,
5081/// `https://`, or `www.` URL after a `[` no `]` has closed yet stays text, and
5082/// only an email address links.
5083fn parse_literal_autolink(
5084    input: &str,
5085    index: usize,
5086    scan: &mut LiteralAutolinkScan,
5087    urls: bool,
5088) -> Option<(usize, &'static str)> {
5089    let rest = &input[index..];
5090    let www = rest
5091        .as_bytes()
5092        .get(..4)
5093        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."));
5094    if !urls {
5095        // A literal starting `www.` is a URL, linked or not, as outside an
5096        // open `[`: no email starts there.
5097        if www {
5098            return None;
5099        }
5100        return parse_literal_email(input, index, scan);
5101    }
5102
5103    // `http://` / `https://` URLs. cmark requires the char before the scheme
5104    // to be non-alphanumeric (so `mmmhttp://…` does not link from `mmmh`).
5105    if let Some(scheme_len) = rest
5106        .starts_with("http://")
5107        .then_some(7)
5108        .or_else(|| rest.starts_with("https://").then_some(8))
5109    {
5110        if !literal_scheme_prefix_ok(input, index) {
5111            return None;
5112        }
5113        // A non-empty domain is additionally required, so `http://`,
5114        // `http://#`, `http://$`, and a bracketed IPv6 host are not links.
5115        if !http_literal_host_ok(&input[index + scheme_len..]) {
5116            return None;
5117        }
5118        // The URL extent is scanned from the very start (after `://`) and the
5119        // trailing trim runs over the whole URL.
5120        let end = autolink_url_end(input, index + scheme_len);
5121        if end <= index + scheme_len {
5122            return None;
5123        }
5124        return Some((end, ""));
5125    }
5126
5127    // `www.` URLs (synthesize a `http://` href). cmark allows the preceding
5128    // char to be one of `*_~(` or whitespace (or start of input).
5129    if www {
5130        if !literal_www_prefix_ok(input, index) {
5131            return None;
5132        }
5133        if !check_domain(rest, false) {
5134            return None;
5135        }
5136        let end = autolink_url_end(input, index);
5137        if end <= index || (end <= index + 3 && !literal_starts_line(input, index)) {
5138            return None;
5139        }
5140        return Some((end, "http://"));
5141    }
5142
5143    parse_literal_email(input, index, scan)
5144}
5145
5146// The char immediately before a `http(s)://` literal must be non-alphabetic.
5147// An escaped `<` (`\<http://…`) is just literal text before the URL, so the
5148// literal still forms (the `<` is not treated as an angle-autolink opener).
5149fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
5150    if index == 0 {
5151        return true;
5152    }
5153    let Some(previous) = input[..index].chars().next_back() else {
5154        return true;
5155    };
5156    !previous.is_ascii_alphabetic()
5157}
5158
5159// The char before a `www.` literal must be one of cmark-gfm's accepted ASCII
5160// delimiters or ordinary Markdown layout whitespace. As on GitHub, other
5161// Unicode whitespace is not a start delimiter for this branch.
5162fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
5163    if index == 0 {
5164        return true;
5165    }
5166    let Some(previous) = input[..index].chars().next_back() else {
5167        return true;
5168    };
5169    if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
5170        return true;
5171    }
5172    matches!(previous, ' ' | '\t' | '\n' | '\r')
5173}
5174
5175fn literal_starts_line(input: &str, index: usize) -> bool {
5176    index == 0
5177        || input
5178            .as_bytes()
5179            .get(index - 1)
5180            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
5181}
5182
5183fn http_literal_host_ok(host: &str) -> bool {
5184    match host.chars().next() {
5185        Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => check_domain(host, true),
5186        Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
5187            check_domain(host, true)
5188        }
5189        _ => false,
5190    }
5191}
5192
5193// Port of cmark-gfm `is_valid_hostchar`: a host char is valid when it is not a
5194// Unicode space and not a Unicode punctuation character.
5195fn is_valid_hostchar(char: char) -> bool {
5196    !char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
5197}
5198
5199// Port of cmark-gfm `check_domain`: whether `data` starts with a valid host.
5200// Rejects a `_` in either of the last two `.`-separated host segments (unless
5201// the host has >10 segments — a DoS guard). When `allow_short` is false a dot
5202// is required (the `www.` rule). The URL extent past the host is determined by
5203// `autolink_url_end`, so only the verdict matters here.
5204//
5205// cmark walks bytes with `is_valid_hostchar` decoding each char; this walks
5206// chars directly (UTF-8 safe) over the host prefix, which yields the same
5207// dot/underscore-segment verdict. A `\` escapes the following char, so a host
5208// can run on over escaped spaces through later `www.` candidates; the walk
5209// stops at the eleventh dot, which settles the verdict, so that the walks
5210// from all the candidates of a pass stay linear in total.
5211fn check_domain(data: &str, allow_short: bool) -> bool {
5212    let mut np = 0usize;
5213    let mut uscore1 = 0usize;
5214    let mut uscore2 = 0usize;
5215
5216    let mut chars = data.char_indices().peekable();
5217    while let Some((offset, char)) = chars.next() {
5218        // cmark's accounting loop runs `for (i = 1; i < size - 1; i++)`: it
5219        // never inspects the first char (offset 0) nor the final char of the
5220        // chunk. We replicate that — a trailing `_` (e.g. `http://a_`) is not
5221        // counted, so the link still forms.
5222        let account = offset != 0 && chars.peek().is_some();
5223        match char {
5224            '\\' => {
5225                // Escape: consume the next char as a literal host char.
5226                chars.next();
5227            }
5228            '_' if account => uscore2 += 1,
5229            '.' if account => {
5230                uscore1 = uscore2;
5231                uscore2 = 0;
5232                np += 1;
5233                if np > 10 {
5234                    return true;
5235                }
5236            }
5237            '_' | '.' | '-' => {}
5238            _ => {
5239                if !is_valid_hostchar(source_char(char)) {
5240                    break;
5241                }
5242            }
5243        }
5244    }
5245
5246    if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
5247        return false;
5248    }
5249
5250    allow_short || np > 0
5251}
5252
5253// Forward scan from `start` for the URL extent: Unicode whitespace, `<`, a
5254// non-ASCII char in CommonMark's Unicode punctuation set (full-width `,` or
5255// `。`) other than the replacement char, `[[`, or `]` ends the URL. CommonMark allows
5256// `>` and `[` inside (the renderer percent-encodes them); a `]` is
5257// additionally treated as a hard URL boundary (autolink-3), so a `]` ends the
5258// scan and is never part of the link. The trailing trim may reach `start`.
5259fn autolink_url_end(input: &str, start: usize) -> usize {
5260    let bytes = input.as_bytes();
5261    let mut end = start;
5262    // The extent stops at the first `]` outside backticks unless a `[` came
5263    // before it in the URL (no balancing).
5264    let mut strict_has_open_bracket = false;
5265    let mut strict_inside_backticks = false;
5266    for (offset, char) in input[start..].char_indices() {
5267        if char.is_whitespace()
5268            || char == '<'
5269            || is_autolink_terminating_control(char)
5270            || (!char.is_ascii()
5271                && char != '\u{FFFD}'
5272                && crate::unicode_punctuation::is_unicode_punctuation(char))
5273            || input[start + offset..].starts_with("[[")
5274        {
5275            break;
5276        }
5277        match char {
5278            '[' => strict_has_open_bracket = true,
5279            '`' => strict_inside_backticks = !strict_inside_backticks,
5280            ']' if !strict_has_open_bracket && !strict_inside_backticks => break,
5281            _ => {}
5282        }
5283        // A `\` before ASCII punctuation other than `.` ends the URL. The
5284        // serializer writes text after a literal autolink with backslash
5285        // escapes (`\*`, `\_`, `\[`, …); stopping here keeps such a tree
5286        // writable. cmark-gfm keeps the `\*x` in `www.a.com\*x` inside the
5287        // URL; this crate deliberately diverges. A `\` before `.` or a
5288        // non-punctuation char stays part of the URL (`www.x.com/a\.`).
5289        if char == '\\' {
5290            if let Some(&next) = bytes.get(start + offset + 1) {
5291                if next.is_ascii_punctuation() && next != b'.' {
5292                    break;
5293                }
5294            }
5295        }
5296        end = start + offset + char.len_utf8();
5297    }
5298    autolink_delim(input, start, end)
5299}
5300
5301fn is_autolink_terminating_control(char: char) -> bool {
5302    matches!(char, '\u{2066}'..='\u{2069}')
5303}
5304
5305// Port of cmark-gfm `autolink_delim`: trim trailing delimiters from the end of
5306// the URL. A trailing `) ? ! . , : * _ ~ ' "` is trimmed; `)` only when there
5307// are more `)` than `(` in the link; a trailing `&` and alphanumerics ending
5308// in `;` is excluded whole; any other trailing `;` is trimmed alone.
5309fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
5310    let bytes = input.as_bytes();
5311    let mut opening = 0usize;
5312    let mut closing = 0usize;
5313    for &byte in &bytes[start..end] {
5314        match byte {
5315            b'(' => opening += 1,
5316            b')' => closing += 1,
5317            _ => {}
5318        }
5319    }
5320
5321    while end > start {
5322        match bytes[end - 1] {
5323            b')' => {
5324                if closing <= opening {
5325                    break;
5326                }
5327                closing -= 1;
5328                end -= 1;
5329            }
5330            b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
5331                end -= 1;
5332            }
5333            b';' => {
5334                // Walk back over alphanumerics; if they reach a `&`, exclude
5335                // the whole `&…;` entity run, otherwise trim just the `;`.
5336                let mut new_end = end - 1;
5337                while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
5338                    new_end -= 1;
5339                }
5340                if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
5341                    end = new_end - 1;
5342                } else {
5343                    end -= 1;
5344                }
5345            }
5346            _ => break,
5347        }
5348    }
5349    end
5350}
5351
5352/// Scan state shared by the `parse_literal_autolink` calls of one inline pass.
5353/// The pass only moves forward, so each piece lets a later start position reuse
5354/// the bytes already walked for an earlier one instead of rescanning them.
5355#[derive(Default)]
5356struct LiteralAutolinkScan {
5357    /// Runs of email local-part bytes (plus `:` for `mailto:`/`xmpp:`).
5358    email_local: ByteRun,
5359    /// What every start before one `@` shares: its last `:` and its domain.
5360    email_at: EmailAt,
5361    /// The text before a start, for the `+` boundary check.
5362    email_prefix: EmailPrefix,
5363}
5364
5365/// The facts of one `@` that every email start before it reads: the last `:`
5366/// in its local-part run at or after `from`, and the domain end for the plain
5367/// and the `xmpp:` form. A query starting before `from` rescans the run.
5368#[derive(Default)]
5369struct EmailAt {
5370    at: Option<usize>,
5371    from: usize,
5372    last_colon: Option<usize>,
5373    domain_end: [Option<Option<usize>>; 2],
5374}
5375
5376impl EmailAt {
5377    fn update(&mut self, input: &str, index: usize, at: usize) {
5378        if self.at != Some(at) {
5379            *self = Self {
5380                at: Some(at),
5381                from: usize::MAX,
5382                ..Self::default()
5383            };
5384        }
5385        if index < self.from {
5386            self.last_colon = input[index..at].rfind(':').map(|offset| index + offset);
5387            self.from = index;
5388        }
5389    }
5390
5391    /// The end of the domain after the `@`, trimmed and validated, or `None`.
5392    fn domain_end(&mut self, input: &str, is_xmpp: bool) -> Option<usize> {
5393        let at = self.at.expect("updated before use");
5394        *self.domain_end[usize::from(is_xmpp)].get_or_insert_with(|| {
5395            let start = at + 1;
5396            let end = literal_email_domain_end(input, start, is_xmpp)?;
5397            let trimmed = autolink_delim(input, start, end);
5398            (trimmed > start && is_gfm_email_domain(&input[start..trimmed], is_xmpp))
5399                .then_some(trimmed)
5400        })
5401    }
5402}
5403
5404/// Whether the text before a position ends in a bare email, kept as a pass
5405/// walks forward: the start of the text after the last whitespace, the first
5406/// byte there that no local part holds, the last `@`, and after it the first
5407/// byte no domain holds, the first `.`, and the first `.` opening an empty
5408/// label. A query before the walked position walks again from the start.
5409#[derive(Default)]
5410struct EmailPrefix {
5411    cursor: usize,
5412    segment: usize,
5413    non_local: Option<usize>,
5414    at: Option<usize>,
5415    domain_bad: Option<usize>,
5416    domain_dot: Option<usize>,
5417    domain_empty_label: Option<usize>,
5418}
5419
5420impl EmailPrefix {
5421    fn advance(&mut self, input: &str, end: usize) {
5422        if end < self.cursor {
5423            *self = Self::default();
5424        }
5425        let bytes = input.as_bytes();
5426        let from = self.cursor;
5427        for (offset, char) in input[from..end].char_indices() {
5428            let position = from + offset;
5429            if char.is_whitespace() {
5430                *self = Self {
5431                    segment: position + char.len_utf8(),
5432                    ..Self::default()
5433                };
5434                continue;
5435            }
5436            let byte = char.is_ascii().then_some(char as u8);
5437            if self.non_local.is_none() && !byte.is_some_and(is_gfm_email_local_byte) {
5438                self.non_local = Some(position);
5439            }
5440            if char == '@' {
5441                self.at = Some(position);
5442                self.domain_bad = None;
5443                self.domain_dot = None;
5444                self.domain_empty_label = None;
5445                continue;
5446            }
5447            let Some(at) = self.at else {
5448                continue;
5449            };
5450            match byte {
5451                Some(b'.') => {
5452                    self.domain_dot.get_or_insert(position);
5453                    if position == at + 1 || bytes[position - 1] == b'.' {
5454                        self.domain_empty_label.get_or_insert(position);
5455                    }
5456                }
5457                Some(byte) if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') => {}
5458                _ => {
5459                    self.domain_bad.get_or_insert(position);
5460                }
5461            }
5462        }
5463        self.cursor = end;
5464    }
5465
5466    /// Whether the text from the last whitespace before `end` to `end` ends
5467    /// in a bare email: `prefix_ends_with_gfm_email`, walked once per pass.
5468    fn ends_with_email(&mut self, input: &str, end: usize) -> bool {
5469        self.advance(input, end);
5470        let Some(at) = self.at else {
5471            return false;
5472        };
5473        let before = |position: Option<usize>| position.is_some_and(|position| position < end);
5474        at > self.segment
5475            && !self.non_local.is_some_and(|position| position < at)
5476            && end > at + 1
5477            && !before(self.domain_bad)
5478            && before(self.domain_dot)
5479            && !before(self.domain_empty_label)
5480            && !matches!(input.as_bytes()[end - 1], b'.' | b'-' | b'_')
5481    }
5482}
5483
5484/// The extent of one run of bytes accepted by a fixed predicate: every start
5485/// position in `start..=end` stops at `end`. Each `ByteRun` is always queried
5486/// with the same predicate.
5487#[derive(Default)]
5488struct ByteRun {
5489    start: usize,
5490    end: Option<usize>,
5491}
5492
5493impl ByteRun {
5494    /// Returns the first byte position at or after `index` that `in_run`
5495    /// rejects, or the input length.
5496    fn end(&mut self, input: &str, index: usize, in_run: fn(u8) -> bool) -> usize {
5497        if let Some(end) = self.end {
5498            if self.start <= index && index <= end {
5499                return end;
5500            }
5501        }
5502        let end = input.as_bytes()[index..]
5503            .iter()
5504            .position(|byte| !in_run(*byte))
5505            .map_or(input.len(), |offset| index + offset);
5506        self.start = index;
5507        self.end = Some(end);
5508        end
5509    }
5510}
5511
5512fn is_email_local_or_scheme_byte(byte: u8) -> bool {
5513    is_gfm_email_local_byte(byte) || byte == b':'
5514}
5515
5516/// A GFM bare-email literal (and the extended `mailto:`/`xmpp:` protocol
5517/// forms) starting at `index`, which must be the link start: cmark anchors
5518/// the email at the left edge found by rewinding from `@` over
5519/// `[A-Za-z0-9._+-]` (or a `mailto:`/`xmpp:` scheme), so this only succeeds
5520/// when the char before `index` is not part of that left extent.
5521fn parse_literal_email(
5522    input: &str,
5523    index: usize,
5524    scan: &mut LiteralAutolinkScan,
5525) -> Option<(usize, &'static str)> {
5526    // A valid local part holds only local-part bytes behind an optional
5527    // `mailto:`/`xmpp:` scheme, so the `@` must be the byte that ends that run.
5528    let at_index = scan
5529        .email_local
5530        .end(input, index, is_email_local_or_scheme_byte);
5531    if input.as_bytes().get(at_index) != Some(&b'@') || at_index == index {
5532        return None;
5533    }
5534
5535    // Determine whether this `@` is preceded by an extended protocol scheme
5536    // (`mailto:` / `xmpp:`), which both relaxes the href synthesis and (xmpp)
5537    // allows `/` in the domain.
5538    let (auto_mailto, is_xmpp) = classify_email_local(&input[index..at_index]);
5539
5540    // Left-boundary guard (autolink-1): the char before `index` must not be a
5541    // local-part continuation char, otherwise the true link starts earlier and
5542    // this position is interior. After a recognized scheme, the scheme's own
5543    // preceding-char rule is what matters.
5544    if !email_left_boundary_ok(input, index, auto_mailto, |end| {
5545        scan.email_prefix.ends_with_email(input, end)
5546    }) {
5547        return None;
5548    }
5549
5550    // The run holds only local-part bytes and `:`, so the local part after
5551    // its scheme is valid when it is not empty and holds no `:`. The facts of
5552    // the `@` are shared by every start before it, so a run of starts reads
5553    // them once.
5554    let body_start = if auto_mailto {
5555        index
5556    } else if is_xmpp {
5557        index + "xmpp:".len()
5558    } else {
5559        index + "mailto:".len()
5560    };
5561    scan.email_at.update(input, index, at_index);
5562    if body_start >= at_index
5563        || scan
5564            .email_at
5565            .last_colon
5566            .is_some_and(|colon| colon >= body_start)
5567    {
5568        return None;
5569    }
5570    let end = scan.email_at.domain_end(input, is_xmpp)?;
5571    Some((end, if auto_mailto { "mailto:" } else { "" }))
5572}
5573
5574// Classify the local part for the extended-protocol forms. Returns
5575// `(auto_mailto, is_xmpp)`: `mailto:user` → (false, false); `xmpp:user` →
5576// (false, true); a bare local part → (true, false). The scheme match is
5577// case-insensitive.
5578fn classify_email_local(local: &str) -> (bool, bool) {
5579    if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5580        if !rest.is_empty() {
5581            return (false, false);
5582        }
5583    }
5584    if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5585        if !rest.is_empty() {
5586            return (false, true);
5587        }
5588    }
5589    (true, false)
5590}
5591
5592fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
5593    let bytes = input.as_bytes();
5594    let plen = prefix.len();
5595    if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
5596        Some(&input[plen..])
5597    } else {
5598        None
5599    }
5600}
5601
5602// The left-boundary check for an email literal. The link is anchored at its
5603// true left edge: the preceding char must not be an ASCII alphanumeric (which
5604// would extend the local part leftward). For the bare form, a preceding `/` is
5605// also rejected (`/a@b.c` is not linked), while the extended
5606// `mailto:`/`xmpp:` form permits `/` before the scheme (so
5607// `…/mailto:beedrill@…` links).
5608fn email_left_boundary_ok(
5609    input: &str,
5610    index: usize,
5611    auto_mailto: bool,
5612    prefix_ends_with_email: impl FnOnce(usize) -> bool,
5613) -> bool {
5614    if index == 0 {
5615        return true;
5616    }
5617    let Some(previous) = input[..index].chars().next_back() else {
5618        return true;
5619    };
5620    if previous.is_ascii_alphanumeric() {
5621        if auto_mailto && input[index..].starts_with('+') && prefix_ends_with_email(index) {
5622            return true;
5623        }
5624        return false;
5625    }
5626    if auto_mailto && previous == '/' {
5627        return false;
5628    }
5629    true
5630}
5631
5632#[cfg(test)]
5633fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
5634    let start = input[..end]
5635        .char_indices()
5636        .rev()
5637        .find(|(_, char)| char.is_whitespace())
5638        .map_or(0, |(offset, char)| offset + char.len_utf8());
5639    let candidate = &input[start..end];
5640    let Some(at) = candidate.rfind('@') else {
5641        return false;
5642    };
5643    email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
5644}
5645
5646// Validate the email local part. For the bare form, every char must be a GFM
5647// email atext byte (`[A-Za-z0-9.+_-]` plus the dot-separated structure). For
5648// the extended-protocol forms, the part after the scheme is validated.
5649#[cfg(test)]
5650fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
5651    let body = if auto_mailto {
5652        local
5653    } else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5654        rest
5655    } else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5656        rest
5657    } else {
5658        local
5659    };
5660    !body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
5661}
5662
5663// GFM email local-part charset (autolink-1): a narrower set than RFC atext,
5664// matching cmark's rewind class `[A-Za-z0-9.+_-]`.
5665fn is_gfm_email_local_byte(byte: u8) -> bool {
5666    byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
5667}
5668
5669fn is_email_local_part(input: &str) -> bool {
5670    !input.is_empty()
5671        && input
5672            .split('.')
5673            .all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
5674}
5675
5676fn is_email_atext(byte: u8) -> bool {
5677    byte.is_ascii_alphanumeric()
5678        || matches!(
5679            byte,
5680            b'!' | b'#'
5681                | b'$'
5682                | b'%'
5683                | b'&'
5684                | b'\''
5685                | b'*'
5686                | b'+'
5687                | b'/'
5688                | b'='
5689                | b'?'
5690                | b'^'
5691                | b'_'
5692                | b'`'
5693                | b'{'
5694                | b'|'
5695                | b'}'
5696                | b'~'
5697                | b'-'
5698        )
5699}
5700
5701// Port of cmark-gfm's email-domain scan (`postprocess_text`). Scans forward
5702// from `index` over the email domain, accepting alphanumerics, `-`, `_`, and
5703// `.`; for the `xmpp:` form a `/` is also accepted (path). A dot only counts
5704// toward the "at least one dot" requirement when it is followed by an
5705// alphanumeric. The scanned span must be >= 1 byte, contain at least one such
5706// dot, and end in an alphabetic char or a dot. Returns the domain end offset
5707// (before trailing trim), or `None` when invalid.
5708fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
5709    let bytes = input.as_bytes();
5710    let mut end = index;
5711    let mut np = 0usize;
5712    while end < bytes.len() {
5713        let byte = bytes[end];
5714        if byte.is_ascii_alphanumeric() {
5715            end += 1;
5716        } else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
5717            np += 1;
5718            end += 1;
5719        } else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
5720            // `-`/`_` always continue the domain; `/` continues only the xmpp
5721            // path form.
5722            end += 1;
5723        } else {
5724            break;
5725        }
5726    }
5727    if end <= index {
5728        return None;
5729    }
5730    let len = end - index;
5731    let last = bytes[end - 1];
5732    if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
5733        return None;
5734    }
5735    Some(end)
5736}
5737
5738// Final structural validation of the trimmed email domain. The cmark scan
5739// already enforced the dot/last-char rules; this re-checks them after the
5740// shared trailing trim removed any delimiters, and rejects a domain ending in
5741// `-`/`_` (autolink-7: a hyphen in the final label disqualifies the link).
5742fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
5743    if input.is_empty() {
5744        return false;
5745    }
5746    // A `/` path is only legal in the `xmpp:` form; split it off for the host
5747    // structural checks.
5748    let host = if is_xmpp {
5749        input.split('/').next().unwrap_or(input)
5750    } else {
5751        input
5752    };
5753    if !host.contains('.') {
5754        return false;
5755    }
5756    let last = host.as_bytes()[host.len() - 1];
5757    // The final label must not end in `-` or `_`, and the trailing label may
5758    // not be all ASCII digits.
5759    if matches!(last, b'-' | b'_') {
5760        return false;
5761    }
5762    host.split('.').all(|label| {
5763        !label.is_empty()
5764            && label
5765                .bytes()
5766                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
5767    })
5768}
5769
5770fn is_email_domain(input: &str, min_labels: usize) -> bool {
5771    let mut label_count = 0usize;
5772    for label in input.split('.') {
5773        label_count += 1;
5774        let bytes = label.as_bytes();
5775        if bytes.is_empty()
5776            || bytes.len() > 63
5777            || !bytes
5778                .first()
5779                .is_some_and(|byte| byte.is_ascii_alphanumeric())
5780            || !bytes
5781                .last()
5782                .is_some_and(|byte| byte.is_ascii_alphanumeric())
5783            || !bytes
5784                .iter()
5785                .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
5786        {
5787            return false;
5788        }
5789    }
5790    label_count >= min_labels
5791}
5792
5793/// A footnote label: no space, tab, or line ending and, as in a link label,
5794/// no unescaped bracket.
5795pub(crate) fn is_footnote_label(label: &str) -> bool {
5796    !label.is_empty()
5797        && reference_label_is_within_limit(label)
5798        && !label.contains([' ', '\t', '\n', '\r'])
5799        && !label
5800            .match_indices(['[', ']'])
5801            .any(|(index, _)| !is_escaped_at(label, index))
5802}
5803
5804fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
5805    let close = find_footnote_reference_label_end(input, 2)?;
5806    if input.as_bytes().get(close + 1) == Some(&b':') {
5807        Some(close)
5808    } else {
5809        None
5810    }
5811}
5812
5813fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
5814    while cursor < input.len() {
5815        let (next, char) = next_char(input, cursor)?;
5816        if char == ']' && !is_escaped_at(input, cursor) {
5817            return Some(cursor);
5818        }
5819        cursor = next;
5820    }
5821    None
5822}