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