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