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, with flanking as the
1572/// CJK-friendly amendments define it (see `delimiter_flanking_within`).
1573///
1574/// Flanking is computed on the whole run (CommonMark treats left/right-flanking
1575/// as a property of the run, not of an individual delimiter), including the
1576/// `_` intraword punctuation rules.
1577///
1578/// GFM's cross-marker bonus: a `*` run immediately adjacent to a `~` counts as
1579/// openable/closeable even though `~` is a punctuation character (this is what
1580/// makes `a*~b~*c` emphasize). A `_` run or a `~` run gets plain CommonMark
1581/// flanking beside a `~`.
1582///
1583/// A side that touches the boundary of an enclosing mark span (`bounded_before`
1584/// / `bounded_after`) flanks as the end of the span's content.
1585fn emphasis_roles(
1586    input: &str,
1587    index: usize,
1588    length: usize,
1589    marker: u8,
1590    bounded_before: bool,
1591    bounded_after: bool,
1592) -> (bool, bool) {
1593    let flanking = delimiter_flanking_within(input, index, length, bounded_before, bounded_after);
1594    let (mut can_open, mut can_close) = if marker == b'_' {
1595        (
1596            flanking.left && (!flanking.right || flanking.previous_punctuation),
1597            flanking.right && (!flanking.left || flanking.next_punctuation),
1598        )
1599    } else {
1600        (flanking.left, flanking.right)
1601    };
1602
1603    // GFM: a `*` run touching a `~` strikethrough marker may open/close even
1604    // when ordinary flanking refuses it (the `~` would otherwise be a blocking
1605    // punctuation neighbour).
1606    if marker == b'*' {
1607        if flanking.next == Some('~') {
1608            can_open = true;
1609        }
1610        if flanking.previous == Some('~') {
1611            can_close = true;
1612        }
1613    }
1614    (can_open, can_close)
1615}
1616
1617/// Settles the roles of every recorded run. The `==` runs take theirs first
1618/// and are paired among themselves; each `*`, `_`, and `~~` run then takes its
1619/// roles once the `==` spans are known, flanking as the edge of a span's
1620/// content where it touches the inner side of the span's delimiter.
1621fn assign_emphasis_roles(input: &str, delimiters: &mut [DelimMarker]) {
1622    let mut opens_span = alloc::vec![false; delimiters.len()];
1623    let mut closes_span = alloc::vec![false; delimiters.len()];
1624    let mut open_marks: Vec<usize> = Vec::new();
1625    // The run each span opened by a run closes at (a run opens at most one).
1626    let mut span_close = alloc::vec![NIL; delimiters.len()];
1627    for (index, run) in delimiters.iter_mut().enumerate() {
1628        if run.marker != b'=' {
1629            continue;
1630        }
1631        assign_double_mark_roles(input, run);
1632        let mut closed = 0;
1633        if run.can_close {
1634            while closed <= run.recloses {
1635                let Some(opener) = open_marks.pop() else {
1636                    break;
1637                };
1638                opens_span[opener] = true;
1639                closes_span[index] = true;
1640                span_close[opener] = index;
1641                closed += 1;
1642            }
1643        }
1644        // What is left of the run after closing may still open.
1645        if run.can_open && run.length >= 2 * closed + 2 {
1646            open_marks.push(index);
1647        }
1648    }
1649
1650    for index in 0..delimiters.len() {
1651        let run = delimiters[index];
1652        if run.marker == b'=' {
1653            continue;
1654        }
1655        let bounded_before = index > 0 && {
1656            let before = &delimiters[index - 1];
1657            before.position + before.length == run.position && opens_span[index - 1]
1658        };
1659        let bounded_after = index + 1 < delimiters.len() && {
1660            let after = &delimiters[index + 1];
1661            after.position == run.position + run.length && closes_span[index + 1]
1662        };
1663        let (can_open, can_close) = emphasis_roles(
1664            input,
1665            run.position,
1666            run.length,
1667            run.marker,
1668            bounded_before,
1669            bounded_after,
1670        );
1671        delimiters[index].can_open = can_open;
1672        delimiters[index].can_close = can_close;
1673    }
1674
1675    // Each run's latest-starting enclosing mark span. Spans are pushed in
1676    // opener order, so the top of `open_spans` is the latest-starting one;
1677    // spans that have ended are dropped when they reach the top.
1678    let mut open_spans: Vec<usize> = Vec::new();
1679    for (index, run) in delimiters.iter_mut().enumerate() {
1680        while open_spans
1681            .last()
1682            .is_some_and(|&opener| span_close[opener] <= index)
1683        {
1684            open_spans.pop();
1685        }
1686        if let Some(&opener) = open_spans.last() {
1687            run.enclosed_from = opener;
1688        }
1689        if span_close[index] != NIL {
1690            open_spans.push(index);
1691        }
1692    }
1693}
1694
1695/// Sets the roles of a `==` run: it opens with its last two characters and
1696/// closes with its first two (and then the next two), each subject to
1697/// CommonMark flanking of that two-character delimiter. A run right after an
1698/// escaped character of the same mark never closes.
1699fn assign_double_mark_roles(input: &str, run: &mut DelimMarker) {
1700    let (index, length) = (run.position, run.length);
1701    let after_escaped_mark =
1702        index > 0 && input.as_bytes()[index - 1] == run.marker && is_escaped_at(input, index - 1);
1703    run.can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
1704    run.recloses = if run.can_close {
1705        (1..length / 2)
1706            .take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
1707            .count()
1708    } else {
1709        0
1710    };
1711    run.can_open = can_open_delimited(input, index + length - 2, 2);
1712}
1713
1714/// Resolves recorded delimiter runs into emphasis and mark nodes using the
1715/// delimiter-stack algorithm, leaving unmatched runs as text. Closers are taken
1716/// in source order; when a pair closes, runs strictly between its opener and
1717/// closer can no longer pair across it.
1718///
1719/// `formed` lists the link, image, footnote, and directive nodes already built
1720/// among `nodes`, with their nesting depth, so spans around them count it.
1721/// Returns the nodes and the deepest nesting among them.
1722///
1723/// `depth` counts the nesting levels enclosing these runs: the inline passes
1724/// around this one, plus the open brackets around a bracket label.
1725fn process_emphasis(
1726    nodes: Vec<Inline>,
1727    mut delimiters: Vec<DelimMarker>,
1728    depth: usize,
1729    formed: &[(usize, usize)],
1730) -> (Vec<Inline>, usize) {
1731    let mut deepest = formed.iter().map(|&(_, depth)| depth).max().unwrap_or(0);
1732    if delimiters.is_empty() {
1733        let mut nodes = nodes;
1734        merge_adjacent_text(&mut nodes);
1735        return (nodes, deepest);
1736    }
1737    let mut nodes = InlineList::new(nodes);
1738
1739    // A run that is consumed, demoted to plain text, or enclosed by a newly
1740    // closed span is unlinked, so the opener walk below only ever visits live
1741    // runs.
1742    let mut links = DelimiterLinks::new(delimiters.len());
1743    for &(node, depth) in formed {
1744        let before = delimiters.partition_point(|run| run.node_index < node);
1745        if before > 0 {
1746            let gap = &mut links.nesting_after[before - 1].total;
1747            *gap = (*gap).max(depth);
1748        }
1749    }
1750
1751    // `openers_bottom` records, per (marker, opener-can-also-close, length % 3),
1752    // the lowest opener index a nested closer is allowed to reach. Closers below
1753    // this bound for their key have already been proven to have no compatible
1754    // opener.
1755    let mut openers_bottom: [Option<usize>; NESTED_MARKERS * 6] = [None; NESTED_MARKERS * 6];
1756    let mut span_bottom: Vec<Vec<(usize, usize)>> = Vec::new();
1757    let mut closer_idx = 0;
1758
1759    // Every run at or after `closer_idx` is still linked: runs are only ever
1760    // unlinked at or before the current closer.
1761    while closer_idx < delimiters.len() {
1762        let closer = delimiters[closer_idx];
1763
1764        let opener = if closer.can_close {
1765            nested_opener(
1766                &delimiters,
1767                &links,
1768                &openers_bottom,
1769                &span_bottom,
1770                closer_idx,
1771            )
1772        } else {
1773            None
1774        };
1775
1776        let Some(opener_idx) = opener else {
1777            if closer.can_close {
1778                // No opener found: remember how far we searched so future
1779                // closers of the same key skip the same dead range.
1780                let key = openers_bottom_key(&closer);
1781                match span_floor(&closer) {
1782                    Some(span) => {
1783                        if span_bottom.len() <= span {
1784                            span_bottom.resize_with(span + 1, Vec::new);
1785                        }
1786                        match span_bottom[span].iter_mut().find(|(k, _)| *k == key) {
1787                            Some(bound) => bound.1 = closer_idx,
1788                            None => span_bottom[span].push((key, closer_idx)),
1789                        }
1790                    }
1791                    None => openers_bottom[key] = Some(closer_idx),
1792                }
1793            }
1794            if !closer.can_open {
1795                links.unlink(closer_idx);
1796            }
1797            closer_idx += 1;
1798            continue;
1799        };
1800
1801        let (used, wrap) = pair_shape(&delimiters[opener_idx], &delimiters[closer_idx]);
1802
1803        // Drop delimiters strictly between the opener and closer: they could not
1804        // match outward across this newly closed span.
1805        let inner = links.close_span(opener_idx, closer_idx);
1806        let fits = depth + inner.total < MAX_INLINE_NESTING
1807            && (wrap.is_mark() || inner.emphasis < MAX_EMPHASIS_NESTING);
1808        if fits {
1809            apply_emphasis(
1810                &mut nodes,
1811                &mut delimiters,
1812                opener_idx,
1813                closer_idx,
1814                used,
1815                wrap,
1816            );
1817            links.nesting_after[opener_idx] = Nesting {
1818                emphasis: if wrap.is_mark() {
1819                    0
1820                } else {
1821                    inner.emphasis + 1
1822                },
1823                total: inner.total + 1,
1824            };
1825            deepest = deepest.max(inner.total + 1);
1826        } else {
1827            // Too deep to wrap: both runs stay whole in their text nodes as
1828            // literal text and pair no further. Any span enclosing this one is
1829            // at least as deep, so it stays literal too.
1830            delimiters[opener_idx].length = 0;
1831            delimiters[closer_idx].length = 0;
1832            links.nesting_after[opener_idx] = inner;
1833        }
1834        if closer.marker == b'=' {
1835            // `==` runs open only with their last two characters, and a long
1836            // run closes a second time only with its next two characters.
1837            delimiters[opener_idx].can_open = false;
1838            let run = &mut delimiters[closer_idx];
1839            run.can_close = run.recloses > 0;
1840            run.recloses = run.recloses.saturating_sub(1);
1841        }
1842
1843        if delimiters[opener_idx].length == 0 {
1844            links.unlink(opener_idx);
1845        }
1846        if delimiters[closer_idx].length == 0 {
1847            links.unlink(closer_idx);
1848            closer_idx += 1;
1849        }
1850        // When a closer still has delimiters left it stays the active closer
1851        // so the leftover can match an earlier opener (e.g. `***foo*` keeps
1852        // `**`).
1853    }
1854
1855    // Adjacent text nodes can appear where unmatched delimiter runs ended up
1856    // beside literal text (`**foo*bar*` -> `**foo` + emphasis). CommonMark
1857    // coalesces them as the final step; do the same for the spans we created.
1858    let mut nodes = nodes.into_vec();
1859    merge_adjacent_text(&mut nodes);
1860    (nodes, deepest)
1861}
1862
1863/// The markers that pair through nested roles, in `openers_bottom` order.
1864const NESTED_MARKERS: usize = 4;
1865
1866/// The nearest live opener before `closer_idx` that the nested closer there can
1867/// pair with, searching no lower than its `openers_bottom` bound.
1868///
1869/// A closer that can also open does not close across a `==` span enclosing it:
1870/// it searches no lower than that span's opener, bounded per span by
1871/// `span_bottom` the way `openers_bottom` bounds the whole list.
1872fn nested_opener(
1873    delimiters: &[DelimMarker],
1874    links: &DelimiterLinks,
1875    openers_bottom: &[Option<usize>],
1876    span_bottom: &[Vec<(usize, usize)>],
1877    closer_idx: usize,
1878) -> Option<usize> {
1879    let closer = &delimiters[closer_idx];
1880    let key = openers_bottom_key(closer);
1881    let mut bottom = openers_bottom[key];
1882    if let Some(span) = span_floor(closer) {
1883        let span_floor = span_bottom
1884            .get(span)
1885            .and_then(|bounds| bounds.iter().find(|(k, _)| *k == key))
1886            .map_or(span + 1, |&(_, bound)| bound);
1887        bottom = bottom.max(Some(span_floor));
1888    }
1889    let mut search = links.prev[closer_idx];
1890    while search != NIL {
1891        if bottom.is_some_and(|bottom| search < bottom) {
1892            return None;
1893        }
1894        let candidate = &delimiters[search];
1895        if candidate.marker == closer.marker
1896            && candidate.can_open
1897            && emphasis_delimiters_match(candidate, closer)
1898        {
1899            return Some(search);
1900        }
1901        search = links.prev[search];
1902    }
1903    None
1904}
1905
1906/// The enclosing mark span a nested closer may not close across: only a run
1907/// that can also open is held inside it.
1908fn span_floor(closer: &DelimMarker) -> Option<usize> {
1909    (closer.can_open && closer.enclosed_from != NIL).then_some(closer.enclosed_from)
1910}
1911
1912/// How many characters a pair consumes from each run, and the node it forms.
1913fn pair_shape(opener: &DelimMarker, closer: &DelimMarker) -> (usize, EmphasisWrap) {
1914    match closer.marker {
1915        // Strikethrough consumes the whole `~~` run on each side.
1916        b'~' => (2, EmphasisWrap::Delete),
1917        b'=' => (2, EmphasisWrap::Mark),
1918        marker => {
1919            let delimiter = if marker == b'_' {
1920                EmphasisDelimiter::Underscore
1921            } else {
1922                EmphasisDelimiter::Asterisk
1923            };
1924            if opener.length >= 2 && closer.length >= 2 {
1925                (2, EmphasisWrap::Strong(delimiter))
1926            } else {
1927                (1, EmphasisWrap::Emphasis(delimiter))
1928            }
1929        }
1930    }
1931}
1932
1933/// Merges consecutive `Text` nodes in a list, recursing into the emphasis and
1934/// mark nodes produced at this level. Other containers were already finalized
1935/// by their own `parse_inlines` pass and are left untouched.
1936fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
1937    let mut write = 0;
1938    for read in 0..nodes.len() {
1939        if read != write {
1940            nodes.swap(read, write);
1941        }
1942        if write > 0 {
1943            let (head, tail) = nodes.split_at_mut(write);
1944            if let (Inline::Text(previous), Inline::Text(current)) =
1945                (&mut head[write - 1], &tail[0])
1946            {
1947                previous.value.push_str(&current.value);
1948                if let (Some(previous_span), Some(current_span)) =
1949                    (previous.meta.span.as_mut(), current.meta.span)
1950                {
1951                    previous_span.end = current_span.end;
1952                }
1953                continue;
1954            }
1955        }
1956        write += 1;
1957    }
1958    nodes.truncate(write);
1959
1960    for node in nodes.iter_mut() {
1961        match node {
1962            Inline::Emphasis(node) => merge_adjacent_text(&mut node.children),
1963            Inline::Strong(node) => merge_adjacent_text(&mut node.children),
1964            Inline::Delete(node) => merge_adjacent_text(&mut node.children),
1965            Inline::Mark(node) => merge_adjacent_text(&mut node.children),
1966            _ => {}
1967        }
1968    }
1969}
1970
1971/// Index into `openers_bottom` for a nested closer's (marker, both-flags,
1972/// length % 3) key.
1973fn openers_bottom_key(closer: &DelimMarker) -> usize {
1974    let marker = match closer.marker {
1975        b'_' => 1,
1976        b'~' => 2,
1977        b'=' => 3,
1978        _ => 0,
1979    };
1980    let both = usize::from(closer.can_open && closer.can_close);
1981    // The key holds what `emphasis_delimiters_match` reads of the closer: the
1982    // whole run's length for the rule of three on `*` / `_`, and what is left
1983    // of the run for the other marks.
1984    let length = match closer.marker {
1985        b'*' | b'_' => closer.run_length,
1986        _ => closer.length,
1987    };
1988    let modulo = length % 3;
1989    ((marker * 2) + both) * 3 + modulo
1990}
1991
1992/// Nested opener/closer compatibility, including CommonMark's rule of three.
1993fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
1994    match opener.marker {
1995        // GFM strikethrough: only runs of exactly two `~` are recorded, and a
1996        // pair consumes both whole, so any two live runs match. The rule of
1997        // three does not apply to `~`.
1998        b'~' => true,
1999        // `==` pairs two characters from each run.
2000        b'=' => opener.length >= 2 && closer.length >= 2,
2001        _ => {
2002            // Rule of three: if either delimiter can both open and close, the
2003            // sum of the lengths of the runs containing them must not be a
2004            // multiple of three, unless both are themselves multiples of three.
2005            // CommonMark counts whole runs, not what earlier pairings left.
2006            let opener_both = opener.can_open && opener.can_close;
2007            let closer_both = closer.can_open && closer.can_close;
2008            if opener_both || closer_both {
2009                let (opener_run, closer_run) = (opener.run_length, closer.run_length);
2010                let sum = opener_run + closer_run;
2011                if sum % 3 == 0 && !(opener_run % 3 == 0 && closer_run % 3 == 0) {
2012                    return false;
2013                }
2014            }
2015            true
2016        }
2017    }
2018}
2019
2020/// The node a matched delimiter pair collapses into.
2021#[derive(Clone, Copy)]
2022enum EmphasisWrap {
2023    Emphasis(EmphasisDelimiter),
2024    Strong(EmphasisDelimiter),
2025    Delete,
2026    Mark,
2027}
2028
2029impl EmphasisWrap {
2030    /// Whether the node counts toward `MAX_INLINE_NESTING` rather than
2031    /// `MAX_EMPHASIS_NESTING`.
2032    fn is_mark(self) -> bool {
2033        matches!(self, Self::Mark)
2034    }
2035}
2036
2037/// Wraps the nodes between two delimiter runs into the node `wrap` names and
2038/// consumes `used` characters from each side.
2039fn apply_emphasis(
2040    nodes: &mut InlineList,
2041    delimiters: &mut [DelimMarker],
2042    opener_idx: usize,
2043    closer_idx: usize,
2044    used: usize,
2045    wrap: EmphasisWrap,
2046) {
2047    let opener_node = delimiters[opener_idx].node_index;
2048    let closer_node = delimiters[closer_idx].node_index;
2049
2050    // A span covers exactly its consumed delimiters and what they enclose: the
2051    // last `used` characters left in the opener's text node through the first
2052    // `used` left in the closer's.
2053    let span_start = nodes
2054        .node_mut(opener_node)
2055        .span()
2056        .map(|span| span.end - used);
2057    let span_end = nodes
2058        .node_mut(closer_node)
2059        .span()
2060        .map(|span| span.start + used);
2061    // Trim the consumed characters from the opener's text node (right side) and
2062    // the closer's text node (left side), updating their recorded lengths/spans.
2063    trim_delimiter_text_tail(nodes.node_mut(opener_node), used);
2064    trim_delimiter_text_head(nodes.node_mut(closer_node), used);
2065    delimiters[opener_idx].length -= used;
2066    let closer = &mut delimiters[closer_idx];
2067    closer.length -= used;
2068    closer.position += used;
2069
2070    // The wrapped children are the nodes strictly between the opener and closer
2071    // text nodes.
2072    let meta = NodeMeta::new(
2073        span_start
2074            .zip(span_end)
2075            .map(|(start, end)| Span::new(start, end)),
2076    );
2077    nodes.wrap_between(opener_node, closer_node, |children| match wrap {
2078        EmphasisWrap::Strong(delimiter) => Inline::Strong(Strong {
2079            meta,
2080            delimiter,
2081            children,
2082        }),
2083        EmphasisWrap::Emphasis(delimiter) => Inline::Emphasis(Emphasis {
2084            meta,
2085            delimiter,
2086            children,
2087        }),
2088        EmphasisWrap::Delete => Inline::Delete(Delete { meta, children }),
2089        EmphasisWrap::Mark => Inline::Mark(Mark { meta, children }),
2090    });
2091
2092    // Drop any placeholder text node that has been fully consumed so leftover
2093    // delimiters never survive as literal text.
2094    if delimiters[closer_idx].length == 0 {
2095        nodes.remove(closer_node);
2096    }
2097    if delimiters[opener_idx].length == 0 {
2098        nodes.remove(opener_node);
2099    }
2100}
2101
2102/// What a `[`-family opener can become when its `]` arrives.
2103#[derive(Clone, Copy, PartialEq, Eq)]
2104enum BracketKind {
2105    Link,
2106    Image,
2107    InlineFootnote,
2108}
2109
2110/// A `[`, `![`, or `^[` waiting for its `]`. Its `[` is a placeholder text
2111/// node; an image's `!` and an inline footnote's `^` sit in the node before it.
2112struct BracketOpener {
2113    kind: BracketKind,
2114    /// The placeholder text node holding the `[`.
2115    node_index: usize,
2116    /// Offset of the opener's first character (`!`, `^`, or `[`) in the input.
2117    position: usize,
2118    /// `delimiters.len()` when the opener was pushed: later runs are in its
2119    /// label.
2120    delimiter_bottom: usize,
2121}
2122
2123/// What a `]` resolves a link-kind opener to.
2124enum LinkTarget {
2125    Resource(ParsedLinkResource),
2126    Reference {
2127        identifier: String,
2128        kind: ReferenceKind,
2129    },
2130}
2131
2132/// Matches what follows the `]` at `close` of a link or image whose label is
2133/// `input[label_start..close]`: an inline `(…)` resource, a full or collapsed
2134/// reference, or a shortcut reference to a defined label. A `(…)` that is not a
2135/// valid resource leaves the reference forms to try, for images and links
2136/// alike. Returns the end of the construct and its target.
2137fn match_link_target(
2138    scan: &mut InlineScan,
2139    input: &str,
2140    label_start: usize,
2141    close: usize,
2142    definitions: Option<Labels<'_>>,
2143) -> Option<(usize, LinkTarget)> {
2144    let label = &input[label_start..close];
2145    let after = close + 1;
2146    if input.as_bytes().get(after) == Some(&b'(') {
2147        match parse_link_resource(&mut scan.lookups, input, after) {
2148            Some((end, resource, _)) => return Some((end, LinkTarget::Resource(resource))),
2149            // A present-but-invalid `(...)` resource is not an inline link or
2150            // image, but CommonMark still resolves `[label]` as a shortcut
2151            // reference and leaves the invalid `(...)` as literal text (links
2152            // 568).
2153            None => {}
2154        }
2155    }
2156    // A `[` that opens no reference label leaves `[label]` a shortcut
2157    // reference followed by literal text.
2158    let reference_close = if input.as_bytes().get(after) == Some(&b'[') {
2159        scan.reference_label_end(after)
2160    } else {
2161        None
2162    };
2163    if let Some(reference_close) = reference_close {
2164        let reference = &input[after + 1..reference_close];
2165        let identifier = if reference.is_empty() {
2166            label
2167        } else {
2168            reference
2169        };
2170        // A present `[...]` second label that resolves to no definition is
2171        // not a link, and CommonMark does not fall back to treating the first
2172        // label as a shortcut (`[x][ ]`, `[x][undef]` stay literal).
2173        return definition_exists(definitions, identifier).then(|| {
2174            let kind = if reference.is_empty() {
2175                ReferenceKind::Collapsed
2176            } else {
2177                ReferenceKind::Full
2178            };
2179            (
2180                reference_close + 1,
2181                LinkTarget::Reference {
2182                    identifier: identifier.into(),
2183                    kind,
2184                },
2185            )
2186        });
2187    }
2188    definition_exists(definitions, label).then(|| {
2189        (
2190            after,
2191            LinkTarget::Reference {
2192                identifier: label.into(),
2193                kind: ReferenceKind::Shortcut,
2194            },
2195        )
2196    })
2197}
2198
2199/// Builds the link or image node for a matched target over `children`.
2200fn link_node(target: LinkTarget, image: bool, span: Span, mut children: Vec<Inline>) -> Inline {
2201    let meta = NodeMeta::new(Some(span));
2202    if !image {
2203        demote_links(&mut children);
2204    }
2205    match (target, image) {
2206        (LinkTarget::Resource(resource), false) => Inline::Link(Link {
2207            meta,
2208            destination: resource.destination,
2209            destination_kind: resource.destination_kind,
2210            title: resource.title,
2211            children,
2212        }),
2213        (LinkTarget::Resource(resource), true) => Inline::Image(Image {
2214            meta,
2215            destination: resource.destination,
2216            destination_kind: resource.destination_kind,
2217            title: resource.title,
2218            alt: children,
2219        }),
2220        (LinkTarget::Reference { identifier, kind }, false) => {
2221            Inline::LinkReference(LinkReference {
2222                meta,
2223                identifier: normalize_label(&identifier),
2224                label: identifier,
2225                kind,
2226                children,
2227            })
2228        }
2229        (LinkTarget::Reference { identifier, kind }, true) => {
2230            Inline::ImageReference(ImageReference {
2231                meta,
2232                identifier: normalize_label(&identifier),
2233                label: identifier,
2234                kind,
2235                alt: children,
2236            })
2237        }
2238    }
2239}
2240
2241/// An autolink written in `form` as `text`, spanning `span`.
2242fn autolink_node(form: AutolinkForm, span: Span, text: &str) -> Inline {
2243    Inline::Autolink(Autolink {
2244        meta: NodeMeta::new(Some(span)),
2245        form,
2246        text: text.into(),
2247    })
2248}
2249
2250/// Link text holds no links: autolinks inside it, the only links a formed
2251/// link's text can hold, read as their text, an angle-bracket autolink
2252/// without its brackets. Image alt text inside it keeps its own links.
2253fn demote_links(nodes: &mut Vec<Inline>) {
2254    for node in nodes.iter_mut() {
2255        match node {
2256            Inline::Autolink(autolink) => {
2257                *node = Inline::Text(Text {
2258                    meta: autolink.meta.clone(),
2259                    value: core::mem::take(&mut autolink.text),
2260                });
2261            }
2262            Inline::Emphasis(node) => demote_links(&mut node.children),
2263            Inline::Strong(node) => demote_links(&mut node.children),
2264            Inline::Delete(node) => demote_links(&mut node.children),
2265            Inline::Mark(node) => demote_links(&mut node.children),
2266            Inline::InlineFootnote(node) => demote_links(&mut node.children),
2267            Inline::TextDirective(node) => demote_links(&mut node.label),
2268            Inline::Image(node) => demote_links(&mut node.alt),
2269            Inline::ImageReference(node) => demote_links(&mut node.alt),
2270            _ => {}
2271        }
2272    }
2273    merge_adjacent_text(nodes);
2274}
2275
2276/// The open brackets of one inline pass and the runs and nodes they enclose.
2277struct Brackets {
2278    openers: Vec<BracketOpener>,
2279    /// Openers below this stack index can no longer form links: a link formed
2280    /// after them.
2281    link_floor: usize,
2282    /// How many `[` were left as text for want of nesting room and are still
2283    /// open: the next that many `]` close them, as text.
2284    overflow: usize,
2285    /// Formed link, image, footnote, and directive nodes with their nesting
2286    /// depth, by node index, so spans around them count it.
2287    formed: Vec<(usize, usize)>,
2288}
2289
2290impl Brackets {
2291    fn new() -> Self {
2292        Self {
2293            openers: Vec::new(),
2294            link_floor: 0,
2295            overflow: 0,
2296            formed: Vec::new(),
2297        }
2298    }
2299
2300    /// Whether a `[` is open: pushed and not yet closed by a `]`, as a link or
2301    /// image opener or as text left for want of nesting room. An inline
2302    /// footnote's `^[` opens no link text. The stack is at most the nesting
2303    /// limit deep.
2304    fn open(&self) -> bool {
2305        self.overflow > 0
2306            || self
2307                .openers
2308                .iter()
2309                .any(|opener| !matches!(opener.kind, BracketKind::InlineFootnote))
2310    }
2311
2312    /// Keeps every open `[` from forming a link around what follows.
2313    fn close_links(&mut self) {
2314        self.link_floor = self.openers.len();
2315    }
2316}
2317
2318/// Takes the nodes and runs after `opener` out of the pass, pairs the runs
2319/// among themselves, and returns the resolved label content with its nesting
2320/// depth.
2321fn take_label(
2322    input: &str,
2323    nodes: &mut Vec<Inline>,
2324    delimiters: &mut Vec<DelimMarker>,
2325    brackets: &mut Brackets,
2326    opener: &BracketOpener,
2327    depth: usize,
2328) -> (Vec<Inline>, usize) {
2329    let first = opener.node_index + 1;
2330    let children: Vec<Inline> = nodes.drain(first..).collect();
2331    let mut runs = delimiters.split_off(opener.delimiter_bottom);
2332    for run in &mut runs {
2333        run.node_index -= first;
2334    }
2335    let split = brackets.formed.partition_point(|&(node, _)| node < first);
2336    let formed: Vec<(usize, usize)> = brackets
2337        .formed
2338        .split_off(split)
2339        .into_iter()
2340        .map(|(node, marks)| (node - first, marks))
2341        .collect();
2342    assign_emphasis_roles(input, &mut runs);
2343    process_emphasis(children, runs, depth, &formed)
2344}
2345
2346/// Removes `count` trailing delimiter characters from a placeholder text node.
2347fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
2348    if let Inline::Text(text) = node {
2349        let new_len = text.value.len().saturating_sub(count);
2350        text.value.truncate(new_len);
2351        if let Some(span) = text.meta.span.as_mut() {
2352            span.end = span.end.saturating_sub(count);
2353        }
2354    }
2355}
2356
2357/// Removes `count` leading delimiter characters from a placeholder text node.
2358/// The node holds one repeated marker byte, so dropping from the front leaves
2359/// the same text as truncating the back.
2360fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
2361    if let Inline::Text(text) = node {
2362        let count = count.min(text.value.len());
2363        text.value.truncate(text.value.len() - count);
2364        if let Some(span) = text.meta.span.as_mut() {
2365            span.start += count;
2366        }
2367    }
2368}
2369
2370/// Forward lookups the inline scanners ask of one input. `DirectLookups`
2371/// answers each with a fresh scan, for callers that ask once; `InlineLookups`
2372/// memoizes the answers for the many questions one inline pass asks. Both
2373/// answer identically.
2374trait Lookups {
2375    /// The first occurrence of `pattern` that starts at or after `from`.
2376    fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize>;
2377    /// The first run of exactly `len` backticks at or after `start`, which is
2378    /// not inside a backtick run.
2379    fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize>;
2380    /// The first unescaped `<` or `>`, or line break, at or after `from`.
2381    fn angle_destination_stop(&mut self, from: usize) -> Option<usize>;
2382    /// The first `$` not right after a `\` at or after `from`.
2383    fn unescaped_dollar(&mut self, from: usize) -> Option<usize>;
2384}
2385
2386struct DirectLookups<'a> {
2387    input: &'a str,
2388}
2389
2390impl Lookups for DirectLookups<'_> {
2391    fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2392        self.input[from..].find(pattern).map(|offset| from + offset)
2393    }
2394
2395    fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2396        find_code_span_close(self.input, start, len)
2397    }
2398
2399    fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2400        (from..self.input.len()).find(|index| is_angle_destination_stop(self.input, *index))
2401    }
2402
2403    fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2404        (from..self.input.len()).find(|index| is_unescaped_dollar(self.input, *index))
2405    }
2406}
2407
2408/// The patterns `InlineLookups::find` keeps position tables for.
2409const CACHED_PATTERNS: [&str; 8] = [">", "-->", "?>", "]]>", "\"", "'", "`$", "$$"];
2410
2411/// `Lookups` over one inline input, each answered from a table built on first
2412/// use.
2413struct InlineLookups<'a> {
2414    input: &'a str,
2415    patterns: [Positions; CACHED_PATTERNS.len()],
2416    backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
2417    angle_destination_stops: Positions,
2418    unescaped_dollars: Positions,
2419}
2420
2421impl<'a> InlineLookups<'a> {
2422    fn new(input: &'a str) -> Self {
2423        Self {
2424            input,
2425            patterns: Default::default(),
2426            backtick_runs: None,
2427            angle_destination_stops: Positions::default(),
2428            unescaped_dollars: Positions::default(),
2429        }
2430    }
2431}
2432
2433impl Lookups for InlineLookups<'_> {
2434    fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2435        let input = self.input;
2436        let Some(slot) = CACHED_PATTERNS.iter().position(|cached| *cached == pattern) else {
2437            return DirectLookups { input }.find(pattern, from);
2438        };
2439        self.patterns[slot].first_at_or_after(from, || pattern_starts(input, pattern))
2440    }
2441
2442    fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2443        let input = self.input;
2444        let runs = self
2445            .backtick_runs
2446            .get_or_insert_with(|| backtick_runs_by_length(input));
2447        let starts = runs.get(&len)?;
2448        starts
2449            .get(starts.partition_point(|run| *run < start))
2450            .copied()
2451    }
2452
2453    fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2454        let input = self.input;
2455        self.angle_destination_stops.first_at_or_after(from, || {
2456            (0..input.len())
2457                .filter(|index| is_angle_destination_stop(input, *index))
2458                .collect()
2459        })
2460    }
2461
2462    fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2463        let input = self.input;
2464        self.unescaped_dollars.first_at_or_after(from, || {
2465            (0..input.len())
2466                .filter(|index| is_unescaped_dollar(input, *index))
2467                .collect()
2468        })
2469    }
2470}
2471
2472/// Start offsets of every maximal backtick run, grouped by run length.
2473fn backtick_runs_by_length(input: &str) -> BTreeMap<usize, Vec<usize>> {
2474    let mut runs: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
2475    let mut index = 0;
2476    while index < input.len() {
2477        if input.as_bytes()[index] == b'`' {
2478            let len = backtick_run_len(input, index);
2479            runs.entry(len).or_default().push(index);
2480            index += len;
2481        } else {
2482            index += 1;
2483        }
2484    }
2485    runs
2486}
2487
2488fn backtick_run_len(input: &str, index: usize) -> usize {
2489    input.as_bytes()[index..]
2490        .iter()
2491        .take_while(|byte| **byte == b'`')
2492        .count()
2493}
2494
2495/// A `$` that single-dollar math reads as a delimiter: one right after a `\`
2496/// is content, whether or not that `\` is itself escaped.
2497fn is_unescaped_dollar(input: &str, index: usize) -> bool {
2498    let bytes = input.as_bytes();
2499    bytes[index] == b'$' && (index == 0 || bytes[index - 1] != b'\\')
2500}
2501
2502fn is_angle_destination_stop(input: &str, index: usize) -> bool {
2503    match input.as_bytes()[index] {
2504        b'<' | b'>' => !is_escaped_at(input, index),
2505        b'\n' | b'\r' => true,
2506        _ => false,
2507    }
2508}
2509
2510/// Everything one `parse_inline_content` pass memoizes about its input, so its
2511/// questions about closing delimiters cost amortized linear time in total.
2512struct InlineScan<'a> {
2513    input: &'a str,
2514    lookups: InlineLookups<'a>,
2515    label_ends: BracketMemo,
2516    reference_label_ends: PathMemo,
2517    wikilink_closes: PathMemo,
2518    directive_attribute_closes: PathMemo,
2519    literal_autolinks: LiteralAutolinkScan,
2520}
2521
2522impl<'a> InlineScan<'a> {
2523    fn new(input: &'a str) -> Self {
2524        Self {
2525            input,
2526            lookups: InlineLookups::new(input),
2527            label_ends: BracketMemo::default(),
2528            reference_label_ends: PathMemo::default(),
2529            wikilink_closes: PathMemo::default(),
2530            directive_attribute_closes: PathMemo::default(),
2531            literal_autolinks: LiteralAutolinkScan::default(),
2532        }
2533    }
2534
2535    /// `find_link_label_end`, memoized.
2536    fn link_label_end(&mut self, open: usize) -> Option<usize> {
2537        let input = self.input;
2538        if input.as_bytes().get(open) != Some(&b'[') {
2539            return None;
2540        }
2541        let lookups = &mut self.lookups;
2542        self.label_ends
2543            .resolve(input.len() + 1, open + 1, |cursor| {
2544                link_label_step(lookups, input, cursor)
2545            })
2546    }
2547
2548    /// `find_reference_label_end`, memoized.
2549    fn reference_label_end(&mut self, open: usize) -> Option<usize> {
2550        let input = self.input;
2551        if input.as_bytes().get(open) != Some(&b'[') {
2552            return None;
2553        }
2554        let close = self
2555            .reference_label_ends
2556            .resolve(input.len() + 1, open + 1, |cursor| {
2557                reference_label_step(input, cursor)
2558            })?;
2559        reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
2560    }
2561
2562    /// The `]]` closing the wikilink whose content starts at `start`; it must
2563    /// be on the same line.
2564    fn wikilink_close(&mut self, start: usize) -> Option<usize> {
2565        let input = self.input;
2566        self.wikilink_closes
2567            .resolve(input.len() + 1, start, |cursor| {
2568                wikilink_close_step(input, cursor)
2569            })
2570    }
2571
2572    /// `find_directive_attributes_close`, memoized.
2573    fn directive_attributes_close(&mut self, open: usize) -> Option<usize> {
2574        let input = self.input;
2575        if input.as_bytes().get(open) != Some(&b'{') {
2576            return None;
2577        }
2578        self.directive_attribute_closes.resolve(
2579            (input.len() + 1) * DIRECTIVE_QUOTE_STATES,
2580            (open + 1) * DIRECTIVE_QUOTE_STATES,
2581            |node| directive_attributes_step(input, node),
2582        )
2583    }
2584}
2585
2586/// Parses the inline content `input`, whose source map is `map`. The inline
2587/// parser works in `input`'s coordinates; the spans it produces, on nodes and
2588/// on the diagnostics it pushes, are translated to the input afterwards.
2589fn parse_inlines(
2590    input: &str,
2591    map: &SourceMap,
2592    definitions: Option<Labels<'_>>,
2593    diagnostics: &mut Vec<Diagnostic>,
2594    tokens: Option<&mut Vec<InlineToken>>,
2595) -> Vec<Inline> {
2596    // Definitions are collected from the block structure alone.
2597    if definitions.is_none() {
2598        return Vec::new();
2599    }
2600    let first_diagnostic = diagnostics.len();
2601    let mut nodes = parse_inlines_with_context(
2602        input,
2603        0,
2604        definitions,
2605        diagnostics,
2606        &mut InlineState::default(),
2607    );
2608    tags::split_tags(input, &mut nodes);
2609    if let Some(tokens) = tokens {
2610        *tokens = tokens::tokens(input, &nodes);
2611        source_map::translate_tokens(map, tokens);
2612    }
2613    source_map::translate_inlines(map, &mut nodes, &mut diagnostics[first_diagnostic..]);
2614    nodes
2615}
2616
2617/// The deepest inline nesting the parser builds. Directive labels parse their
2618/// content one inline pass deeper; within a pass, open brackets and every
2619/// emphasis or mark span each add a level. Content past this depth stays
2620/// literal text, so recursion and the native stack it uses stay bounded.
2621const MAX_INLINE_NESTING: usize = 32;
2622
2623/// State shared by every inline parse nested under one block-level inline
2624/// parse. All nested inputs are slices of that one input, addressed by their
2625/// start offset in it; `parse_inlines` maps the result to the original input.
2626#[derive(Default)]
2627struct InlineState {
2628    /// How many inline parses enclose the current one.
2629    depth: usize,
2630}
2631
2632/// What one inline pass threads through its construct parsers: the state it
2633/// shares with enclosing passes and the memoized scans of its own input.
2634struct InlinePass<'p, 'a> {
2635    state: &'p mut InlineState,
2636    scan: InlineScan<'a>,
2637}
2638
2639fn parse_inlines_with_context(
2640    input: &str,
2641    base_offset: usize,
2642    definitions: Option<Labels<'_>>,
2643    diagnostics: &mut Vec<Diagnostic>,
2644    state: &mut InlineState,
2645) -> Vec<Inline> {
2646    if state.depth >= MAX_INLINE_NESTING {
2647        if input.is_empty() {
2648            return Vec::new();
2649        }
2650        return vec![Inline::Text(Text {
2651            meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + input.len()))),
2652            value: input.into(),
2653        })];
2654    }
2655    state.depth += 1;
2656    let nodes = parse_inline_content(input, base_offset, definitions, diagnostics, state);
2657    state.depth -= 1;
2658    nodes
2659}
2660
2661fn parse_inline_content(
2662    input: &str,
2663    base_offset: usize,
2664    definitions: Option<Labels<'_>>,
2665    diagnostics: &mut Vec<Diagnostic>,
2666    state: &mut InlineState,
2667) -> Vec<Inline> {
2668    let bytes = input.as_bytes();
2669    let mut nodes = Vec::new();
2670    let mut text_start = 0;
2671    let mut text = String::new();
2672    let mut index = 0;
2673    // Emphasis and mark delimiters are resolved with a delimiter stack after the
2674    // scan completes. During the scan we emit each candidate delimiter run as a
2675    // literal text node and record it here so `process_emphasis` can rewrite the
2676    // flat node list into emphasis and mark nodes (or leave it as text).
2677    let mut delimiters: Vec<DelimMarker> = Vec::new();
2678    let mut brackets = Brackets::new();
2679    let mut pass = InlinePass {
2680        state,
2681        scan: InlineScan::new(input),
2682    };
2683    // A literal autolink holds `://`, `www.`, or an email's `@`; content
2684    // without any of them is not scanned for one at each position.
2685    let literal_autolinks = bytes.contains(&b'@')
2686        || input.contains("://")
2687        || bytes
2688            .windows(4)
2689            .any(|window| window.eq_ignore_ascii_case(b"www."));
2690
2691    while index < bytes.len() {
2692        if bytes[index] == b'\\' {
2693            if let Some((next_index, char)) = next_char(input, index + 1) {
2694                if char.is_ascii_punctuation() {
2695                    flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2696                    nodes.push(Inline::Escape(Escape {
2697                        meta: NodeMeta::new(Some(Span::new(
2698                            base_offset + index,
2699                            base_offset + next_index,
2700                        ))),
2701                        value: char,
2702                    }));
2703                    index = next_index;
2704                    text_start = index;
2705                    continue;
2706                }
2707            }
2708        }
2709
2710        if bytes[index] == b'&' {
2711            if let Some((end, _)) = parse_character_reference(input, index) {
2712                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2713                nodes.push(Inline::CharacterReference(CharacterReference {
2714                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2715                    reference: input[index..end].into(),
2716                }));
2717                index = end;
2718                text_start = index;
2719                continue;
2720            }
2721        }
2722
2723        if bytes[index] == b'\n' {
2724            // Only a literal backslash, not one an escape or a reference wrote,
2725            // makes the line ending a hard break.
2726            if index > 0
2727                && bytes[index - 1] == b'\\'
2728                && !is_escaped_at(input, index - 1)
2729                && text.ends_with('\\')
2730            {
2731                text.pop();
2732                flush_text(
2733                    &mut nodes,
2734                    &mut text,
2735                    text_start,
2736                    base_offset + index.saturating_sub(1),
2737                );
2738                nodes.push(Inline::LineBreak(LineBreak {
2739                    meta: NodeMeta::new(Some(Span::new(
2740                        base_offset + index.saturating_sub(1),
2741                        base_offset + index + 1,
2742                    ))),
2743                    kind: LineBreakKind::Backslash,
2744                }));
2745                index += 1;
2746                text_start = index;
2747                continue;
2748            }
2749            // The spaces and tabs ending the line: a hard break or stripped.
2750            let trailing_spaces = trailing_space_count(&text);
2751            if is_hard_break_suffix(&text, trailing_spaces) {
2752                text.truncate(text.len() - trailing_spaces);
2753                flush_text(
2754                    &mut nodes,
2755                    &mut text,
2756                    text_start,
2757                    base_offset + index.saturating_sub(trailing_spaces),
2758                );
2759                nodes.push(Inline::LineBreak(LineBreak {
2760                    meta: NodeMeta::new(Some(Span::new(
2761                        base_offset + index.saturating_sub(trailing_spaces),
2762                        base_offset + index + 1,
2763                    ))),
2764                    kind: LineBreakKind::Spaces,
2765                }));
2766                index += 1;
2767                text_start = index;
2768                continue;
2769            }
2770            // Stripped spaces and tabs that no text holds, as after another
2771            // inline, are read as part of the soft break.
2772            let stripped_alone = trailing_spaces > 0 && text.len() == trailing_spaces;
2773            if trailing_spaces > 0 {
2774                text.truncate(text.len() - trailing_spaces);
2775            }
2776            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2777            let start = if stripped_alone {
2778                index - trailing_spaces
2779            } else {
2780                index
2781            };
2782            nodes.push(Inline::SoftBreak(SoftBreak {
2783                meta: NodeMeta::new(Some(Span::new(
2784                    base_offset + start,
2785                    base_offset + index + 1,
2786                ))),
2787            }));
2788            index += 1;
2789            text_start = index;
2790            continue;
2791        }
2792
2793        if bytes[index] == b'`' {
2794            if let Some((end, value)) = parse_code_span_with(&mut pass.scan.lookups, input, index) {
2795                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2796                nodes.push(Inline::Code(CodeInline {
2797                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2798                    value,
2799                }));
2800                index = end;
2801                text_start = index;
2802                continue;
2803            } else {
2804                // No matching-length close for this opening backtick run:
2805                // CommonMark renders the whole run as literal text. Consume the
2806                // entire run here so the loop does not advance one byte and retry
2807                // a shorter sub-run that could spuriously match a shorter close
2808                // (```foo`` stayed a phantom 2-backtick code span).
2809                let run = bytes[index..]
2810                    .iter()
2811                    .take_while(|byte| **byte == b'`')
2812                    .count();
2813                if text.is_empty() {
2814                    text_start = base_offset + index;
2815                }
2816                for _ in 0..run {
2817                    text.push('`');
2818                }
2819                index += run;
2820                continue;
2821            }
2822        }
2823
2824        if bytes[index] == b'*' && starts_delimiter_run(input, index, b'*') {
2825            let run_len = delimiter_byte_run_len(input, index, b'*');
2826            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2827            push_delimiter(
2828                &mut nodes,
2829                &mut delimiters,
2830                index,
2831                base_offset,
2832                b'*',
2833                run_len,
2834            );
2835            index += run_len;
2836            text_start = index;
2837            continue;
2838        }
2839
2840        // Core `_` emphasis/strong is resolved by the delimiter stack, just like
2841        // `*`.
2842        if bytes[index] == b'_' && starts_delimiter_run(input, index, b'_') {
2843            // A leading `_` can begin a GFM email local part (`_a@b.c`); try the
2844            // literal autolink before recording the `_` as an emphasis
2845            // delimiter, otherwise the `_` would be consumed and the email would
2846            // wrongly start one char later (where its left boundary fails).
2847            if literal_autolinks {
2848                if let Some((end, _)) = parse_literal_autolink(
2849                    input,
2850                    index,
2851                    &mut pass.scan.literal_autolinks,
2852                    !brackets.open(),
2853                ) {
2854                    flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2855                    nodes.push(autolink_node(
2856                        AutolinkForm::Literal,
2857                        Span::new(base_offset + index, base_offset + end),
2858                        &input[index..end],
2859                    ));
2860                    index = end;
2861                    text_start = index;
2862                    continue;
2863                }
2864            }
2865            let run_len = delimiter_byte_run_len(input, index, b'_');
2866            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2867            push_delimiter(
2868                &mut nodes,
2869                &mut delimiters,
2870                index,
2871                base_offset,
2872                b'_',
2873                run_len,
2874            );
2875            index += run_len;
2876            text_start = index;
2877            continue;
2878        }
2879
2880        if bytes[index] == b'=' {
2881            let run_len = delimiter_byte_run_len(input, index, b'=');
2882            if run_len >= 2 {
2883                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2884                push_delimiter(
2885                    &mut nodes,
2886                    &mut delimiters,
2887                    index,
2888                    base_offset,
2889                    b'=',
2890                    run_len,
2891                );
2892                index += run_len;
2893                text_start = index;
2894                continue;
2895            }
2896        }
2897
2898        let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
2899
2900        // `^[` opens an inline footnote.
2901        if bracket_room && bytes[index] == b'^' && bytes.get(index + 1) == Some(&b'[') {
2902            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2903            push_text(&mut nodes, base_offset + index, "^");
2904            push_text(&mut nodes, base_offset + index + 1, "[");
2905            brackets.openers.push(BracketOpener {
2906                kind: BracketKind::InlineFootnote,
2907                node_index: nodes.len() - 1,
2908                position: index,
2909                delimiter_bottom: delimiters.len(),
2910            });
2911            index += 2;
2912            text_start = index;
2913            continue;
2914        }
2915
2916        // A run of exactly two `~` may open or close GFM strikethrough.
2917        // The run is measured only from its start, so the bytes of a longer
2918        // run, which go on as text one at a time, are not measured again.
2919        if bytes[index] == b'~' && starts_delimiter_run(input, index, b'~') {
2920            let run_len = delimiter_byte_run_len(input, index, b'~');
2921            if run_len == 2 {
2922                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2923                push_delimiter(
2924                    &mut nodes,
2925                    &mut delimiters,
2926                    index,
2927                    base_offset,
2928                    b'~',
2929                    run_len,
2930                );
2931                index += run_len;
2932                text_start = index;
2933                continue;
2934            }
2935        }
2936
2937        if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') {
2938            // A wikilink at the `[` wins over the image, as it wins over a
2939            // link at a lone `[`, and the `!` makes it an embed; it takes no
2940            // bracket nesting, so it forms past the limit too.
2941            if let Some((end, wikilink)) =
2942                parse_wikilink(input, index + 1, base_offset, &mut pass.scan)
2943            {
2944                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2945                nodes.push(embed(wikilink, base_offset + index));
2946                // A wikilink is a link: no open bracket forms a link around it.
2947                brackets.close_links();
2948                index = end;
2949                text_start = index;
2950                continue;
2951            }
2952        }
2953
2954        if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') && bracket_room {
2955            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2956            push_text(&mut nodes, base_offset + index, "!");
2957            push_text(&mut nodes, base_offset + index + 1, "[");
2958            brackets.openers.push(BracketOpener {
2959                kind: BracketKind::Image,
2960                node_index: nodes.len() - 1,
2961                position: index,
2962                delimiter_bottom: delimiters.len(),
2963            });
2964            index += 2;
2965            text_start = index;
2966            continue;
2967        }
2968
2969        if bytes[index] == b'[' {
2970            if let Some((end, wikilink)) = parse_wikilink(input, index, base_offset, &mut pass.scan)
2971            {
2972                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2973                nodes.push(wikilink);
2974                brackets.close_links();
2975                index = end;
2976                text_start = index;
2977                continue;
2978            }
2979            if bracket_room {
2980                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2981                push_text(&mut nodes, base_offset + index, "[");
2982                brackets.openers.push(BracketOpener {
2983                    kind: BracketKind::Link,
2984                    node_index: nodes.len() - 1,
2985                    position: index,
2986                    delimiter_bottom: delimiters.len(),
2987                });
2988                index += 1;
2989                text_start = index;
2990                continue;
2991            }
2992            brackets.overflow += 1;
2993        }
2994
2995        if bytes[index] == b']' && brackets.overflow > 0 {
2996            // Closes a `[` left as text past the nesting limit.
2997            brackets.overflow -= 1;
2998        } else if bytes[index] == b']' && !brackets.openers.is_empty() {
2999            flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3000            if let Some(end) = close_bracket(
3001                input,
3002                base_offset,
3003                index,
3004                definitions,
3005                &mut nodes,
3006                &mut delimiters,
3007                &mut brackets,
3008                &mut pass,
3009            ) {
3010                index = end;
3011                text_start = index;
3012                continue;
3013            }
3014        }
3015
3016        if bytes[index] == b'$' {
3017            if let Some((end, value, kind)) =
3018                parse_math_inline(&mut pass.scan.lookups, input, index)
3019            {
3020                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3021                nodes.push(Inline::Math(MathInline {
3022                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3023                    value,
3024                    kind,
3025                }));
3026                index = end;
3027                text_start = index;
3028                continue;
3029            }
3030            // A dollar run that opens but finds no exact-length close is emitted
3031            // as literal text in one piece (like a code-span). Skipping the
3032            // whole run prevents re-opening with a shorter marker inside it, so
3033            // `$$$foo$$` stays literal rather than matching `$$foo$$`. A lone
3034            // `$` before a backtick (the code-math form) is a run of 1, so this
3035            // still advances correctly when that form fails.
3036            let run = bytes[index..]
3037                .iter()
3038                .take_while(|byte| **byte == b'$')
3039                .count();
3040            if run > 1 {
3041                if text.is_empty() {
3042                    text_start = base_offset + index;
3043                }
3044                text.push_str(&input[index..index + run]);
3045                index += run;
3046                continue;
3047            }
3048        }
3049
3050        // Inside an open `[` only an email address links; if the bracket
3051        // forms a link, `demote_links` turns it back into text.
3052        if literal_autolinks {
3053            if let Some((end, _)) = parse_literal_autolink(
3054                input,
3055                index,
3056                &mut pass.scan.literal_autolinks,
3057                !brackets.open(),
3058            ) {
3059                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3060                nodes.push(autolink_node(
3061                    AutolinkForm::Literal,
3062                    Span::new(base_offset + index, base_offset + end),
3063                    &input[index..end],
3064                ));
3065                index = end;
3066                text_start = index;
3067                continue;
3068            }
3069        }
3070
3071        if bytes[index] == b'<' {
3072            if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
3073                let uri = &input[index + 1..end - 1];
3074                if is_angle_autolink(uri) {
3075                    flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3076                    nodes.push(autolink_node(
3077                        AutolinkForm::Angle,
3078                        Span::new(base_offset + index, base_offset + end),
3079                        uri,
3080                    ));
3081                    index = end;
3082                    text_start = index;
3083                    continue;
3084                }
3085            }
3086            if let Some(end) = html_inline_end(&mut pass.scan.lookups, input, index) {
3087                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3088                nodes.push(Inline::Html(HtmlInline {
3089                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3090                    value: input[index..end].into(),
3091                }));
3092                index = end;
3093                text_start = index;
3094                continue;
3095            }
3096        }
3097
3098        if bytes[index] == b':' {
3099            if let Some((end, name)) = parse_shortcode(input, index) {
3100                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3101                nodes.push(Inline::Shortcode(Shortcode {
3102                    meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3103                    name,
3104                }));
3105                index = end;
3106                text_start = index;
3107                continue;
3108            }
3109        }
3110
3111        // A directive counts one nesting level for itself, on top of the
3112        // enclosing passes and open brackets.
3113        if bytes[index] == b':' && pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING {
3114            // The label parses one pass deeper than the open brackets around
3115            // it.
3116            pass.state.depth += brackets.openers.len();
3117            let directive = parse_text_directive(
3118                input,
3119                index,
3120                base_offset,
3121                definitions,
3122                diagnostics,
3123                &mut pass,
3124            );
3125            pass.state.depth -= brackets.openers.len();
3126            if let Some((end, directive)) = directive {
3127                flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3128                if let Inline::TextDirective(node) = &directive {
3129                    // A link inside the directive label keeps open brackets
3130                    // from forming links around it.
3131                    if contains_link_inline(&node.label) {
3132                        brackets.close_links();
3133                    }
3134                    let depth = inline_nesting(&node.label) + 1;
3135                    brackets.formed.push((nodes.len(), depth));
3136                }
3137                nodes.push(directive);
3138                index = end;
3139                text_start = index;
3140                continue;
3141            }
3142        }
3143
3144        let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
3145        if text.is_empty() {
3146            text_start = base_offset + index;
3147        }
3148        text.push(char);
3149        index = next_index;
3150    }
3151
3152    flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
3153    assign_emphasis_roles(input, &mut delimiters);
3154    process_emphasis(nodes, delimiters, pass.state.depth - 1, &brackets.formed).0
3155}
3156
3157/// A `:name:` shortcode at `index`: `name` is in the gemoji table, and the
3158/// source chars directly outside the colons are no Unicode letter or digit,
3159/// so clock times and words joined by a colon stay text.
3160fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
3161    if input[index..].starts_with("::") {
3162        return None;
3163    }
3164    if input[..index]
3165        .chars()
3166        .next_back()
3167        .is_some_and(char::is_alphanumeric)
3168    {
3169        return None;
3170    }
3171
3172    let mut cursor = index + 1;
3173    while let Some((next, char)) = next_char(input, cursor) {
3174        if char == ':' {
3175            let name = &input[index + 1..cursor];
3176            if crate::gemoji::glyph(name).is_none()
3177                || input[next..]
3178                    .chars()
3179                    .next()
3180                    .is_some_and(char::is_alphanumeric)
3181            {
3182                return None;
3183            }
3184            return Some((next, name.into()));
3185        }
3186        if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
3187            return None;
3188        }
3189        cursor = next;
3190    }
3191    None
3192}
3193
3194fn parse_wikilink(
3195    input: &str,
3196    index: usize,
3197    base_offset: usize,
3198    scan: &mut InlineScan,
3199) -> Option<(usize, Inline)> {
3200    if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
3201    {
3202        return None;
3203    }
3204
3205    let close = scan.wikilink_close(index + 2)?;
3206    let source = &input[index + 2..close];
3207    if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
3208        return None;
3209    }
3210
3211    let (target_source, label_source) = match find_wikilink_separator(source) {
3212        Some(separator) => (&source[..separator], &source[separator + 1..]),
3213        None => (source, source),
3214    };
3215
3216    // Target and label keep their escapes and character references as
3217    // written.
3218    if target_source.is_empty() {
3219        return None;
3220    }
3221    let target = String::from(target_source);
3222    let label = String::from(label_source);
3223    let end = close + 2;
3224    Some((
3225        end,
3226        Inline::WikiLink(WikiLink {
3227            meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3228            target,
3229            label,
3230            embed: false,
3231        }),
3232    ))
3233}
3234
3235/// `wikilink` marked as an embed by the `!` at `bang`, where its span starts.
3236fn embed(mut wikilink: Inline, bang: usize) -> Inline {
3237    if let Inline::WikiLink(node) = &mut wikilink {
3238        node.embed = true;
3239        node.meta.span = node.meta.span.map(|span| Span::new(bang, span.end));
3240    }
3241    wikilink
3242}
3243
3244/// One step of the walk to a wikilink's closing `]]`, which must be on the
3245/// opener's line. The content holds no unescaped `[` or `]`.
3246fn wikilink_close_step(input: &str, cursor: usize) -> Step {
3247    let bytes = input.as_bytes();
3248    match bytes.get(cursor) {
3249        None | Some(b'\n' | b'\r' | b'[') => Step::Done(None),
3250        Some(b'\\') => {
3251            let escaped = cursor + 1;
3252            Step::Next(next_char(input, escaped).map_or(escaped, |(after_escape, _)| after_escape))
3253        }
3254        Some(b']') if bytes.get(cursor + 1) == Some(&b']') => Step::Done(Some(cursor)),
3255        Some(b']') => Step::Done(None),
3256        Some(_) => Step::Next(next_char(input, cursor).map_or(input.len(), |(next, _)| next)),
3257    }
3258}
3259
3260fn find_wikilink_separator(input: &str) -> Option<usize> {
3261    let bytes = input.as_bytes();
3262    let mut cursor = 0;
3263    while cursor < input.len() {
3264        match bytes[cursor] {
3265            b'\\' => {
3266                cursor += 1;
3267                if cursor < input.len() {
3268                    cursor = next_char(input, cursor)?.0;
3269                }
3270            }
3271            b'|' => return Some(cursor),
3272            _ => cursor = next_char(input, cursor)?.0,
3273        }
3274    }
3275    None
3276}
3277
3278fn trailing_space_count(input: &str) -> usize {
3279    input
3280        .as_bytes()
3281        .iter()
3282        .rev()
3283        .take_while(|byte| matches!(**byte, b' ' | b'\t'))
3284        .count()
3285}
3286
3287fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
3288    // A hard line break is two or more spaces immediately before the newline
3289    // with no intervening tab; a tab anywhere in the trailing whitespace run
3290    // demotes it to a soft break.
3291    let bytes = input.as_bytes();
3292    trailing >= 2
3293        && bytes[bytes.len() - trailing..]
3294            .iter()
3295            .all(|byte| *byte == b' ')
3296}
3297
3298/// The walk `InlineScan::reference_label_end` and `definition_label` take in
3299/// steps, run once from the start; the scan tests hold them to it.
3300#[cfg(test)]
3301fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
3302    // A reference/definition link label does not nest: it ends at the first
3303    // unescaped `]`, and an unescaped interior `[` disqualifies it.
3304    if input.as_bytes().get(open) != Some(&b'[') {
3305        return None;
3306    }
3307    let close = path_walk(open + 1, |cursor| reference_label_step(input, cursor))?;
3308    reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
3309}
3310
3311/// One step of the walk to a reference label's closing `]`.
3312fn reference_label_step(input: &str, cursor: usize) -> Step {
3313    let Some((next, char)) = next_char(input, cursor) else {
3314        return Step::Done(None);
3315    };
3316    match char {
3317        '\\' => Step::Next(next_char(input, next).map_or(next, |(after_escape, _)| after_escape)),
3318        '[' => Step::Done(None),
3319        ']' => Step::Done(Some(cursor)),
3320        _ => Step::Next(next),
3321    }
3322}
3323
3324/// How many container levels `inlines` nest, counting empty containers.
3325fn inline_nesting(inlines: &[Inline]) -> usize {
3326    let mut deepest = 0;
3327    let mut pending = vec![(inlines, 0)];
3328    while let Some((nodes, depth)) = pending.pop() {
3329        for node in nodes {
3330            if is_inline_container(node) {
3331                deepest = deepest.max(depth + 1);
3332                pending.push((node.children(), depth + 1));
3333            }
3334        }
3335    }
3336    deepest
3337}
3338
3339fn is_inline_container(inline: &Inline) -> bool {
3340    matches!(
3341        inline,
3342        Inline::Emphasis(_)
3343            | Inline::Strong(_)
3344            | Inline::Delete(_)
3345            | Inline::Mark(_)
3346            | Inline::Link(_)
3347            | Inline::Image(_)
3348            | Inline::LinkReference(_)
3349            | Inline::ImageReference(_)
3350            | Inline::InlineFootnote(_)
3351            | Inline::TextDirective(_)
3352    )
3353}
3354
3355/// Whether `inlines` hold a link formed from brackets, at any depth: an
3356/// autolink, which keeps open brackets from nothing, does not count.
3357fn contains_link_inline(inlines: &[Inline]) -> bool {
3358    inlines.iter().any(|inline| {
3359        matches!(
3360            inline,
3361            Inline::Link(_) | Inline::LinkReference(_) | Inline::WikiLink(_)
3362        ) || contains_link_inline(inline.children())
3363    })
3364}
3365
3366fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
3367    if input.as_bytes().get(open) != Some(&b'[') {
3368        return None;
3369    }
3370    let mut lookups = DirectLookups { input };
3371    bracket_walk(open + 1, |cursor| {
3372        link_label_step(&mut lookups, input, cursor)
3373    })
3374}
3375
3376/// One step of the walk to a link label's closing `]`: escapes, code spans,
3377/// autolinks, and inline HTML are stepped over whole, so brackets inside them
3378/// do not count; other brackets nest.
3379fn link_label_step(lookups: &mut impl Lookups, input: &str, cursor: usize) -> BracketStep {
3380    let Some((next, char)) = next_char(input, cursor) else {
3381        return BracketStep::End;
3382    };
3383    match char {
3384        '\\' => {
3385            BracketStep::Pass(next_char(input, next).map_or(next, |(after_escape, _)| after_escape))
3386        }
3387        '`' => BracketStep::Pass(code_span_end(lookups, input, cursor).unwrap_or(next)),
3388        '<' => BracketStep::Pass(
3389            autolink_end(lookups, input, cursor)
3390                .or_else(|| html_inline_end(lookups, input, cursor))
3391                .unwrap_or(next),
3392        ),
3393        '[' => BracketStep::Open(next),
3394        ']' => BracketStep::Close(next),
3395        _ => BracketStep::Pass(next),
3396    }
3397}
3398
3399fn parse_text_directive(
3400    input: &str,
3401    index: usize,
3402    base_offset: usize,
3403    definitions: Option<Labels<'_>>,
3404    diagnostics: &mut Vec<Diagnostic>,
3405    pass: &mut InlinePass,
3406) -> Option<(usize, Inline)> {
3407    if input[index..].starts_with("::") {
3408        return None;
3409    }
3410    if index > 0 {
3411        let previous = input[..index].chars().next_back()?;
3412        if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
3413            return None;
3414        }
3415    }
3416    let opener_offset = index + 1;
3417    let opener = parse_directive_opener_with(
3418        &input[opener_offset..],
3419        |close, open| {
3420            let found = match close {
3421                DirectiveClose::Label => pass.scan.link_label_end(opener_offset + open),
3422                DirectiveClose::Attributes => {
3423                    pass.scan.directive_attributes_close(opener_offset + open)
3424                }
3425            };
3426            found.map(|position| position - opener_offset)
3427        },
3428        |label, attributes, rest| text_directive_ends(label, attributes, rest.bytes().next()),
3429    );
3430    let opener = match opener {
3431        Ok(opener) => opener,
3432        Err(refused) => {
3433            // A `[` or `{` left open after a valid name is malformed; a bad
3434            // name, or an opener refused for what follows it, is plain text.
3435            if refused == Refused::Unclosed {
3436                diagnostics.push(Diagnostic::new(
3437                    DiagnosticSeverity::Error,
3438                    DiagnosticCode::InvalidDirectiveName,
3439                    Span::new(base_offset + index, base_offset + input.len()),
3440                    "text directive opener is malformed",
3441                ));
3442            }
3443            return None;
3444        }
3445    };
3446    let label = opener
3447        .label
3448        .map(|source| {
3449            parse_inlines_with_context(
3450                source,
3451                base_offset + index + 1 + opener.name.len() + 1,
3452                definitions,
3453                diagnostics,
3454                pass.state,
3455            )
3456        })
3457        .unwrap_or_default();
3458    let at = base_offset + opener_offset;
3459    opener.report_dropped(diagnostics, |start, end| Span::new(at + start, at + end));
3460    let DirectiveOpener {
3461        name,
3462        attributes,
3463        consumed,
3464        ..
3465    } = opener;
3466    Some((
3467        index + 1 + consumed,
3468        Inline::TextDirective(TextDirective {
3469            meta: NodeMeta::new(Some(Span::new(
3470                base_offset + index,
3471                base_offset + index + 1 + consumed,
3472            ))),
3473            name,
3474            label,
3475            attributes,
3476        }),
3477    ))
3478}
3479
3480/// Whether a text directive with this label and attribute source ends before
3481/// `next`: whitespace or the end of the content, or ASCII punctuation when the
3482/// label is not empty or the attributes hold a non-space char. So `:e[a].` is
3483/// a directive, while `:word:`, `:a@b.c`, `(:note)`, `:e{}x`, `:e{}.`, and
3484/// `:e[a]b` stay text. Deciding from the sources keeps a refused opener from
3485/// parsing its attributes.
3486fn text_directive_ends(label: Option<&str>, attributes: Option<&str>, next: Option<u8>) -> bool {
3487    match next {
3488        None | Some(b' ' | b'\t' | b'\n' | b'\r') => true,
3489        Some(byte) if byte.is_ascii_punctuation() => {
3490            label.is_some_and(|label| !label.is_empty())
3491                || attributes
3492                    .is_some_and(|source| source.bytes().any(|byte| !byte.is_ascii_whitespace()))
3493        }
3494        Some(_) => false,
3495    }
3496}
3497
3498/// A directive's name, label, and attributes, read from the text after its
3499/// colons.
3500struct DirectiveOpener<'a> {
3501    name: String,
3502    label: Option<&'a str>,
3503    attributes: Vec<DirectiveAttribute>,
3504    /// The ranges of the attributes dropped for having no valid name.
3505    dropped: Vec<(usize, usize)>,
3506    /// The bytes the opener takes.
3507    consumed: usize,
3508}
3509
3510impl DirectiveOpener<'_> {
3511    /// A warning for each dropped attribute, spanning `span(start, end)`.
3512    fn report_dropped(
3513        &self,
3514        diagnostics: &mut Vec<Diagnostic>,
3515        span: impl Fn(usize, usize) -> Span,
3516    ) {
3517        for &(start, end) in &self.dropped {
3518            diagnostics.push(Diagnostic::new(
3519                DiagnosticSeverity::Warning,
3520                DiagnosticCode::InvalidDirectiveAttribute,
3521                span(start, end),
3522                "directive attribute without a valid name is dropped",
3523            ));
3524        }
3525    }
3526}
3527
3528/// Why `parse_directive_opener_with` read no opener.
3529#[derive(Clone, Copy, PartialEq, Eq)]
3530enum Refused {
3531    /// The text after the colons does not start with a valid directive name.
3532    Name,
3533    /// The `[` or `{` after the name has no closing `]` or `}`.
3534    Unclosed,
3535    /// `accept` refused the opener for its label, attributes, or what follows.
3536    Follow,
3537}
3538
3539/// `parse_directive_opener_with` for a block directive line, finding closing
3540/// brackets in `input` alone.
3541fn parse_directive_opener<'a>(
3542    input: &'a str,
3543    accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3544) -> Result<DirectiveOpener<'a>, Refused> {
3545    parse_directive_opener_with(
3546        input,
3547        |close, open| match close {
3548            DirectiveClose::Label => find_link_label_end(input, open),
3549            DirectiveClose::Attributes => find_directive_attributes_close(input, open),
3550        },
3551        accept,
3552    )
3553}
3554
3555/// Which closing position `parse_directive_opener_with` asks its finder for.
3556#[derive(Clone, Copy)]
3557enum DirectiveClose {
3558    /// The `]` closing the `[label]` opening at the given position.
3559    Label,
3560    /// The `}` closing the `{attributes}` opening at the given position.
3561    Attributes,
3562}
3563
3564/// Reads the directive opener at the start of `input`, the text after the
3565/// colons: a name, then an optional `[label]`, then optional `{attributes}`.
3566/// `find_close` gives the `]` or `}` closing the bracket at a position.
3567///
3568/// `accept` sees the label source, the attribute source, and the text after
3569/// the opener, and runs before the attributes are parsed. That order keeps
3570/// refused openers linear-time: `:a{` repeated, then `}x`, refuses every
3571/// opener without parsing the attribute source each one spans to the end.
3572/// The cost is that acceptance reads the attribute source, not the parsed
3573/// attributes: `:e{!}.` is a text directive, since its attribute source is not
3574/// blank, whose invalid attribute is dropped, so it is written `:e.`, which
3575/// reads back as text.
3576fn parse_directive_opener_with<'a>(
3577    input: &'a str,
3578    mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
3579    accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3580) -> Result<DirectiveOpener<'a>, Refused> {
3581    let mut index = 0;
3582    while let Some((next, char)) = next_char(input, index) {
3583        if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
3584            index = next;
3585        } else {
3586            break;
3587        }
3588    }
3589    let name = &input[..index];
3590    if !is_directive_name(name) {
3591        return Err(Refused::Name);
3592    }
3593
3594    let mut label = None;
3595    let mut attribute_source = None;
3596    let mut consumed = index;
3597    if input.as_bytes().get(consumed) == Some(&b'[') {
3598        let close = find_close(DirectiveClose::Label, consumed).ok_or(Refused::Unclosed)?;
3599        label = Some(&input[consumed + 1..close]);
3600        consumed = close + 1;
3601    }
3602    if input.as_bytes().get(consumed) == Some(&b'{') {
3603        let close = find_close(DirectiveClose::Attributes, consumed).ok_or(Refused::Unclosed)?;
3604        attribute_source = Some((consumed + 1, &input[consumed + 1..close]));
3605        consumed = close + 1;
3606    }
3607    if !accept(
3608        label,
3609        attribute_source.map(|(_, source)| source),
3610        &input[consumed..],
3611    ) {
3612        return Err(Refused::Follow);
3613    }
3614    let (attributes, dropped) = match attribute_source {
3615        Some((start, source)) => {
3616            let (attributes, mut dropped) = parse_attributes(source);
3617            for range in &mut dropped {
3618                *range = (range.0 + start, range.1 + start);
3619            }
3620            (attributes, dropped)
3621        }
3622        None => (Vec::new(), Vec::new()),
3623    };
3624
3625    Ok(DirectiveOpener {
3626        name: name.into(),
3627        label,
3628        attributes,
3629        dropped,
3630        consumed,
3631    })
3632}
3633
3634/// Quote states of the walk to a directive's closing `}`: outside quotes,
3635/// inside `"…"`, inside `'…'`.
3636const DIRECTIVE_QUOTE_STATES: usize = 3;
3637
3638fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
3639    if input.as_bytes().get(open) != Some(&b'{') {
3640        return None;
3641    }
3642    path_walk((open + 1) * DIRECTIVE_QUOTE_STATES, |node| {
3643        directive_attributes_step(input, node)
3644    })
3645}
3646
3647/// One step of the walk to a directive's closing `}`, over nodes
3648/// `byte position * DIRECTIVE_QUOTE_STATES + quote state`: a `\` escapes the
3649/// next byte, and a `}` inside quotes does not close.
3650fn directive_attributes_step(input: &str, node: usize) -> Step {
3651    let bytes = input.as_bytes();
3652    let (cursor, quote) = (node / DIRECTIVE_QUOTE_STATES, node % DIRECTIVE_QUOTE_STATES);
3653    let at = |position: usize, quote: usize| Step::Next(position * DIRECTIVE_QUOTE_STATES + quote);
3654    let Some(&byte) = bytes.get(cursor) else {
3655        return Step::Done(None);
3656    };
3657    if byte == b'\\' {
3658        return at((cursor + 2).min(bytes.len()), quote);
3659    }
3660    match (quote, byte) {
3661        (0, b'"') => at(cursor + 1, 1),
3662        (0, b'\'') => at(cursor + 1, 2),
3663        (0, b'}') => Step::Done(Some(cursor)),
3664        (1, b'"') | (2, b'\'') => at(cursor + 1, 0),
3665        _ => at(cursor + 1, quote),
3666    }
3667}
3668
3669/// The attributes in `input`, the text between a directive's braces, and the
3670/// ranges of `input` holding attributes dropped for having no valid name.
3671fn parse_attributes(input: &str) -> (Vec<DirectiveAttribute>, Vec<(usize, usize)>) {
3672    let mut attributes = Vec::new();
3673    let mut dropped = Vec::new();
3674    let mut cursor = 0;
3675    while cursor < input.len() {
3676        cursor = skip_spaces(input, cursor);
3677        if cursor >= input.len() {
3678            break;
3679        }
3680
3681        if input.as_bytes().get(cursor) == Some(&b'#') {
3682            let (id, next) = parse_attribute_token(input, cursor + 1);
3683            if !id.is_empty() {
3684                attributes.push(DirectiveAttribute {
3685                    name: "id".into(),
3686                    value: Some(id.into()),
3687                });
3688            }
3689            cursor = next;
3690            continue;
3691        }
3692
3693        if input.as_bytes().get(cursor) == Some(&b'.') {
3694            let (class, next) = parse_attribute_token(input, cursor + 1);
3695            if !class.is_empty() {
3696                attributes.push(DirectiveAttribute {
3697                    name: "class".into(),
3698                    value: Some(class.into()),
3699                });
3700            }
3701            cursor = next;
3702            continue;
3703        }
3704
3705        let token_start = cursor;
3706        let (name, next) = parse_attribute_name(input, cursor);
3707        if name.is_empty() {
3708            dropped.push((token_start, input.trim_end().len().max(token_start)));
3709            break;
3710        }
3711        let mut token_end = next;
3712        cursor = skip_spaces(input, next);
3713        let value = if input.as_bytes().get(cursor) == Some(&b'=') {
3714            cursor = skip_spaces(input, cursor + 1);
3715            token_end = cursor;
3716            if let Some((value, next)) = parse_attribute_value(input, cursor) {
3717                cursor = next;
3718                token_end = next;
3719                Some(value)
3720            } else {
3721                Some(String::new())
3722            }
3723        } else {
3724            None
3725        };
3726        // A token that is no attribute name is dropped, as a malformed value
3727        // is read leniently: the document keeps only attributes it can write.
3728        if crate::validate::is_attribute_name(name) {
3729            attributes.push(DirectiveAttribute {
3730                name: name.into(),
3731                value,
3732            });
3733        } else {
3734            dropped.push((token_start, token_end));
3735        }
3736    }
3737    (attributes, dropped)
3738}
3739
3740fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
3741    let mut cursor = index;
3742    while let Some((next, char)) = next_char(input, cursor) {
3743        if char.is_whitespace() {
3744            break;
3745        }
3746        cursor = next;
3747    }
3748    (&input[index..cursor], cursor)
3749}
3750
3751fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
3752    let mut cursor = index;
3753    while let Some((next, char)) = next_char(input, cursor) {
3754        if char.is_whitespace() || char == '=' {
3755            break;
3756        }
3757        cursor = next;
3758    }
3759    (&input[index..cursor], cursor)
3760}
3761
3762fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
3763    let quote = input.as_bytes().get(index).copied();
3764    if matches!(quote, Some(b'"' | b'\'')) {
3765        let quote = quote?;
3766        let mut cursor = index + 1;
3767        while cursor < input.len() {
3768            let (next, char) = next_char(input, cursor)?;
3769            if char as u8 == quote && !is_escaped_at(input, cursor) {
3770                return Some((
3771                    decode_escapes_and_references(&input[index + 1..cursor]),
3772                    next,
3773                ));
3774            }
3775            cursor = next;
3776        }
3777        return None;
3778    }
3779
3780    let (value, next) = parse_attribute_token(input, index);
3781    Some((
3782        decode_selected_escapes_and_references(value, |char| matches!(char, '\\' | '&')),
3783        next,
3784    ))
3785}
3786
3787/// The end and the normalized value of the code span opening at `index`.
3788fn parse_code_span_with(
3789    lookups: &mut impl Lookups,
3790    input: &str,
3791    index: usize,
3792) -> Option<(usize, String)> {
3793    let len = backtick_run_len(input, index);
3794    let search_start = index + len;
3795    let close = lookups.code_span_close(search_start, len)?;
3796    Some((
3797        close + len,
3798        normalize_code_span(&input[search_start..close]),
3799    ))
3800}
3801
3802/// The end of the code span opening at `index`, as `parse_code_span` finds it.
3803fn code_span_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
3804    let len = backtick_run_len(input, index);
3805    Some(lookups.code_span_close(index + len, len)? + len)
3806}
3807
3808fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
3809    let bytes = input.as_bytes();
3810    let mut cursor = start;
3811    while cursor < bytes.len() {
3812        if bytes[cursor] != b'`' {
3813            cursor = next_char(input, cursor)
3814                .map(|(next, _)| next)
3815                .unwrap_or(bytes.len());
3816            continue;
3817        }
3818        let run_len = bytes[cursor..]
3819            .iter()
3820            .take_while(|byte| **byte == b'`')
3821            .count();
3822        if run_len == marker_len {
3823            return Some(cursor);
3824        }
3825        cursor += run_len;
3826    }
3827    None
3828}
3829
3830fn normalize_code_span(input: &str) -> String {
3831    let (before, after) = code_padding(input);
3832    let mut normalized = String::new();
3833    let content = &input[before..input.len() - after];
3834    let mut cursor = 0;
3835    while cursor < content.len() {
3836        let (next, char) = next_char(content, cursor).expect("valid UTF-8 byte index");
3837        if char == '\r' {
3838            if content.as_bytes().get(next) == Some(&b'\n') {
3839                cursor = next + 1;
3840            } else {
3841                cursor = next;
3842            }
3843            normalized.push(' ');
3844            continue;
3845        }
3846        if char == '\n' {
3847            normalized.push(' ');
3848            cursor = next;
3849            continue;
3850        }
3851        normalized.push(char);
3852        cursor = next;
3853    }
3854    normalized
3855}
3856
3857/// The bytes of padding a code span or `$` math drops from each end of
3858/// `input`, its content: one space or line ending at each end, when both ends
3859/// hold one and the content is not all spaces and line endings.
3860pub(crate) fn code_padding(input: &str) -> (usize, usize) {
3861    let space = |text: &str| -> Option<usize> {
3862        match text.as_bytes().first()? {
3863            b' ' | b'\n' => Some(1),
3864            b'\r' if text.as_bytes().get(1) == Some(&b'\n') => Some(2),
3865            b'\r' => Some(1),
3866            _ => None,
3867        }
3868    };
3869    let end_space = |text: &str| -> Option<usize> {
3870        match text.as_bytes().last()? {
3871            b' ' | b'\r' => Some(1),
3872            b'\n' if text.len() >= 2 && text.as_bytes()[text.len() - 2] == b'\r' => Some(2),
3873            b'\n' => Some(1),
3874            _ => None,
3875        }
3876    };
3877    match (space(input), end_space(input)) {
3878        (Some(before), Some(after))
3879            if input
3880                .bytes()
3881                .any(|byte| !matches!(byte, b' ' | b'\n' | b'\r')) =>
3882        {
3883            (before, after)
3884        }
3885        _ => (0, 0),
3886    }
3887}
3888
3889fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3890    delimiter_flanking(input, index, marker_len).left
3891}
3892
3893fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3894    delimiter_flanking(input, index, marker_len).right
3895}
3896
3897/// Whether a run of `marker` bytes starts at `index`: the byte before it is
3898/// not an unescaped `marker`. Only the one byte is looked at, so asking at
3899/// every byte of a long run stays linear.
3900fn starts_delimiter_run(input: &str, index: usize, marker: u8) -> bool {
3901    index == 0 || input.as_bytes()[index - 1] != marker || is_escaped_at(input, index - 1)
3902}
3903
3904fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
3905    let bytes = input.as_bytes();
3906    let mut cursor = index;
3907    while bytes.get(cursor) == Some(&marker) {
3908        cursor += 1;
3909    }
3910    cursor - index
3911}
3912
3913#[derive(Clone, Copy)]
3914struct DelimiterFlanking {
3915    left: bool,
3916    right: bool,
3917    previous: Option<char>,
3918    next: Option<char>,
3919    /// The char before the run, read past a variation selector, is
3920    /// punctuation (a "Unicode punctuation sequence").
3921    previous_punctuation: bool,
3922    next_punctuation: bool,
3923}
3924
3925/// How one side of a delimiter run reads for flanking.
3926#[derive(Clone, Copy, Default)]
3927struct FlankingSide {
3928    whitespace: bool,
3929    punctuation: bool,
3930    cjk: bool,
3931    ideographic_variation_selector: bool,
3932}
3933
3934impl FlankingSide {
3935    fn of(value: Option<char>) -> Self {
3936        let Some(value) = value else {
3937            return Self {
3938                whitespace: true,
3939                ..Self::default()
3940            };
3941        };
3942        Self {
3943            whitespace: value.is_whitespace(),
3944            punctuation: is_flanking_punctuation(value),
3945            cjk: crate::unicode_cjk::is_cjk(value),
3946            ideographic_variation_selector: crate::unicode_cjk::is_ideographic_variation_selector(
3947                value,
3948            ),
3949        }
3950    }
3951
3952    /// The side before a run whose previous char is `previous` and, before
3953    /// that, `before_previous`: a general-use variation selector reads as the
3954    /// char it follows, unless that is whitespace or absent, where it reads as
3955    /// a plain character (unspecified by the amendments), and `‘’“”` with
3956    /// U+FE01 read as CJK punctuation, as micromark-extension-cjk-friendly
3957    /// reads them.
3958    fn before(previous: Option<char>, before_previous: impl FnOnce() -> Option<char>) -> Self {
3959        match previous {
3960            Some(selector) if crate::unicode_cjk::is_general_use_variation_selector(selector) => {
3961                match before_previous() {
3962                    Some(base) if !base.is_whitespace() => {
3963                        if selector == '\u{FE01}' && matches!(base, '‘' | '’' | '“' | '”') {
3964                            return Self {
3965                                punctuation: true,
3966                                cjk: true,
3967                                ..Self::default()
3968                            };
3969                        }
3970                        Self {
3971                            ideographic_variation_selector: false,
3972                            ..Self::of(Some(base))
3973                        }
3974                    }
3975                    _ => Self::default(),
3976                }
3977            }
3978            _ => Self::of(previous),
3979        }
3980    }
3981}
3982
3983fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
3984    delimiter_flanking_within(input, index, marker_len, false, false)
3985}
3986
3987/// `delimiter_flanking` for a run that may sit right inside a mark span: a
3988/// bounded side flanks as if the input ended there, the way the span's content
3989/// reads on its own.
3990///
3991/// Left- and right-flanking follow the CommonMark CJK-friendly amendments
3992/// (<https://github.com/tats-u/markdown-cjk-friendly/blob/5b17173/specification.md>,
3993/// section 6.2), in the reduced form of the amendments' implementers' tips:
3994/// a run is left-flanking when not followed by whitespace and either not
3995/// followed by punctuation, or preceded by whitespace or punctuation, or with
3996/// a CJK char on either side or an ideographic variation selector before it;
3997/// right-flanking is the mirror, without the selector.
3998fn delimiter_flanking_within(
3999    input: &str,
4000    index: usize,
4001    marker_len: usize,
4002    bounded_before: bool,
4003    bounded_after: bool,
4004) -> DelimiterFlanking {
4005    let previous = if bounded_before {
4006        None
4007    } else {
4008        input[..index].chars().next_back().map(source_char)
4009    };
4010    let next = if bounded_after {
4011        None
4012    } else {
4013        input[index + marker_len..].chars().next().map(source_char)
4014    };
4015
4016    let before = FlankingSide::before(previous, || {
4017        let mut chars = input[..index].chars();
4018        chars.next_back();
4019        chars.next_back().map(source_char)
4020    });
4021    let after = FlankingSide::of(next);
4022
4023    let left = !after.whitespace
4024        && (!after.punctuation
4025            || before.whitespace
4026            || before.punctuation
4027            || after.cjk
4028            || before.cjk
4029            || before.ideographic_variation_selector);
4030    let right = !before.whitespace
4031        && (!before.punctuation
4032            || after.whitespace
4033            || after.punctuation
4034            || before.cjk
4035            || after.cjk);
4036
4037    DelimiterFlanking {
4038        left,
4039        right,
4040        previous,
4041        next,
4042        previous_punctuation: before.punctuation,
4043        next_punctuation: after.punctuation,
4044    }
4045}
4046
4047/// Dollar-fenced inline math, GitHub Flavored Markdown dialect.
4048///
4049/// A `$` is a flanking delimiter resolved at scan time (math is not pushed onto
4050/// the emphasis delimiter stack). An opening run of one or two `$` (runs of
4051/// three or more never form math) scans forward for a matching closing run:
4052///
4053/// * single `$`: cannot open if the next char is ASCII whitespace; the closing
4054///   `$` cannot be preceded by ASCII whitespace nor followed by an ASCII digit;
4055///   a `\$` inside is skipped (the backslash is kept verbatim, never a
4056///   delimiter); the close must be a run of exactly one `$`.
4057/// * double `$$`: no flanking and no digit guard; closes on the next run of two
4058///   `$`; content is kept verbatim and may span newlines (this is still an
4059///   inline display span — `$$` flow blocks are handled by `parse_math_block`).
4060///
4061/// The closing run is matched greedily (the nearest valid close wins), which is
4062/// equivalent to emphasis-style "nearest preceding open" because a failed open
4063/// emits a literal `$`/`$$` and the scan resumes after it. Content for the
4064/// single-`$` form is normalized like a code span (line endings → spaces, one
4065/// edge-space strip); the `$$` display form is verbatim. The `` $`…`$ `` code
4066/// form takes precedence.
4067fn parse_math_inline(
4068    lookups: &mut impl Lookups,
4069    input: &str,
4070    index: usize,
4071) -> Option<(usize, String, MathInlineKind)> {
4072    if let Some((end, value)) = parse_math_code_inline(lookups, input, index) {
4073        return Some((end, value, MathInlineKind::Code));
4074    }
4075
4076    let bytes = input.as_bytes();
4077    let open_dollars = bytes[index..]
4078        .iter()
4079        .take_while(|byte| **byte == b'$')
4080        .count();
4081    // The maximum math fence length is 2 dollars: a run of three or more never
4082    // opens math.
4083    if open_dollars == 0 || open_dollars > 2 {
4084        return None;
4085    }
4086
4087    let content_start = index + open_dollars;
4088    let close = scan_to_closing_dollar(lookups, input, content_start, open_dollars)?;
4089    let content_end = close - open_dollars;
4090    // The span requires `endpos - startpos >= fence_length * 2 + 1`, i.e. at
4091    // least one content byte between the open and close fences.
4092    if content_end <= content_start {
4093        return None;
4094    }
4095
4096    let raw = &input[content_start..content_end];
4097    let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
4098    let kind = MathInlineKind::Dollar { dollars };
4099    let value = if math_is_padded(kind) {
4100        normalize_math_text(raw)
4101    } else {
4102        raw.into()
4103    };
4104    Some((close, value, kind))
4105}
4106
4107/// Whether math of `kind` follows the code-span content rules: line endings
4108/// read as spaces, and padding `code_padding` finds is dropped. Only
4109/// single-dollar math does.
4110pub(crate) fn math_is_padded(kind: MathInlineKind) -> bool {
4111    matches!(kind, MathInlineKind::Dollar { dollars: 1 })
4112}
4113
4114/// Whether `written`, read alone, is exactly one inline math holding `value`
4115/// as `kind`: validation's check that dollar math reads back as written.
4116pub(crate) fn reads_as_inline_math(written: &str, value: &str, kind: MathInlineKind) -> bool {
4117    parse_math_inline(&mut DirectLookups { input: written }, written, 0).is_some_and(
4118        |(end, read, read_kind)| end == written.len() && read == value && read_kind == kind,
4119    )
4120}
4121
4122/// Scans for the closing dollar run. `start` is the first content byte
4123/// (just past the opening run); returns the byte offset just past a matching
4124/// closing run of exactly `open_dollars` `$`.
4125fn scan_to_closing_dollar(
4126    lookups: &mut impl Lookups,
4127    input: &str,
4128    start: usize,
4129    open_dollars: usize,
4130) -> Option<usize> {
4131    let bytes = input.as_bytes();
4132    if open_dollars == 1 {
4133        // A space immediately after a single opening `$` forbids the open.
4134        if bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
4135            return None;
4136        }
4137        // An escaped `\$` is content, not a delimiter (the backslash stays in
4138        // the content verbatim); the first other `$` closes, unless a space
4139        // comes before it or an ASCII digit after it. The lookup keeps a run
4140        // of `\$` from being walked again from each `$` that opens before it.
4141        let close = lookups.unescaped_dollar(start)?;
4142        if is_math_space(bytes[close - 1]) || bytes.get(close + 1).is_some_and(u8::is_ascii_digit) {
4143            return None;
4144        }
4145        return Some(close + 1);
4146    }
4147    // The display form closes at the first `$$`, even the first two `$` of a
4148    // longer run.
4149    lookups.find("$$", start).map(|close| close + 2)
4150}
4151
4152/// Math whitespace: ASCII tab, line feed, carriage return, and space.
4153fn is_math_space(byte: u8) -> bool {
4154    matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
4155}
4156
4157/// Applies the code-span content rules to dollar-fenced math: line endings
4158/// become single spaces, and the padding `code_padding` finds is dropped.
4159fn normalize_math_text(input: &str) -> String {
4160    normalize_code_span(input)
4161}
4162
4163fn parse_math_code_inline(
4164    lookups: &mut impl Lookups,
4165    input: &str,
4166    index: usize,
4167) -> Option<(usize, String)> {
4168    if !input[index..].starts_with("$`") {
4169        return None;
4170    }
4171
4172    let search_start = index + 2;
4173    let close = lookups.find("`$", search_start)?;
4174    if close == search_start {
4175        return None;
4176    }
4177
4178    Some((close + 2, input[search_start..close].into()))
4179}
4180
4181fn parse_link_resource(
4182    lookups: &mut impl Lookups,
4183    input: &str,
4184    open: usize,
4185) -> Option<(usize, ParsedLinkResource, LinkTailRanges)> {
4186    let bytes = input.as_bytes();
4187    if bytes.get(open) != Some(&b'(') {
4188        return None;
4189    }
4190    let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
4191    if bytes.get(cursor) == Some(&b')') {
4192        return Some((
4193            cursor + 1,
4194            ParsedLinkResource {
4195                destination: String::new(),
4196                destination_kind: LinkDestinationKind::Omitted,
4197                title: None,
4198            },
4199            LinkTailRanges {
4200                destination: (cursor, cursor),
4201                title: None,
4202            },
4203        ));
4204    }
4205    if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
4206        let omitted = cursor;
4207        let (title, next) = parse_link_title(input, cursor)?;
4208        let title_range = (cursor + 1, next - 1);
4209        cursor = skip_link_resource_space(input, next)?;
4210        if bytes.get(cursor) == Some(&b')') {
4211            return Some((
4212                cursor + 1,
4213                ParsedLinkResource {
4214                    destination: String::new(),
4215                    destination_kind: LinkDestinationKind::Omitted,
4216                    title: Some(title),
4217                },
4218                LinkTailRanges {
4219                    destination: (omitted, omitted),
4220                    title: Some(title_range),
4221                },
4222            ));
4223        }
4224        return None;
4225    }
4226    let destination_start = cursor;
4227    let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
4228    let destination_range = match destination_kind {
4229        LinkDestinationKind::Angle => (destination_start + 1, next - 1),
4230        _ => (destination_start, next),
4231    };
4232    let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
4233    cursor = after_destination;
4234    if bytes.get(cursor) == Some(&b')') {
4235        return Some((
4236            cursor + 1,
4237            ParsedLinkResource {
4238                destination,
4239                destination_kind,
4240                title: None,
4241            },
4242            LinkTailRanges {
4243                destination: destination_range,
4244                title: None,
4245            },
4246        ));
4247    }
4248    if !had_space {
4249        return None;
4250    }
4251
4252    let (title, next) = parse_link_title(input, cursor)?;
4253    let title_range = (cursor + 1, next - 1);
4254    cursor = skip_link_resource_space(input, next)?;
4255    if bytes.get(cursor) == Some(&b')') {
4256        Some((
4257            cursor + 1,
4258            ParsedLinkResource {
4259                destination,
4260                destination_kind,
4261                title: Some(title),
4262            },
4263            LinkTailRanges {
4264                destination: destination_range,
4265                title: Some(title_range),
4266            },
4267        ))
4268    } else {
4269        None
4270    }
4271}
4272
4273fn parse_link_destination(
4274    lookups: &mut impl Lookups,
4275    input: &str,
4276    index: usize,
4277) -> Option<(String, LinkDestinationKind, usize)> {
4278    if input.as_bytes().get(index) == Some(&b'<') {
4279        let stop = lookups.angle_destination_stop(index + 1)?;
4280        if input.as_bytes()[stop] != b'>' {
4281            return None;
4282        }
4283        return Some((
4284            decode_escapes_and_references(&input[index + 1..stop]),
4285            LinkDestinationKind::Angle,
4286            stop + 1,
4287        ));
4288    }
4289
4290    let mut cursor = index;
4291    let mut depth = 0usize;
4292    while cursor < input.len() {
4293        let (next, char) = next_char(input, cursor)?;
4294        // A bare destination terminates on ASCII space or an ASCII control
4295        // character; Unicode whitespace (e.g. U+00A0) is ordinary. A backslash
4296        // before a space is NOT an escape (only ASCII punctuation is escapable),
4297        // so `\ ` still terminates the destination → `[a](\ b)` is not a link.
4298        if char == ' ' || source_char(char).is_ascii_control() {
4299            break;
4300        }
4301        if char == '(' && !is_escaped_at(input, cursor) {
4302            depth += 1;
4303            // CommonMark caps balanced parens in a bare destination at depth 32.
4304            if depth > 32 {
4305                return None;
4306            }
4307        } else if char == ')' && !is_escaped_at(input, cursor) {
4308            if depth == 0 {
4309                break;
4310            }
4311            depth -= 1;
4312        }
4313        cursor = next;
4314    }
4315
4316    if cursor == index || depth > 0 {
4317        None
4318    } else {
4319        Some((
4320            decode_escapes_and_references(&input[index..cursor]),
4321            LinkDestinationKind::Bare,
4322            cursor,
4323        ))
4324    }
4325}
4326
4327fn parse_link_title(input: &str, index: usize) -> Option<(Title, usize)> {
4328    let opener = input.as_bytes().get(index).copied()?;
4329    let (closer, title_kind) = match opener {
4330        b'"' => ('"', LinkTitleKind::DoubleQuote),
4331        b'\'' => ('\'', LinkTitleKind::SingleQuote),
4332        b'(' => (')', LinkTitleKind::Paren),
4333        _ => return None,
4334    };
4335    let mut cursor = index + 1;
4336    while cursor < input.len() {
4337        let (next, char) = next_char(input, cursor)?;
4338        if char == closer && !is_escaped_at(input, cursor) {
4339            if contains_blank_line(&input[index + 1..cursor]) {
4340                return None;
4341            }
4342            let value = decode_escapes_and_references(&input[index + 1..cursor]);
4343            return Some((Title::new(value, title_kind), next));
4344        }
4345        if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
4346            return None;
4347        }
4348        cursor = next;
4349    }
4350    None
4351}
4352
4353fn contains_blank_line(input: &str) -> bool {
4354    if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
4355        return false;
4356    }
4357    // A title that merely begins or ends with an EOL is allowed; only an INTERIOR
4358    // blank line (a blank line bounded by content on both sides) is rejected. The
4359    // empty first/last line entries that a leading/trailing newline produces are
4360    // boundary artifacts, not blank lines in the title content.
4361    let map = SourceMap::verbatim(input.len(), 0);
4362    let lines = collect_lines(input, &map);
4363    let interior = lines.len().saturating_sub(1);
4364    lines
4365        .iter()
4366        .take(interior)
4367        .skip(1)
4368        .any(|line| is_blank(line.text))
4369}
4370
4371fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
4372    skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
4373}
4374
4375fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
4376    let mut line_breaks = 0usize;
4377    let mut had_space = false;
4378    while input
4379        .as_bytes()
4380        .get(index)
4381        .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
4382    {
4383        had_space = true;
4384        match input.as_bytes()[index] {
4385            b'\n' => {
4386                line_breaks += 1;
4387                if line_breaks > 1 {
4388                    return None;
4389                }
4390                index += 1;
4391            }
4392            b'\r' => {
4393                line_breaks += 1;
4394                if line_breaks > 1 {
4395                    return None;
4396                }
4397                if input.as_bytes().get(index + 1) == Some(&b'\n') {
4398                    index += 2;
4399                } else {
4400                    index += 1;
4401                }
4402            }
4403            _ => index += 1,
4404        }
4405    }
4406    Some((index, had_space))
4407}
4408
4409pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
4410    let bytes = input.as_bytes();
4411    let mut cursor = index;
4412    let mut count = 0;
4413    while cursor > 0 && bytes[cursor - 1] == b'\\' {
4414        count += 1;
4415        cursor -= 1;
4416    }
4417    count % 2 == 1
4418}
4419
4420fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
4421    let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
4422    let (destination, destination_kind, next) =
4423        parse_link_destination(&mut DirectLookups { input }, input, cursor)?;
4424    cursor = next;
4425
4426    let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
4427    cursor = next;
4428    if cursor >= input.len() {
4429        return Some(ParsedLinkResource {
4430            destination,
4431            destination_kind,
4432            title: None,
4433        });
4434    }
4435    if !had_space {
4436        return None;
4437    }
4438
4439    let (title, next) = parse_link_title(input, cursor)?;
4440    let after_title = skip_link_resource_space(input, next)?;
4441    (after_title == input.len()).then_some(ParsedLinkResource {
4442        destination,
4443        destination_kind,
4444        title: Some(title),
4445    })
4446}
4447
4448/// The char that closes the title `source` opens after its destination, when
4449/// no unescaped one follows the opener yet.
4450fn open_definition_title(source: &str) -> Option<char> {
4451    let (cursor, _) = skip_link_resource_space_with_info(source, 0)?;
4452    let (_, _, next) =
4453        parse_link_destination(&mut DirectLookups { input: source }, source, cursor)?;
4454    let (cursor, had_space) = skip_link_resource_space_with_info(source, next)?;
4455    if !had_space {
4456        return None;
4457    }
4458    let closer = match source.as_bytes().get(cursor)? {
4459        b'"' => '"',
4460        b'\'' => '\'',
4461        b'(' => ')',
4462        _ => return None,
4463    };
4464    (!line_may_close_title(&source[cursor + 1..], closer)).then_some(closer)
4465}
4466
4467/// Whether `line` holds an unescaped `closer`, or an unescaped `(` inside a
4468/// parenthesized title, either of which settles the title. A backslash
4469/// escape never spans a line ending, so each line is read alone.
4470fn line_may_close_title(line: &str, closer: char) -> bool {
4471    line.char_indices().any(|(index, char)| {
4472        (char == closer || (closer == ')' && char == '(')) && !is_escaped_at(line, index)
4473    })
4474}
4475
4476fn line_can_start_definition_title(input: &str) -> bool {
4477    let trimmed = trim_ascii_start(input);
4478    matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
4479}
4480
4481fn push_line(output: &mut String, line: &str) {
4482    if !output.is_empty() {
4483        output.push('\n');
4484    }
4485    output.push_str(line);
4486}
4487
4488/// Ends a code or math block's last line when the input ended without a line
4489/// ending: every line of such a block's value ends with one. The added ending
4490/// repeats the value's first line ending, `\n` when it has none.
4491fn end_last_line(value: &mut String) {
4492    if value.is_empty() || ends_with_line_ending(value) {
4493        return;
4494    }
4495    value.push_str(value_line_ending(value));
4496}
4497
4498/// The line ending a code or math value's lines take: its first one, `\n`
4499/// when it has none.
4500fn value_line_ending(value: &str) -> &'static str {
4501    match value.find(['\r', '\n']) {
4502        Some(index) if value[index..].starts_with("\r\n") => "\r\n",
4503        Some(index) if value[index..].starts_with('\r') => "\r",
4504        _ => "\n",
4505    }
4506}
4507
4508fn ends_with_line_ending(input: &str) -> bool {
4509    input.ends_with('\n') || input.ends_with('\r')
4510}
4511
4512fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
4513    if !text.is_empty() {
4514        nodes.push(Inline::Text(Text {
4515            meta: NodeMeta::new(Some(Span::new(text_start, end))),
4516            value: core::mem::take(text),
4517        }));
4518    }
4519}
4520
4521fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
4522    let char = input[index..].chars().next()?;
4523    Some((index + char.len_utf8(), char))
4524}
4525
4526/// A CommonMark "Unicode punctuation character" for emphasis/strong flanking:
4527/// ASCII punctuation plus the non-ASCII Unicode `P*`/`S*` categories. Only the
4528/// flanking classification needs the Unicode set; escape/label logic stays
4529/// ASCII-only via `char::is_ascii_punctuation`.
4530fn is_flanking_punctuation(value: char) -> bool {
4531    value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
4532}
4533
4534/// Fold a reference label to its matching identifier. Per CommonMark, two
4535/// labels match when their RAW source (no backslash unescape, no entity decode)
4536/// agrees after collapsing internal spaces, tabs, and line endings to a single
4537/// space, trimming, and Unicode case-folding (`to_uppercase()` then
4538/// `to_lowercase()`). So `[foo\!]`
4539/// does NOT match `[foo!]`, and `[&copy;]` does NOT match `[©]`.
4540///
4541/// The serializer's `normalize_reference_label` delegates here so the
4542/// Shortcut/Collapsed omission oracle stays in lockstep with this matcher.
4543pub(crate) fn normalize_label(label: &str) -> String {
4544    label
4545        // Unicode full casefold maps capital sharp S (ẞ, U+1E9E) to "ss"; Rust's
4546        // `to_uppercase` leaves it unchanged (it is already uppercase), so without
4547        // this `[ẞ]` would not match a `[SS]: …` definition (links 540). This is
4548        // the only char where `to_uppercase().to_lowercase()` diverges from the
4549        // full casefold that matters for label matching.
4550        .replace('ẞ', "ss")
4551        .replace('\0', "\u{FFFD}")
4552        .split([' ', '\t', '\n', '\r'])
4553        .filter(|word| !word.is_empty())
4554        .collect::<Vec<_>>()
4555        .join(" ")
4556        .to_uppercase()
4557        .to_lowercase()
4558}
4559
4560/// The identifiers a document's references can resolve to: those of its own
4561/// definitions, sorted and deduplicated so that a lookup is a binary search.
4562#[derive(Clone, Copy)]
4563pub(crate) struct Labels<'a> {
4564    pub(crate) own: &'a [String],
4565}
4566
4567fn definition_exists(definitions: Option<Labels<'_>>, label: &str) -> bool {
4568    if label.is_empty() || !reference_label_is_within_limit(label) {
4569        return false;
4570    }
4571    let Some(definitions) = definitions else {
4572        return false;
4573    };
4574    definitions.own.binary_search(&identifier(label)).is_ok()
4575}
4576
4577/// The identifier a reference with `label` resolves to: its normalized form,
4578/// with U+0000 read as U+FFFD as in the definitions a document holds.
4579fn identifier(label: &str) -> String {
4580    let identifier = normalize_label(label);
4581    if identifier.contains('\0') {
4582        identifier.replace('\0', "\u{FFFD}")
4583    } else {
4584        identifier
4585    }
4586}
4587
4588fn reference_label_is_within_limit(label: &str) -> bool {
4589    label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
4590}
4591
4592/// Whether `[label]` reads back as a link reference label holding `label`:
4593/// the walk to its `]` ends at its own `]`, so the label holds no unescaped
4594/// bracket and does not escape its close; it is within the length limit, is
4595/// not blank, and holds no blank line, which would end its paragraph.
4596pub(crate) fn is_reference_label(label: &str) -> bool {
4597    let written = alloc::format!("[{label}]");
4598    path_walk(1, |cursor| reference_label_step(&written, cursor)) == Some(written.len() - 1)
4599        && reference_label_is_within_limit(label)
4600        && !normalize_label(label).is_empty()
4601        && !holds_blank_line(label)
4602}
4603
4604/// Whether `[^label]` reads back as a footnote label holding `label`: a
4605/// footnote label whose `]` it does not escape.
4606pub(crate) fn is_written_footnote_label(label: &str) -> bool {
4607    is_footnote_label(label) && !is_escaped_at(&alloc::format!("{label}]"), label.len())
4608}
4609
4610/// Whether a line of `text` other than its first and its last, which share
4611/// their lines with what is written around `text`, is blank.
4612fn holds_blank_line(text: &str) -> bool {
4613    let text = text.replace("\r\n", "\n");
4614    let lines: Vec<&str> = text.split(['\n', '\r']).collect();
4615    lines.len() > 2 && lines[1..lines.len() - 1].iter().any(|line| is_blank(line))
4616}
4617
4618fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
4619    let (columns, bytes) = leading_indent(input);
4620    if columns <= 3 {
4621        Some(&input[bytes..])
4622    } else {
4623        None
4624    }
4625}
4626
4627/// The marker and length of the code fence that `input` opens: three or more
4628/// backticks or tildes, where a backtick fence's info string holds no backtick.
4629fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
4630    let marker = match input.as_bytes().first()? {
4631        b'`' => FenceMarker::Backtick,
4632        b'~' => FenceMarker::Tilde,
4633        _ => return None,
4634    };
4635    let byte = match marker {
4636        FenceMarker::Backtick => b'`',
4637        FenceMarker::Tilde => b'~',
4638    };
4639    let length = input
4640        .as_bytes()
4641        .iter()
4642        .take_while(|item| **item == byte)
4643        .count();
4644    let opens = length >= 3 && (byte == b'~' || !input[length..].contains('`'));
4645    opens.then_some((marker, length))
4646}
4647
4648fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
4649    let byte = match marker {
4650        FenceMarker::Backtick => b'`',
4651        FenceMarker::Tilde => b'~',
4652    };
4653    let count = input
4654        .as_bytes()
4655        .iter()
4656        .take_while(|item| **item == byte)
4657        .count();
4658    count >= length && is_blank(&input[count..])
4659}
4660
4661fn trim_closing_hashes(input: &str) -> &str {
4662    let input = input.trim_end_matches([' ', '\t']);
4663    let hash_start = input.trim_end_matches('#').len();
4664    if hash_start == input.len() {
4665        return input;
4666    }
4667    if hash_start == 0 {
4668        return "";
4669    }
4670
4671    let before = &input[..hash_start];
4672    if before.ends_with(' ') || before.ends_with('\t') {
4673        before.trim_end_matches([' ', '\t'])
4674    } else {
4675        input
4676    }
4677}
4678
4679/// Whether `input` opens with a list item marker, up to three spaces in.
4680fn opens_list_item(input: &str) -> bool {
4681    trim_up_to_three_spaces(input).is_some_and(|rest| blocks::list_marker_head(rest).is_some())
4682}
4683
4684/// The offset of `slice` inside `text` when it is a borrowed sub-slice of it.
4685fn slice_offset_in(text: &str, slice: &str) -> Option<usize> {
4686    let offset = (slice.as_ptr() as usize).checked_sub(text.as_ptr() as usize)?;
4687    (offset + slice.len() <= text.len()).then_some(offset)
4688}
4689
4690fn leading_indent(input: &str) -> (usize, usize) {
4691    leading_indent_at(input, 0)
4692}
4693
4694/// The columns and bytes of `input`'s leading spaces and tabs, its first
4695/// char at source column `start`, from which tabs reach their stops.
4696fn leading_indent_at(input: &str, start: usize) -> (usize, usize) {
4697    let mut column = start;
4698    let mut bytes = 0usize;
4699    for byte in input.as_bytes() {
4700        match *byte {
4701            b' ' => column += 1,
4702            b'\t' => column += 4 - (column % 4),
4703            _ => break,
4704        }
4705        bytes += 1;
4706    }
4707    (column - start, bytes)
4708}
4709
4710fn task_marker_checked(input: &str) -> Option<bool> {
4711    if input.starts_with("[ ]") {
4712        Some(false)
4713    } else if input.starts_with("[x]") || input.starts_with("[X]") {
4714        Some(true)
4715    } else {
4716        None
4717    }
4718}
4719
4720/// Whether `text` holds only spaces and tabs, as a blank line does.
4721pub(crate) fn is_blank(text: &str) -> bool {
4722    text.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
4723}
4724
4725fn trim_ascii_start(input: &str) -> &str {
4726    input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
4727}
4728
4729fn leading_trim_bytes(input: &str) -> usize {
4730    input.len() - trim_ascii_start(input).len()
4731}
4732
4733fn parse_table_delimiter(input: &str) -> Option<Vec<TableAlignment>> {
4734    // Every cell trims to colons around dashes, so a row with any other char
4735    // is no delimiter row, whatever its cells.
4736    if !input.contains('-')
4737        || !input
4738            .chars()
4739            .all(|char| matches!(char, '|' | '-' | ':' | ' ' | '\t'))
4740    {
4741        return None;
4742    }
4743    let cells = table_row_cell_ranges(input);
4744    if cells.is_empty() {
4745        return None;
4746    }
4747    let mut alignments = Vec::new();
4748    for (start, end) in cells {
4749        alignments.push(table_delimiter_alignment(
4750            input[start..end].trim_matches([' ', '\t']),
4751        )?);
4752    }
4753    Some(alignments)
4754}
4755
4756// A delimiter cell is `:?` `-`+ `:?` once trimmed: colons only at the
4757// boundaries, the dashes contiguous, no interior space or colon.
4758fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
4759    let bytes = cell.as_bytes();
4760    let mut cursor = 0;
4761    let left = bytes.first() == Some(&b':');
4762    if left {
4763        cursor += 1;
4764    }
4765    let dash_start = cursor;
4766    while bytes.get(cursor) == Some(&b'-') {
4767        cursor += 1;
4768    }
4769    if cursor == dash_start {
4770        return None;
4771    }
4772    let right = bytes.get(cursor) == Some(&b':');
4773    if right {
4774        cursor += 1;
4775    }
4776    if cursor != bytes.len() {
4777        return None;
4778    }
4779    Some(match (left, right) {
4780        (true, true) => TableAlignment::Center,
4781        (true, false) => TableAlignment::Left,
4782        (false, true) => TableAlignment::Right,
4783        (false, false) => TableAlignment::None,
4784    })
4785}
4786
4787/// The cell-delimiter pipes of a table row, in order: every `|` that is not
4788/// escaped by an odd backslash run. The cell's inline content reads an
4789/// escaped pipe as `|`.
4790fn table_row_delimiters(row: &str) -> Vec<usize> {
4791    let bytes = row.as_bytes();
4792    let mut delimiters = Vec::new();
4793    let mut cursor = 0;
4794    while cursor < bytes.len() {
4795        match bytes[cursor] {
4796            b'\\' => {
4797                let backslashes = delimiter_byte_run_len(row, cursor, b'\\');
4798                let pipe = cursor + backslashes;
4799                cursor = if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
4800                    pipe + 1
4801                } else {
4802                    pipe
4803                };
4804            }
4805            b'|' => {
4806                delimiters.push(cursor);
4807                cursor += 1;
4808            }
4809            _ => cursor += 1,
4810        }
4811    }
4812    delimiters
4813}
4814
4815/// The byte ranges of `input`'s cells, between the pipes that delimit them.
4816fn table_row_cell_ranges(input: &str) -> Vec<(usize, usize)> {
4817    let trimmed = input.trim_matches([' ', '\t']);
4818    let offset = leading_trim_bytes(input);
4819    let delimiters = table_row_delimiters(trimmed);
4820    let mut cells = Vec::new();
4821    let mut start = 0;
4822    for &pipe in &delimiters {
4823        cells.push((offset + start, offset + pipe));
4824        start = pipe + 1;
4825    }
4826    cells.push((offset + start, offset + trimmed.len()));
4827
4828    // A delimiter at the very start or end (only whitespace after it) is a
4829    // border, not the edge of an empty cell.
4830    if delimiters.first() == Some(&0) {
4831        cells.remove(0);
4832    }
4833    if delimiters
4834        .last()
4835        .is_some_and(|&pipe| is_blank(&trimmed[pipe + 1..]))
4836    {
4837        cells.pop();
4838    }
4839    cells
4840}
4841
4842/// One table cell's inline input (whitespace trimmed, escaped pipes read as
4843/// `|`), its source map, and its span.
4844struct TableCellSource {
4845    text: DerivedText,
4846    span: Span,
4847}
4848
4849/// The cells of `row`, a slice of `line.text`, as `table_row_cell_ranges`
4850/// splits it.
4851fn table_row_cells(line: &Line<'_>, row: &str) -> Vec<TableCellSource> {
4852    let row_offset = slice_offset_in(line.text, row).unwrap_or(0);
4853    table_row_cell_ranges(row)
4854        .into_iter()
4855        .map(|(start, end)| {
4856            let raw = &row[start..end];
4857            let leading = leading_trim_bytes(raw);
4858            let content = raw.trim_matches([' ', '\t']);
4859            let offset = row_offset + start + leading;
4860            let span = Span::new(
4861                line.source_start(offset),
4862                line.source_end(offset + content.len()),
4863            );
4864            let mut text = DerivedText::default();
4865            // GitHub/cmark-gfm reads an odd backslash run before `|` as a
4866            // literal pipe; the run keeps its other backslashes, which the
4867            // inline parser resolves as written. The `|` read from `\|` maps
4868            // to both of its bytes.
4869            let bytes = content.as_bytes();
4870            let mut copied = 0;
4871            let mut cursor = 0;
4872            while cursor < bytes.len() {
4873                if bytes[cursor] == b'\\' {
4874                    let pipe = cursor + delimiter_byte_run_len(content, cursor, b'\\');
4875                    if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
4876                        text.append(line, &content[copied..pipe - 1]);
4877                        text.append_replacing(
4878                            "|",
4879                            line.source_start(offset + pipe - 1),
4880                            line.source_end(offset + pipe + 1),
4881                        );
4882                        copied = pipe + 1;
4883                    }
4884                    cursor = pipe;
4885                } else {
4886                    cursor += 1;
4887                }
4888            }
4889            text.append(line, &content[copied..]);
4890            TableCellSource { text, span }
4891        })
4892        .collect()
4893}
4894
4895fn table_has_separator(header: &str, delimiter: &str) -> bool {
4896    // GFM makes leading/trailing pipes optional, so `parse_table_delimiter` plus
4897    // the header/alignment column-count check usually suffice. The one exception
4898    // is a single resolved column with no disambiguating syntax: `a\n-\nb` has
4899    // matching one-column shapes yet no pipe and no alignment colon, so it is a
4900    // loose paragraph/setext, not a table. A single column still forms a table
4901    // when a pipe appears in the header/delimiter or the delimiter carries an
4902    // explicit alignment colon (`a\n-:`, `a\n:-:`, …).
4903    let Some(alignments) = parse_table_delimiter(delimiter) else {
4904        return true;
4905    };
4906    if alignments.len() == 1 {
4907        return contains_unescaped_pipe(header)
4908            || contains_unescaped_pipe(delimiter)
4909            || delimiter.contains(':');
4910    }
4911    true
4912}
4913
4914// Still used by `block_quote_table_body_row` to detect a table row appearing as
4915// a block-quote continuation line (which DOES require a pipe).
4916fn contains_unescaped_pipe(input: &str) -> bool {
4917    !table_row_delimiters(input).is_empty()
4918}
4919
4920// A GFM footnote definition `[^label]:` is a block boundary: it interrupts a
4921// paragraph and ends a prior footnote's lazy continuation.
4922/// The end of the `<…>` autolink opening at `index`.
4923fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4924    let end = lookups.find(">", index)? + 1;
4925    is_angle_autolink(&input[index + 1..end - 1]).then_some(end)
4926}
4927
4928/// The end of the inline HTML (comment, processing instruction, CDATA,
4929/// declaration, or tag) opening at `index`.
4930fn html_inline_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4931    let rest = &input[index..];
4932    if rest.starts_with("<!--") {
4933        return Some(lookups.find("-->", index)? + 3);
4934    }
4935    // The `?>` closing a processing instruction follows its `<?`.
4936    if rest.starts_with("<?") {
4937        return Some(lookups.find("?>", index + 2)? + 2);
4938    }
4939    if rest.starts_with("<![CDATA[") {
4940        return Some(lookups.find("]]>", index)? + 3);
4941    }
4942    if is_declaration_start(rest) {
4943        return Some(lookups.find(">", index)? + 1);
4944    }
4945    parse_html_tag_with(lookups, input, index).map(|(end, _)| end)
4946}
4947
4948fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
4949    parse_html_tag_with(&mut DirectLookups { input }, input, index)
4950}
4951
4952fn parse_html_tag_with<'a>(
4953    lookups: &mut impl Lookups,
4954    input: &'a str,
4955    index: usize,
4956) -> Option<(usize, &'a str)> {
4957    let bytes = input.as_bytes();
4958    if bytes.get(index) != Some(&b'<') {
4959        return None;
4960    }
4961
4962    let closing = bytes.get(index + 1) == Some(&b'/');
4963    let name_start = index + if closing { 2 } else { 1 };
4964    let first = *bytes.get(name_start)?;
4965    if !first.is_ascii_alphabetic() {
4966        return None;
4967    }
4968
4969    let mut cursor = name_start + 1;
4970    while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
4971        cursor += 1;
4972    }
4973    let name = &input[name_start..cursor];
4974
4975    if closing {
4976        cursor = skip_spaces(input, cursor);
4977        if bytes.get(cursor) == Some(&b'>') {
4978            return Some((cursor + 1, name));
4979        }
4980        return None;
4981    }
4982
4983    let mut needs_space = false;
4984    loop {
4985        let before_spaces = cursor;
4986        cursor = skip_spaces(input, cursor);
4987        let had_space = cursor > before_spaces;
4988        match bytes.get(cursor) {
4989            Some(b'>') => return Some((cursor + 1, name)),
4990            Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
4991            Some(byte) if had_space && html_attribute_name_start(*byte) => {
4992                cursor += 1;
4993                while bytes
4994                    .get(cursor)
4995                    .is_some_and(|byte| html_attribute_name_byte(*byte))
4996                {
4997                    cursor += 1;
4998                }
4999                let after_name = cursor;
5000                let after_spaces = skip_spaces(input, cursor);
5001                if bytes.get(after_spaces) == Some(&b'=') {
5002                    cursor = skip_spaces(input, after_spaces + 1);
5003                    cursor = parse_html_attribute_value(lookups, input, cursor)?;
5004                } else {
5005                    cursor = after_name;
5006                }
5007                needs_space = true;
5008            }
5009            Some(_) if needs_space => return None,
5010            _ => return None,
5011        }
5012    }
5013}
5014
5015fn parse_html_attribute_value(
5016    lookups: &mut impl Lookups,
5017    input: &str,
5018    index: usize,
5019) -> Option<usize> {
5020    let bytes = input.as_bytes();
5021    match bytes.get(index)? {
5022        b'"' => Some(lookups.find("\"", index + 1)? + 1),
5023        b'\'' => Some(lookups.find("'", index + 1)? + 1),
5024        b'=' | b'<' | b'>' | b'`' => None,
5025        _ => {
5026            let mut cursor = index;
5027            while bytes.get(cursor).is_some_and(|byte| {
5028                !byte.is_ascii_whitespace()
5029                    && !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
5030            }) {
5031                cursor += 1;
5032            }
5033            if cursor == index {
5034                None
5035            } else {
5036                Some(cursor)
5037            }
5038        }
5039    }
5040}
5041
5042fn html_name_byte(byte: u8) -> bool {
5043    byte.is_ascii_alphanumeric() || byte == b'-'
5044}
5045
5046fn html_attribute_name_start(byte: u8) -> bool {
5047    byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
5048}
5049
5050fn html_attribute_name_byte(byte: u8) -> bool {
5051    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
5052}
5053
5054fn skip_spaces(input: &str, mut index: usize) -> usize {
5055    while input
5056        .as_bytes()
5057        .get(index)
5058        .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
5059    {
5060        index += 1;
5061    }
5062    index
5063}
5064
5065/// The destination of the angle-bracket autolink `<uri>`, when `uri` writes
5066/// one: the URI itself, or `mailto:` and an email address.
5067pub(crate) fn angle_autolink_destination(uri: &str) -> Option<String> {
5068    if is_uri_autolink(uri) {
5069        Some(String::from(uri))
5070    } else if is_email_autolink(uri) {
5071        Some(alloc::format!("mailto:{uri}"))
5072    } else {
5073        None
5074    }
5075}
5076
5077/// Whether `<uri>` is an angle-bracket autolink.
5078fn is_angle_autolink(uri: &str) -> bool {
5079    is_uri_autolink(uri) || is_email_autolink(uri)
5080}
5081
5082fn is_uri_autolink(input: &str) -> bool {
5083    // A scheme is at most 32 bytes, so the `:` ending it is within the first 33.
5084    let Some(colon) = input
5085        .as_bytes()
5086        .iter()
5087        .take(33)
5088        .position(|byte| *byte == b':')
5089    else {
5090        return false;
5091    };
5092    let scheme = &input[..colon];
5093    if scheme.len() < 2 || scheme.len() > 32 {
5094        return false;
5095    }
5096    let mut bytes = scheme.bytes();
5097    if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
5098        return false;
5099    }
5100    if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
5101        return false;
5102    }
5103    input[colon + 1..]
5104        .chars()
5105        .map(source_char)
5106        .all(|char| !matches!(char, '<' | '>' | ' ') && !char.is_ascii_control())
5107}
5108
5109fn is_email_autolink(input: &str) -> bool {
5110    // No email address contains `<`, and stopping at one keeps a run of `<`
5111    // openers that share one later `>` linear.
5112    if input
5113        .chars()
5114        .any(|char| char.is_whitespace() || char == '<')
5115    {
5116        return false;
5117    }
5118    let Some(at) = input.find('@') else {
5119        return false;
5120    };
5121    if at == 0 || at + 1 >= input.len() {
5122        return false;
5123    }
5124    // Angle-bracket `<email>` autolinks use the strict CommonMark domain
5125    // grammar but, unlike the GFM bare form, allow a single (dotless) label.
5126    is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
5127}
5128
5129/// The destination of a literal autolink written as `text`, when `text` is
5130/// exactly one literal autolink the inline parser can read: the text after the
5131/// prefix `parse_literal_autolink` gives it.
5132///
5133/// Whether a literal autolink forms depends in part on what follows it:
5134/// cmark-gfm's domain check counts the `.` and `_` in the punctuation trimmed
5135/// off its end, and `www.` at a line start links `www`. So `text` is read at a
5136/// line start and followed by what lets the most through, `..` and a space:
5137/// every literal autolink the parser reads has a destination, and a text the
5138/// parser would trim never does.
5139pub(crate) fn literal_autolink_destination(text: &str) -> Option<String> {
5140    let mut input = String::from(text);
5141    input.push_str(".. ");
5142    let (end, prefix) =
5143        parse_literal_autolink(&input, 0, &mut LiteralAutolinkScan::default(), true)?;
5144    (end == text.len()).then(|| {
5145        let mut destination = String::from(prefix);
5146        destination.push_str(text);
5147        destination
5148    })
5149}
5150
5151/// A literal autolink starting at `index`, by cmark-gfm's dispatch: it tries,
5152/// in order, `http(s)://` URLs, `www.` URLs, extended-protocol
5153/// (`mailto:`/`xmpp:`) emails, and bare emails. Each branch enforces
5154/// cmark-gfm's per-scheme preceding-character guard and its domain/host
5155/// rules; the trailing trim is shared (`autolink_delim`). Returns the end and
5156/// the prefix the destination puts before `input[index..end]`, the visible
5157/// original: `http://` for a `www.` URL, `mailto:` for a bare email, and
5158/// nothing otherwise.
5159///
5160/// `urls` is false inside an open `[`: as in cmark-gfm, a `http://`,
5161/// `https://`, or `www.` URL after a `[` no `]` has closed yet stays text, and
5162/// only an email address links.
5163fn parse_literal_autolink(
5164    input: &str,
5165    index: usize,
5166    scan: &mut LiteralAutolinkScan,
5167    urls: bool,
5168) -> Option<(usize, &'static str)> {
5169    let rest = &input[index..];
5170    let www = rest
5171        .as_bytes()
5172        .get(..4)
5173        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."));
5174    if !urls {
5175        // A literal starting `www.` is a URL, linked or not, as outside an
5176        // open `[`: no email starts there.
5177        if www {
5178            return None;
5179        }
5180        return parse_literal_email(input, index, scan);
5181    }
5182
5183    // `http://` / `https://` URLs. cmark requires the char before the scheme
5184    // to be non-alphanumeric (so `mmmhttp://…` does not link from `mmmh`).
5185    if let Some(scheme_len) = rest
5186        .starts_with("http://")
5187        .then_some(7)
5188        .or_else(|| rest.starts_with("https://").then_some(8))
5189    {
5190        if !literal_scheme_prefix_ok(input, index) {
5191            return None;
5192        }
5193        // A non-empty domain is additionally required, so `http://`,
5194        // `http://#`, `http://$`, and a bracketed IPv6 host are not links.
5195        if !http_literal_host_ok(&input[index + scheme_len..]) {
5196            return None;
5197        }
5198        // The URL extent is scanned from the very start (after `://`) and the
5199        // trailing trim runs over the whole URL.
5200        let end = autolink_url_end(input, index + scheme_len);
5201        if end <= index + scheme_len {
5202            return None;
5203        }
5204        return Some((end, ""));
5205    }
5206
5207    // `www.` URLs (synthesize a `http://` href). cmark allows the preceding
5208    // char to be one of `*_~(` or whitespace (or start of input).
5209    if www {
5210        if !literal_www_prefix_ok(input, index) {
5211            return None;
5212        }
5213        if !check_domain(rest, false) {
5214            return None;
5215        }
5216        let end = autolink_url_end(input, index);
5217        if end <= index || (end <= index + 3 && !literal_starts_line(input, index)) {
5218            return None;
5219        }
5220        return Some((end, "http://"));
5221    }
5222
5223    parse_literal_email(input, index, scan)
5224}
5225
5226// The char immediately before a `http(s)://` literal must be non-alphabetic.
5227// An escaped `<` (`\<http://…`) is just literal text before the URL, so the
5228// literal still forms (the `<` is not treated as an angle-autolink opener).
5229fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
5230    if index == 0 {
5231        return true;
5232    }
5233    let Some(previous) = input[..index].chars().next_back() else {
5234        return true;
5235    };
5236    !previous.is_ascii_alphabetic()
5237}
5238
5239// The char before a `www.` literal must be one of cmark-gfm's accepted ASCII
5240// delimiters or ordinary Markdown layout whitespace. As on GitHub, other
5241// Unicode whitespace is not a start delimiter for this branch.
5242fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
5243    if index == 0 {
5244        return true;
5245    }
5246    let Some(previous) = input[..index].chars().next_back() else {
5247        return true;
5248    };
5249    if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
5250        return true;
5251    }
5252    matches!(previous, ' ' | '\t' | '\n' | '\r')
5253}
5254
5255fn literal_starts_line(input: &str, index: usize) -> bool {
5256    index == 0
5257        || input
5258            .as_bytes()
5259            .get(index - 1)
5260            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
5261}
5262
5263fn http_literal_host_ok(host: &str) -> bool {
5264    match host.chars().next() {
5265        Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => check_domain(host, true),
5266        Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
5267            check_domain(host, true)
5268        }
5269        _ => false,
5270    }
5271}
5272
5273// Port of cmark-gfm `is_valid_hostchar`: a host char is valid when it is not a
5274// Unicode space and not a Unicode punctuation character.
5275fn is_valid_hostchar(char: char) -> bool {
5276    !char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
5277}
5278
5279// Port of cmark-gfm `check_domain`: whether `data` starts with a valid host.
5280// Rejects a `_` in either of the last two `.`-separated host segments (unless
5281// the host has >10 segments — a DoS guard). When `allow_short` is false a dot
5282// is required (the `www.` rule). The URL extent past the host is determined by
5283// `autolink_url_end`, so only the verdict matters here.
5284//
5285// cmark walks bytes with `is_valid_hostchar` decoding each char; this walks
5286// chars directly (UTF-8 safe) over the host prefix, which yields the same
5287// dot/underscore-segment verdict. A `\` escapes the following char, so a host
5288// can run on over escaped spaces through later `www.` candidates; the walk
5289// stops at the eleventh dot, which settles the verdict, so that the walks
5290// from all the candidates of a pass stay linear in total.
5291fn check_domain(data: &str, allow_short: bool) -> bool {
5292    let mut np = 0usize;
5293    let mut uscore1 = 0usize;
5294    let mut uscore2 = 0usize;
5295
5296    let mut chars = data.char_indices().peekable();
5297    while let Some((offset, char)) = chars.next() {
5298        // cmark's accounting loop runs `for (i = 1; i < size - 1; i++)`: it
5299        // never inspects the first char (offset 0) nor the final char of the
5300        // chunk. We replicate that — a trailing `_` (e.g. `http://a_`) is not
5301        // counted, so the link still forms.
5302        let account = offset != 0 && chars.peek().is_some();
5303        match char {
5304            '\\' => {
5305                // Escape: consume the next char as a literal host char.
5306                chars.next();
5307            }
5308            '_' if account => uscore2 += 1,
5309            '.' if account => {
5310                uscore1 = uscore2;
5311                uscore2 = 0;
5312                np += 1;
5313                if np > 10 {
5314                    return true;
5315                }
5316            }
5317            '_' | '.' | '-' => {}
5318            _ => {
5319                if !is_valid_hostchar(source_char(char)) {
5320                    break;
5321                }
5322            }
5323        }
5324    }
5325
5326    if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
5327        return false;
5328    }
5329
5330    allow_short || np > 0
5331}
5332
5333// Forward scan from `start` for the URL extent: Unicode whitespace, `<`, a
5334// non-ASCII char in CommonMark's Unicode punctuation set (full-width `,` or
5335// `。`) other than the replacement char, `[[`, or `]` ends the URL. CommonMark allows
5336// `>` and `[` inside (the renderer percent-encodes them); a `]` is
5337// additionally treated as a hard URL boundary (autolink-3), so a `]` ends the
5338// scan and is never part of the link. The trailing trim may reach `start`.
5339fn autolink_url_end(input: &str, start: usize) -> usize {
5340    let bytes = input.as_bytes();
5341    let mut end = start;
5342    // The extent stops at the first `]` outside backticks unless a `[` came
5343    // before it in the URL (no balancing).
5344    let mut strict_has_open_bracket = false;
5345    let mut strict_inside_backticks = false;
5346    for (offset, char) in input[start..].char_indices() {
5347        if char.is_whitespace()
5348            || char == '<'
5349            || is_autolink_terminating_control(char)
5350            || (!char.is_ascii()
5351                && char != '\u{FFFD}'
5352                && crate::unicode_punctuation::is_unicode_punctuation(char))
5353            || input[start + offset..].starts_with("[[")
5354        {
5355            break;
5356        }
5357        match char {
5358            '[' => strict_has_open_bracket = true,
5359            '`' => strict_inside_backticks = !strict_inside_backticks,
5360            ']' if !strict_has_open_bracket && !strict_inside_backticks => break,
5361            _ => {}
5362        }
5363        // A `\` before ASCII punctuation other than `.` ends the URL. The
5364        // serializer writes text after a literal autolink with backslash
5365        // escapes (`\*`, `\_`, `\[`, …); stopping here keeps such a tree
5366        // writable. cmark-gfm keeps the `\*x` in `www.a.com\*x` inside the
5367        // URL; this crate deliberately diverges. A `\` before `.` or a
5368        // non-punctuation char stays part of the URL (`www.x.com/a\.`).
5369        if char == '\\' {
5370            if let Some(&next) = bytes.get(start + offset + 1) {
5371                if next.is_ascii_punctuation() && next != b'.' {
5372                    break;
5373                }
5374            }
5375        }
5376        end = start + offset + char.len_utf8();
5377    }
5378    autolink_delim(input, start, end)
5379}
5380
5381fn is_autolink_terminating_control(char: char) -> bool {
5382    matches!(char, '\u{2066}'..='\u{2069}')
5383}
5384
5385// Port of cmark-gfm `autolink_delim`: trim trailing delimiters from the end of
5386// the URL. A trailing `) ? ! . , : * _ ~ ' "` is trimmed; `)` only when there
5387// are more `)` than `(` in the link; a trailing `&` and alphanumerics ending
5388// in `;` is excluded whole; any other trailing `;` is trimmed alone.
5389fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
5390    let bytes = input.as_bytes();
5391    let mut opening = 0usize;
5392    let mut closing = 0usize;
5393    for &byte in &bytes[start..end] {
5394        match byte {
5395            b'(' => opening += 1,
5396            b')' => closing += 1,
5397            _ => {}
5398        }
5399    }
5400
5401    while end > start {
5402        match bytes[end - 1] {
5403            b')' => {
5404                if closing <= opening {
5405                    break;
5406                }
5407                closing -= 1;
5408                end -= 1;
5409            }
5410            b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
5411                end -= 1;
5412            }
5413            b';' => {
5414                // Walk back over alphanumerics; if they reach a `&`, exclude
5415                // the whole `&…;` entity run, otherwise trim just the `;`.
5416                let mut new_end = end - 1;
5417                while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
5418                    new_end -= 1;
5419                }
5420                if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
5421                    end = new_end - 1;
5422                } else {
5423                    end -= 1;
5424                }
5425            }
5426            _ => break,
5427        }
5428    }
5429    end
5430}
5431
5432/// Scan state shared by the `parse_literal_autolink` calls of one inline pass.
5433/// The pass only moves forward, so each piece lets a later start position reuse
5434/// the bytes already walked for an earlier one instead of rescanning them.
5435#[derive(Default)]
5436struct LiteralAutolinkScan {
5437    /// Runs of email local-part bytes (plus `:` for `mailto:`/`xmpp:`).
5438    email_local: ByteRun,
5439    /// What every start before one `@` shares: its last `:` and its domain.
5440    email_at: EmailAt,
5441    /// The text before a start, for the `+` boundary check.
5442    email_prefix: EmailPrefix,
5443}
5444
5445/// The facts of one `@` that every email start before it reads: the last `:`
5446/// in its local-part run at or after `from`, and the domain end for the plain
5447/// and the `xmpp:` form. A query starting before `from` rescans the run.
5448#[derive(Default)]
5449struct EmailAt {
5450    at: Option<usize>,
5451    from: usize,
5452    last_colon: Option<usize>,
5453    domain_end: [Option<Option<usize>>; 2],
5454}
5455
5456impl EmailAt {
5457    fn update(&mut self, input: &str, index: usize, at: usize) {
5458        if self.at != Some(at) {
5459            *self = Self {
5460                at: Some(at),
5461                from: usize::MAX,
5462                ..Self::default()
5463            };
5464        }
5465        if index < self.from {
5466            self.last_colon = input[index..at].rfind(':').map(|offset| index + offset);
5467            self.from = index;
5468        }
5469    }
5470
5471    /// The end of the domain after the `@`, trimmed and validated, or `None`.
5472    fn domain_end(&mut self, input: &str, is_xmpp: bool) -> Option<usize> {
5473        let at = self.at.expect("updated before use");
5474        *self.domain_end[usize::from(is_xmpp)].get_or_insert_with(|| {
5475            let start = at + 1;
5476            let end = literal_email_domain_end(input, start, is_xmpp)?;
5477            let trimmed = autolink_delim(input, start, end);
5478            (trimmed > start && is_gfm_email_domain(&input[start..trimmed], is_xmpp))
5479                .then_some(trimmed)
5480        })
5481    }
5482}
5483
5484/// Whether the text before a position ends in a bare email, kept as a pass
5485/// walks forward: the start of the text after the last whitespace, the first
5486/// byte there that no local part holds, the last `@`, and after it the first
5487/// byte no domain holds, the first `.`, and the first `.` opening an empty
5488/// label. A query before the walked position walks again from the start.
5489#[derive(Default)]
5490struct EmailPrefix {
5491    cursor: usize,
5492    segment: usize,
5493    non_local: Option<usize>,
5494    at: Option<usize>,
5495    domain_bad: Option<usize>,
5496    domain_dot: Option<usize>,
5497    domain_empty_label: Option<usize>,
5498}
5499
5500impl EmailPrefix {
5501    fn advance(&mut self, input: &str, end: usize) {
5502        if end < self.cursor {
5503            *self = Self::default();
5504        }
5505        let bytes = input.as_bytes();
5506        let from = self.cursor;
5507        for (offset, char) in input[from..end].char_indices() {
5508            let position = from + offset;
5509            if char.is_whitespace() {
5510                *self = Self {
5511                    segment: position + char.len_utf8(),
5512                    ..Self::default()
5513                };
5514                continue;
5515            }
5516            let byte = char.is_ascii().then_some(char as u8);
5517            if self.non_local.is_none() && !byte.is_some_and(is_gfm_email_local_byte) {
5518                self.non_local = Some(position);
5519            }
5520            if char == '@' {
5521                self.at = Some(position);
5522                self.domain_bad = None;
5523                self.domain_dot = None;
5524                self.domain_empty_label = None;
5525                continue;
5526            }
5527            let Some(at) = self.at else {
5528                continue;
5529            };
5530            match byte {
5531                Some(b'.') => {
5532                    self.domain_dot.get_or_insert(position);
5533                    if position == at + 1 || bytes[position - 1] == b'.' {
5534                        self.domain_empty_label.get_or_insert(position);
5535                    }
5536                }
5537                Some(byte) if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') => {}
5538                _ => {
5539                    self.domain_bad.get_or_insert(position);
5540                }
5541            }
5542        }
5543        self.cursor = end;
5544    }
5545
5546    /// Whether the text from the last whitespace before `end` to `end` ends
5547    /// in a bare email: `prefix_ends_with_gfm_email`, walked once per pass.
5548    fn ends_with_email(&mut self, input: &str, end: usize) -> bool {
5549        self.advance(input, end);
5550        let Some(at) = self.at else {
5551            return false;
5552        };
5553        let before = |position: Option<usize>| position.is_some_and(|position| position < end);
5554        at > self.segment
5555            && !self.non_local.is_some_and(|position| position < at)
5556            && end > at + 1
5557            && !before(self.domain_bad)
5558            && before(self.domain_dot)
5559            && !before(self.domain_empty_label)
5560            && !matches!(input.as_bytes()[end - 1], b'.' | b'-' | b'_')
5561    }
5562}
5563
5564/// The extent of one run of bytes accepted by a fixed predicate: every start
5565/// position in `start..=end` stops at `end`. Each `ByteRun` is always queried
5566/// with the same predicate.
5567#[derive(Default)]
5568struct ByteRun {
5569    start: usize,
5570    end: Option<usize>,
5571}
5572
5573impl ByteRun {
5574    /// Returns the first byte position at or after `index` that `in_run`
5575    /// rejects, or the input length.
5576    fn end(&mut self, input: &str, index: usize, in_run: fn(u8) -> bool) -> usize {
5577        if let Some(end) = self.end {
5578            if self.start <= index && index <= end {
5579                return end;
5580            }
5581        }
5582        let end = input.as_bytes()[index..]
5583            .iter()
5584            .position(|byte| !in_run(*byte))
5585            .map_or(input.len(), |offset| index + offset);
5586        self.start = index;
5587        self.end = Some(end);
5588        end
5589    }
5590}
5591
5592fn is_email_local_or_scheme_byte(byte: u8) -> bool {
5593    is_gfm_email_local_byte(byte) || byte == b':'
5594}
5595
5596/// A GFM bare-email literal (and the extended `mailto:`/`xmpp:` protocol
5597/// forms) starting at `index`, which must be the link start: cmark anchors
5598/// the email at the left edge found by rewinding from `@` over
5599/// `[A-Za-z0-9._+-]` (or a `mailto:`/`xmpp:` scheme), so this only succeeds
5600/// when the char before `index` is not part of that left extent.
5601fn parse_literal_email(
5602    input: &str,
5603    index: usize,
5604    scan: &mut LiteralAutolinkScan,
5605) -> Option<(usize, &'static str)> {
5606    // A valid local part holds only local-part bytes behind an optional
5607    // `mailto:`/`xmpp:` scheme, so the `@` must be the byte that ends that run.
5608    let at_index = scan
5609        .email_local
5610        .end(input, index, is_email_local_or_scheme_byte);
5611    if input.as_bytes().get(at_index) != Some(&b'@') || at_index == index {
5612        return None;
5613    }
5614
5615    // Determine whether this `@` is preceded by an extended protocol scheme
5616    // (`mailto:` / `xmpp:`), which both relaxes the href synthesis and (xmpp)
5617    // allows `/` in the domain.
5618    let (auto_mailto, is_xmpp) = classify_email_local(&input[index..at_index]);
5619
5620    // Left-boundary guard (autolink-1): the char before `index` must not be a
5621    // local-part continuation char, otherwise the true link starts earlier and
5622    // this position is interior. After a recognized scheme, the scheme's own
5623    // preceding-char rule is what matters.
5624    if !email_left_boundary_ok(input, index, auto_mailto, |end| {
5625        scan.email_prefix.ends_with_email(input, end)
5626    }) {
5627        return None;
5628    }
5629
5630    // The run holds only local-part bytes and `:`, so the local part after
5631    // its scheme is valid when it is not empty and holds no `:`. The facts of
5632    // the `@` are shared by every start before it, so a run of starts reads
5633    // them once.
5634    let body_start = if auto_mailto {
5635        index
5636    } else if is_xmpp {
5637        index + "xmpp:".len()
5638    } else {
5639        index + "mailto:".len()
5640    };
5641    scan.email_at.update(input, index, at_index);
5642    if body_start >= at_index
5643        || scan
5644            .email_at
5645            .last_colon
5646            .is_some_and(|colon| colon >= body_start)
5647    {
5648        return None;
5649    }
5650    let end = scan.email_at.domain_end(input, is_xmpp)?;
5651    Some((end, if auto_mailto { "mailto:" } else { "" }))
5652}
5653
5654// Classify the local part for the extended-protocol forms. Returns
5655// `(auto_mailto, is_xmpp)`: `mailto:user` → (false, false); `xmpp:user` →
5656// (false, true); a bare local part → (true, false). The scheme match is
5657// case-insensitive.
5658fn classify_email_local(local: &str) -> (bool, bool) {
5659    if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5660        if !rest.is_empty() {
5661            return (false, false);
5662        }
5663    }
5664    if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5665        if !rest.is_empty() {
5666            return (false, true);
5667        }
5668    }
5669    (true, false)
5670}
5671
5672fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
5673    let bytes = input.as_bytes();
5674    let plen = prefix.len();
5675    if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
5676        Some(&input[plen..])
5677    } else {
5678        None
5679    }
5680}
5681
5682// The left-boundary check for an email literal. The link is anchored at its
5683// true left edge: the preceding char must not be an ASCII alphanumeric (which
5684// would extend the local part leftward). For the bare form, a preceding `/` is
5685// also rejected (`/a@b.c` is not linked), while the extended
5686// `mailto:`/`xmpp:` form permits `/` before the scheme (so
5687// `…/mailto:beedrill@…` links).
5688fn email_left_boundary_ok(
5689    input: &str,
5690    index: usize,
5691    auto_mailto: bool,
5692    prefix_ends_with_email: impl FnOnce(usize) -> bool,
5693) -> bool {
5694    if index == 0 {
5695        return true;
5696    }
5697    let Some(previous) = input[..index].chars().next_back() else {
5698        return true;
5699    };
5700    if previous.is_ascii_alphanumeric() {
5701        if auto_mailto && input[index..].starts_with('+') && prefix_ends_with_email(index) {
5702            return true;
5703        }
5704        return false;
5705    }
5706    if auto_mailto && previous == '/' {
5707        return false;
5708    }
5709    true
5710}
5711
5712#[cfg(test)]
5713fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
5714    let start = input[..end]
5715        .char_indices()
5716        .rev()
5717        .find(|(_, char)| char.is_whitespace())
5718        .map_or(0, |(offset, char)| offset + char.len_utf8());
5719    let candidate = &input[start..end];
5720    let Some(at) = candidate.rfind('@') else {
5721        return false;
5722    };
5723    email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
5724}
5725
5726// Validate the email local part. For the bare form, every char must be a GFM
5727// email atext byte (`[A-Za-z0-9.+_-]` plus the dot-separated structure). For
5728// the extended-protocol forms, the part after the scheme is validated.
5729#[cfg(test)]
5730fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
5731    let body = if auto_mailto {
5732        local
5733    } else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5734        rest
5735    } else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5736        rest
5737    } else {
5738        local
5739    };
5740    !body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
5741}
5742
5743// GFM email local-part charset (autolink-1): a narrower set than RFC atext,
5744// matching cmark's rewind class `[A-Za-z0-9.+_-]`.
5745fn is_gfm_email_local_byte(byte: u8) -> bool {
5746    byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
5747}
5748
5749fn is_email_local_part(input: &str) -> bool {
5750    !input.is_empty()
5751        && input
5752            .split('.')
5753            .all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
5754}
5755
5756fn is_email_atext(byte: u8) -> bool {
5757    byte.is_ascii_alphanumeric()
5758        || matches!(
5759            byte,
5760            b'!' | b'#'
5761                | b'$'
5762                | b'%'
5763                | b'&'
5764                | b'\''
5765                | b'*'
5766                | b'+'
5767                | b'/'
5768                | b'='
5769                | b'?'
5770                | b'^'
5771                | b'_'
5772                | b'`'
5773                | b'{'
5774                | b'|'
5775                | b'}'
5776                | b'~'
5777                | b'-'
5778        )
5779}
5780
5781// Port of cmark-gfm's email-domain scan (`postprocess_text`). Scans forward
5782// from `index` over the email domain, accepting alphanumerics, `-`, `_`, and
5783// `.`; for the `xmpp:` form a `/` is also accepted (path). A dot only counts
5784// toward the "at least one dot" requirement when it is followed by an
5785// alphanumeric. The scanned span must be >= 1 byte, contain at least one such
5786// dot, and end in an alphabetic char or a dot. Returns the domain end offset
5787// (before trailing trim), or `None` when invalid.
5788fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
5789    let bytes = input.as_bytes();
5790    let mut end = index;
5791    let mut np = 0usize;
5792    while end < bytes.len() {
5793        let byte = bytes[end];
5794        if byte.is_ascii_alphanumeric() {
5795            end += 1;
5796        } else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
5797            np += 1;
5798            end += 1;
5799        } else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
5800            // `-`/`_` always continue the domain; `/` continues only the xmpp
5801            // path form.
5802            end += 1;
5803        } else {
5804            break;
5805        }
5806    }
5807    if end <= index {
5808        return None;
5809    }
5810    let len = end - index;
5811    let last = bytes[end - 1];
5812    if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
5813        return None;
5814    }
5815    Some(end)
5816}
5817
5818// Final structural validation of the trimmed email domain. The cmark scan
5819// already enforced the dot/last-char rules; this re-checks them after the
5820// shared trailing trim removed any delimiters, and rejects a domain ending in
5821// `-`/`_` (autolink-7: a hyphen in the final label disqualifies the link).
5822fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
5823    if input.is_empty() {
5824        return false;
5825    }
5826    // A `/` path is only legal in the `xmpp:` form; split it off for the host
5827    // structural checks.
5828    let host = if is_xmpp {
5829        input.split('/').next().unwrap_or(input)
5830    } else {
5831        input
5832    };
5833    if !host.contains('.') {
5834        return false;
5835    }
5836    let last = host.as_bytes()[host.len() - 1];
5837    // The final label must not end in `-` or `_`, and the trailing label may
5838    // not be all ASCII digits.
5839    if matches!(last, b'-' | b'_') {
5840        return false;
5841    }
5842    host.split('.').all(|label| {
5843        !label.is_empty()
5844            && label
5845                .bytes()
5846                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
5847    })
5848}
5849
5850fn is_email_domain(input: &str, min_labels: usize) -> bool {
5851    let mut label_count = 0usize;
5852    for label in input.split('.') {
5853        label_count += 1;
5854        let bytes = label.as_bytes();
5855        if bytes.is_empty()
5856            || bytes.len() > 63
5857            || !bytes
5858                .first()
5859                .is_some_and(|byte| byte.is_ascii_alphanumeric())
5860            || !bytes
5861                .last()
5862                .is_some_and(|byte| byte.is_ascii_alphanumeric())
5863            || !bytes
5864                .iter()
5865                .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
5866        {
5867            return false;
5868        }
5869    }
5870    label_count >= min_labels
5871}
5872
5873/// A footnote label: no space, tab, or line ending and, as in a link label,
5874/// no unescaped bracket.
5875pub(crate) fn is_footnote_label(label: &str) -> bool {
5876    !label.is_empty()
5877        && reference_label_is_within_limit(label)
5878        && !label.contains([' ', '\t', '\n', '\r'])
5879        && !label
5880            .match_indices(['[', ']'])
5881            .any(|(index, _)| !is_escaped_at(label, index))
5882}
5883
5884fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
5885    let close = find_footnote_reference_label_end(input, 2)?;
5886    if input.as_bytes().get(close + 1) == Some(&b':') {
5887        Some(close)
5888    } else {
5889        None
5890    }
5891}
5892
5893fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
5894    while cursor < input.len() {
5895        let (next, char) = next_char(input, cursor)?;
5896        if char == ']' && !is_escaped_at(input, cursor) {
5897            return Some(cursor);
5898        }
5899        cursor = next;
5900    }
5901    None
5902}