use alloc::{borrow::Cow, collections::BTreeMap, string::String, vec, vec::Vec};
use crate::{
ast::*,
diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
entities::named_character_reference,
memo::{
bracket_walk, path_walk, pattern_starts, BracketMemo, BracketStep, PathMemo, PersistentMap,
Positions, Step,
},
options::{SyntaxConfigError, SyntaxOptions},
span::Span,
validate::is_directive_name,
};
mod nul;
#[cfg(test)]
mod scan_tests;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseOutput {
pub document: Document,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ParseStrictError {
Config(SyntaxConfigError),
Diagnostic(Diagnostic),
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ParsedLinkResource {
destination: String,
destination_kind: LinkDestinationKind,
title: Option<String>,
title_kind: Option<LinkTitleKind>,
}
const REFERENCE_LABEL_MAX_CHARS: usize = 999;
const WIKILINK_MAX_BYTES: usize = 999;
#[derive(Clone, Copy, Debug)]
struct Line<'a> {
text: &'a str,
eol: &'a str,
start: usize,
end: usize,
end_with_eol: usize,
lazy: bool,
}
#[derive(Clone, Copy, Debug)]
struct ListMarkerInfo<'a> {
ordered: bool,
start: Option<u64>,
delimiter: ListDelimiter,
indent: usize,
marker_len: usize,
content_indent: usize,
content: &'a str,
}
#[derive(Clone, Copy, Debug)]
struct DescriptionMarker<'a> {
content_offset: usize,
content: &'a str,
}
#[derive(Clone, Debug)]
struct DescriptionTerm {
marker_index: usize,
term_end: usize,
blank_after_term: bool,
source: String,
source_offset: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum HtmlBlockKind {
RawTag,
BlockTag,
Until(&'static str),
UntilBlank,
}
pub fn parse(input: &str) -> ParseOutput {
SyntaxOptions::default().parse(input)
}
impl SyntaxOptions {
pub fn parse(&self, input: &str) -> ParseOutput {
match parse_checked(input, self) {
Ok(output) => output,
Err(error) => ParseOutput {
document: Document::default(),
diagnostics: vec![Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::StrictParse,
Span::new(0, input.len()),
error.message(),
)],
},
}
}
pub fn parse_strict(&self, input: &str) -> Result<ParseOutput, ParseStrictError> {
let output = parse_checked(input, self).map_err(ParseStrictError::Config)?;
if let Some(diagnostic) = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
{
return Err(ParseStrictError::Diagnostic(diagnostic.clone()));
}
Ok(output)
}
}
fn parse_checked(input: &str, options: &SyntaxOptions) -> Result<ParseOutput, SyntaxConfigError> {
options.validate()?;
let mut diagnostics = Vec::new();
let start = if input.starts_with('\u{feff}') {
'\u{feff}'.len_utf8()
} else {
0
};
let source = &input[start..];
let definitions = collect_definitions(source, start, options);
let children = parse_blocks(
source,
start,
true,
options,
&definitions,
&mut diagnostics,
0,
);
let mut document = Document {
meta: NodeMeta::new(Some(Span::new(0, input.len()))),
children,
};
if source.contains('\0') {
nul::replace_in_document(&mut document);
}
Ok(ParseOutput {
document,
diagnostics,
})
}
fn source_char(char: char) -> char {
if char == '\0' {
'\u{FFFD}'
} else {
char
}
}
const MAX_BLOCK_NESTING: usize = 32;
fn parse_blocks(
input: &str,
base_offset: usize,
allow_frontmatter: bool,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Vec<Block> {
let lines = collect_lines(input, base_offset);
parse_blocks_from_lines(
&lines,
allow_frontmatter,
options,
definitions,
diagnostics,
depth,
)
}
fn parse_blocks_from_lines(
lines: &[Line<'_>],
allow_frontmatter: bool,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Vec<Block> {
let mut blocks = Vec::new();
let mut index = 0;
let nest_containers = depth < MAX_BLOCK_NESTING;
let mut container_closes = BracketMemo::default();
let mut mdx_flow = MdxFlowScan::default();
while index < lines.len() {
let line = lines[index];
if line.text.trim().is_empty() {
index += 1;
continue;
}
let after_definition_unbroken = index > 0
&& !lines[index - 1].text.trim().is_empty()
&& matches!(blocks.last(), Some(Block::Definition(_)));
if allow_frontmatter && index == 0 {
if let Some((block, next)) = parse_frontmatter(lines, index, options) {
blocks.push(block);
index = next;
continue;
}
}
if let Some((block, next)) = nest_containers
.then(|| {
parse_container_directive(lines, index, options, definitions, diagnostics, depth)
})
.flatten()
{
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = parse_math_block(lines, index, options) {
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = parse_fenced_code(lines, index, options) {
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = nest_containers
.then(|| parse_block_quote(lines, index, options, definitions, diagnostics, depth))
.flatten()
{
blocks.push(block);
index = next;
continue;
}
if let Some(block) = parse_atx_heading(line, options, definitions) {
blocks.push(block);
index += 1;
continue;
}
if let Some(block) = parse_thematic_break(line) {
blocks.push(block);
index += 1;
continue;
}
if let Some((block, next)) = nest_containers
.then(|| parse_list(lines, index, options, definitions, diagnostics, depth))
.flatten()
{
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = nest_containers
.then(|| {
parse_footnote_definition(lines, index, options, definitions, diagnostics, depth)
})
.flatten()
{
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) =
parse_definition(lines, index, options, after_definition_unbroken)
{
blocks.push(block);
index = next;
continue;
}
if let Some(block) = parse_leaf_directive(line, options, definitions, diagnostics) {
blocks.push(block);
index += 1;
continue;
}
if let Some((block, next)) = nest_containers
.then(|| {
parse_html_container(
lines,
index,
options,
definitions,
diagnostics,
depth,
&mut container_closes,
)
})
.flatten()
{
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = parse_html_block(lines, index, options) {
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) =
parse_mdx_flow(lines, index, options, diagnostics, &mut mdx_flow)
{
blocks.push(block);
index = next;
continue;
}
if !after_definition_unbroken {
if let Some((block, next)) = parse_indented_code(lines, index, options) {
blocks.push(block);
index = next;
continue;
}
}
if let Some((block, next)) = parse_table(lines, index, options, definitions, diagnostics) {
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = parse_setext_heading(lines, index, options, definitions) {
blocks.push(block);
index = next;
continue;
}
if let Some((block, next)) = nest_containers
.then(|| parse_description_list(lines, index, options, definitions, diagnostics, depth))
.flatten()
{
blocks.push(block);
index = next;
continue;
}
let (block, next) = parse_paragraph(lines, index, options, definitions, diagnostics);
blocks.push(block);
index = next;
}
blocks
}
fn collect_lines(input: &str, base_offset: usize) -> Vec<Line<'_>> {
let bytes = input.as_bytes();
let mut lines = Vec::new();
let mut start = 0;
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'\n' => {
let end = index;
lines.push(Line {
text: &input[start..end],
eol: &input[index..index + 1],
start: base_offset + start,
end: base_offset + end,
end_with_eol: base_offset + index + 1,
lazy: false,
});
index += 1;
start = index;
}
b'\r' => {
let end = index;
let eol_end = if index + 1 < bytes.len() && bytes[index + 1] == b'\n' {
index + 2
} else {
index + 1
};
lines.push(Line {
text: &input[start..end],
eol: &input[index..eol_end],
start: base_offset + start,
end: base_offset + end,
end_with_eol: base_offset + eol_end,
lazy: false,
});
index = eol_end;
start = index;
}
_ => index += 1,
}
}
if start < bytes.len() || input.is_empty() {
lines.push(Line {
text: &input[start..],
eol: "",
start: base_offset + start,
end: base_offset + bytes.len(),
end_with_eol: base_offset + bytes.len(),
lazy: false,
});
}
lines
}
fn collect_definitions(input: &str, base_offset: usize, options: &SyntaxOptions) -> Vec<String> {
let mut diagnostics = Vec::new();
let blocks = parse_blocks(input, base_offset, true, options, &[], &mut diagnostics, 0);
let mut definitions = Vec::new();
collect_definition_refs_from_blocks(&blocks, &mut definitions);
definitions.sort_unstable();
definitions.dedup();
definitions
}
fn collect_definition_refs_from_blocks(blocks: &[Block], definitions: &mut Vec<String>) {
for block in blocks {
match block {
Block::Definition(definition) => {
definitions.push(definition.identifier.clone());
}
Block::BlockQuote(node) => {
collect_definition_refs_from_blocks(&node.children, definitions);
}
Block::Alert(node) => {
collect_definition_refs_from_blocks(&node.children, definitions);
}
Block::List(node) => {
for item in &node.children {
collect_definition_refs_from_blocks(&item.children, definitions);
}
}
Block::DescriptionList(node) => {
for item in &node.children {
for details in &item.details {
collect_definition_refs_from_blocks(&details.children, definitions);
}
}
}
Block::FootnoteDefinition(node) => {
collect_definition_refs_from_blocks(&node.children, definitions);
}
Block::HtmlContainer(node) => {
if let HtmlContainerContent::Blocks(children) = &node.content {
collect_definition_refs_from_blocks(children, definitions);
}
}
Block::ContainerDirective(node) => {
collect_definition_refs_from_blocks(&node.children, definitions);
}
_ => {}
}
}
}
fn parse_frontmatter(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
) -> Option<(Block, usize)> {
if !options.constructs.frontmatter {
return None;
}
let kind = frontmatter_fence_kind(lines[index].text)?;
let mut value = String::new();
let mut cursor = index + 1;
while cursor < lines.len() {
if frontmatter_fence_kind(lines[cursor].text) == Some(kind) {
let span = Span::new(lines[index].start, lines[cursor].end_with_eol);
return Some((
Block::Frontmatter(Frontmatter {
meta: NodeMeta::new(Some(span)),
kind,
value,
}),
cursor + 1,
));
}
push_line(&mut value, lines[cursor].text);
cursor += 1;
}
None
}
fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
match line.trim_end_matches([' ', '\t']) {
"---" => Some(FrontmatterKind::Yaml),
"+++" => Some(FrontmatterKind::Toml),
_ => None,
}
}
fn parse_container_directive(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<(Block, usize)> {
if !options.constructs.directive_container {
return None;
}
let trimmed = trim_up_to_three_spaces(lines[index].text)?;
let Some((fence_len, opener_rest)) = directive_container_opener_prefix(trimmed) else {
return None;
};
let opener_base = lines[index].start + (lines[index].text.len() - trimmed.len()) + fence_len;
let Some((name, label_source, attributes, _consumed)) = parse_directive_opener(opener_rest)
else {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidDirectiveName,
Span::new(lines[index].start, lines[index].end),
"container directive must have a valid name",
));
return None;
};
let label_base = opener_base + name.len() + 1;
let mut content = String::new();
let mut cursor = index + 1;
let mut nested_fences = Vec::new();
while cursor < lines.len() {
let line = lines[cursor].text;
let trimmed = trim_up_to_three_spaces(line);
if let Some(trimmed) = trimmed {
if let Some(nested_len) = nested_fences.last().copied() {
if directive_container_closing_fence(trimmed, nested_len).is_some() {
nested_fences.pop();
push_line(&mut content, line);
cursor += 1;
continue;
}
} else if directive_container_closing_fence(trimmed, fence_len).is_some() {
let label = label_source
.map(|source| {
parse_inlines(source, label_base, options, definitions, diagnostics)
})
.unwrap_or_default();
let children = parse_blocks(
&content,
lines[index + 1].start,
false,
options,
definitions,
diagnostics,
depth + 1,
);
return Some((
Block::ContainerDirective(ContainerDirective {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[cursor].end_with_eol,
))),
name,
label,
attributes,
children,
}),
cursor + 1,
));
}
if let Some((nested_len, nested_rest)) = directive_container_opener_prefix(trimmed) {
if parse_directive_opener(nested_rest).is_some() {
nested_fences.push(nested_len);
}
}
}
push_line(&mut content, line);
cursor += 1;
}
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::UnclosedDirectiveContainer,
Span::new(lines[index].start, lines[index].end),
"container directive is missing a closing fence",
));
Some((
Block::ContainerDirective(ContainerDirective {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines.last()?.end_with_eol,
))),
name,
label: label_source
.map(|source| parse_inlines(source, label_base, options, definitions, diagnostics))
.unwrap_or_default(),
attributes,
children: parse_blocks(
&content,
lines
.get(index + 1)
.map(|line| line.start)
.unwrap_or(lines[index].end),
false,
options,
definitions,
diagnostics,
depth + 1,
),
}),
lines.len(),
))
}
fn directive_container_opener_prefix(input: &str) -> Option<(usize, &str)> {
let fence_len = input
.as_bytes()
.iter()
.take_while(|byte| **byte == b':')
.count();
if fence_len >= 3 {
Some((fence_len, &input[fence_len..]))
} else {
None
}
}
fn directive_container_closing_fence(input: &str, min_len: usize) -> Option<usize> {
let fence_len = input
.as_bytes()
.iter()
.take_while(|byte| **byte == b':')
.count();
if fence_len >= min_len && input[fence_len..].trim().is_empty() {
Some(fence_len)
} else {
None
}
}
fn parse_math_block(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
) -> Option<(Block, usize)> {
if !options.constructs.math_block {
return None;
}
let opener = trim_up_to_three_spaces(lines[index].text)?;
let fence_length = math_block_fence_length(opener)?;
let opening_indent = leading_indent_columns(lines[index].text);
let mut value = String::new();
let mut content_lines = 0usize;
let mut cursor = index + 1;
while cursor < lines.len() {
if let Some(close_line) = trim_up_to_three_spaces(lines[cursor].text) {
if math_block_fence_closes(close_line, fence_length) {
return Some((
Block::MathBlock(MathBlock {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[cursor].end_with_eol,
))),
value,
}),
cursor + 1,
));
}
}
if content_lines > 0 {
ensure_line_separator(&mut value);
}
let stripped = strip_leading_indent_columns(lines[cursor].text, opening_indent);
value.push_str(&stripped);
value.push_str(lines[cursor].eol);
content_lines += 1;
cursor += 1;
}
Some((
Block::MathBlock(MathBlock {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines.last()?.end_with_eol,
))),
value,
}),
lines.len(),
))
}
fn math_block_fence_length(input: &str) -> Option<usize> {
let length = input
.as_bytes()
.iter()
.take_while(|byte| **byte == b'$')
.count();
if length < 2 || input[length..].contains('$') {
return None;
}
Some(length)
}
fn math_block_fence_closes(input: &str, length: usize) -> bool {
let count = input
.as_bytes()
.iter()
.take_while(|byte| **byte == b'$')
.count();
count >= length && input[count..].trim().is_empty()
}
fn parse_fenced_code(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
) -> Option<(Block, usize)> {
let line = fence_line(lines[index].text, options)?;
let (marker, length) = fence_start(line)?;
let opening_indent = leading_indent_columns(lines[index].text);
let info = line[length..].trim();
if marker == FenceMarker::Backtick && info.contains('`') {
return None;
}
let info = if info.is_empty() {
None
} else {
Some(unescape_string(info))
};
let mut value = String::new();
let mut content_lines = 0usize;
let mut cursor = index + 1;
while cursor < lines.len() {
if let Some(close_line) = fence_line(lines[cursor].text, options) {
if fence_close(close_line, marker, length) {
return Some((
Block::CodeBlock(CodeBlock {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[cursor].end_with_eol,
))),
kind: CodeBlockKind::Fenced { marker, length },
info,
value,
}),
cursor + 1,
));
}
}
if content_lines > 0 {
ensure_line_separator(&mut value);
}
let stripped = strip_leading_indent_columns(lines[cursor].text, opening_indent);
value.push_str(&stripped);
value.push_str(lines[cursor].eol);
content_lines += 1;
cursor += 1;
}
Some((
Block::CodeBlock(CodeBlock {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines.last()?.end_with_eol,
))),
kind: CodeBlockKind::Fenced { marker, length },
info,
value,
}),
lines.len(),
))
}
fn fence_line<'a>(line: &'a str, options: &SyntaxOptions) -> Option<&'a str> {
if options.constructs.indented_code {
trim_up_to_three_spaces(line)
} else {
Some(trim_ascii_start(line))
}
}
fn container_closed_after_unclosed_fence(
lines: &[Line<'_>],
cursor: usize,
last_content_index: usize,
content: &str,
options: &SyntaxOptions,
) -> bool {
!lines[last_content_index].eol.is_empty()
&& (cursor >= lines.len() || lines[cursor].text.trim().is_empty())
&& content_has_unclosed_fenced_code(content, options)
}
fn content_has_unclosed_fenced_code(content: &str, options: &SyntaxOptions) -> bool {
let lines = collect_lines(content, 0);
let mut open_fence = None;
for line in lines {
let Some(trimmed) = fence_line(line.text, options) else {
continue;
};
if let Some((marker, length, has_nonblank_content)) = open_fence {
if fence_close(trimmed, marker, length) {
open_fence = None;
} else {
open_fence = Some((
marker,
length,
has_nonblank_content || !trimmed.trim().is_empty(),
));
}
continue;
}
let Some((marker, length)) = fence_start(trimmed) else {
continue;
};
let info = trimmed[length..].trim();
if marker != FenceMarker::Backtick || !info.contains('`') {
open_fence = Some((marker, length, false));
}
}
open_fence.is_some_and(|(_, _, has_nonblank_content)| !has_nonblank_content)
}
fn block_quote_content_paragraph_open(content: &str, options: &SyntaxOptions) -> bool {
let mut source = Cow::Borrowed(content);
let mut offset = 0;
for _ in 0..=MAX_BLOCK_NESTING {
let Some(trimmed) = trim_up_to_three_spaces(&source[offset..]) else {
return false;
};
if trimmed.is_empty() {
return false;
}
let rest = if let Some(rest) = trimmed.strip_prefix('>') {
Cow::Borrowed(rest.strip_prefix(' ').unwrap_or(rest))
} else if let Some(marker) = list_marker_info(trimmed) {
list_marker_first_content(trimmed, marker)
} else {
return !lazy_line_starts_block(trimmed, options);
};
match rest {
Cow::Borrowed(rest) => offset = source.len() - rest.len(),
Cow::Owned(rest) => {
source = Cow::Owned(rest);
offset = 0;
}
}
}
true
}
fn lazy_line_starts_block(input: &str, options: &SyntaxOptions) -> bool {
likely_block_start(input, options)
|| (options.constructs.html_block && line_starts_html_block(input))
|| trim_up_to_three_spaces(input).is_some_and(|t| t.starts_with('`') || t.starts_with('~'))
}
fn parse_block_quote(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<(Block, usize)> {
if !trim_up_to_three_spaces(lines[index].text)?.starts_with('>') {
return None;
}
let mut content = String::new();
let mut lazy_flags: Vec<bool> = Vec::new();
let mut cursor = index;
let mut paragraph_open = false;
let mut in_table = false;
let mut last_content_line: Option<String> = None;
let mut content_base_offset = None;
while cursor < lines.len() {
let raw = lines[cursor].text;
let trimmed_opt = trim_up_to_three_spaces(raw);
let marked = trimmed_opt.is_some_and(|trimmed| trimmed.starts_with('>'));
let quote_rest_owned: String;
if let Some(trimmed) = trimmed_opt {
if trimmed.is_empty() {
break;
}
}
let (line, line_start) = if marked {
let trimmed = trimmed_opt.expect("marked implies a trimmed line");
let trimmed_start = lines[cursor].start + (raw.len() - trimmed.len());
let mut rest_start = 1;
let mut rest = &trimmed[rest_start..];
if rest.starts_with(' ') {
rest_start += 1;
rest = &rest[1..];
} else if rest.starts_with('\t') {
let marker_end_column = leading_indent_columns(raw) + 1;
match strip_leading_indent_columns_from(rest, 1, marker_end_column) {
Cow::Borrowed(stripped) => rest = stripped,
Cow::Owned(stripped) => {
quote_rest_owned = stripped;
rest = "e_rest_owned;
}
}
}
(rest, trimmed_start + rest_start)
} else if in_table {
break;
} else if paragraph_open && !lazy_line_starts_block(raw, options) {
(raw, lines[cursor].start)
} else {
break;
};
let mut escaped_lazy = String::new();
let line = if !marked
&& last_content_line.as_deref().is_some_and(|previous| {
table_can_start_source(
previous,
line,
options.constructs.indented_code,
options.constructs.spoiler,
)
}) {
escaped_lazy.push_str(line);
if let Some(offset) = escaped_lazy.find('-') {
escaped_lazy.insert(offset, '\\');
}
&escaped_lazy
} else {
line
};
let starts_table = last_content_line.as_deref().is_some_and(|previous| {
table_can_start_source(
previous,
line,
options.constructs.indented_code,
options.constructs.spoiler,
)
});
if marked && starts_table {
paragraph_open = false;
in_table = true;
} else if marked && in_table && block_quote_table_body_row(line, options) {
paragraph_open = false;
} else {
in_table = false;
paragraph_open = block_quote_content_paragraph_open(line, options);
}
last_content_line = Some(line.into());
if content_base_offset.is_none() {
content_base_offset = Some(line_start);
}
push_line(&mut content, line);
lazy_flags.push(!marked);
cursor += 1;
}
let span = Span::new(lines[index].start, lines[cursor - 1].end_with_eol);
let child_base_offset = content_base_offset.unwrap_or(lines[index].start);
if !lines[cursor - 1].eol.is_empty() && !ends_with_line_ending(&content) {
content.push_str(lines[cursor - 1].eol);
}
if container_closed_after_unclosed_fence(lines, cursor, cursor - 1, &content, options) {
content.push('\n');
}
if let Some(alert) = parse_alert_from_block_quote(
&content,
child_base_offset,
span,
options,
definitions,
diagnostics,
depth,
) {
return Some((alert, cursor));
}
let mut child_lines = collect_lines(&content, child_base_offset);
for (child, &lazy) in child_lines.iter_mut().zip(lazy_flags.iter()) {
child.lazy = lazy;
}
let children = parse_blocks_from_lines(
&child_lines,
false,
options,
definitions,
diagnostics,
depth + 1,
);
Some((
Block::BlockQuote(BlockQuote {
meta: NodeMeta::new(Some(span)),
children,
}),
cursor,
))
}
fn parse_alert_from_block_quote(
content: &str,
base_offset: usize,
span: Span,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<Block> {
if !options.constructs.gfm_alert {
return None;
}
let (first_line, rest) = content.split_once('\n').unwrap_or((content, ""));
let (kind, title) = parse_alert_marker(first_line)?;
let rest_base_offset = base_offset + first_line.len() + usize::from(!rest.is_empty());
let children = if rest.is_empty() {
Vec::new()
} else {
parse_blocks(
rest,
rest_base_offset,
false,
options,
definitions,
diagnostics,
depth + 1,
)
};
Some(Block::Alert(Alert {
meta: NodeMeta::new(Some(span)),
kind,
title,
children,
}))
}
fn parse_alert_marker(line: &str) -> Option<(AlertKind, Option<String>)> {
let close = line.find(']')?;
let marker = line.get(0..close + 1)?;
if !marker.starts_with("[!") {
return None;
}
let kind = match &marker[2..close].to_ascii_lowercase()[..] {
"note" => AlertKind::Note,
"tip" => AlertKind::Tip,
"important" => AlertKind::Important,
"warning" => AlertKind::Warning,
"caution" => AlertKind::Caution,
_ => return None,
};
let title = line[close + 1..].trim();
Some((
kind,
if title.is_empty() {
None
} else {
Some(title.into())
},
))
}
fn block_quote_table_body_row(line: &str, options: &SyntaxOptions) -> bool {
table_indent_line(line, options.constructs.indented_code).is_some_and(|row| {
!row.trim().is_empty() && contains_unescaped_pipe(row, options.constructs.spoiler)
})
}
fn parse_list(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<(Block, usize)> {
let first_marker = list_marker_info(lines[index].text)?;
let mut items = Vec::new();
let mut cursor = index;
let mut tight = true;
while cursor < lines.len() {
if parse_thematic_break(lines[cursor]).is_some() {
break;
}
let Some(marker) = list_marker_info(lines[cursor].text) else {
break;
};
if !same_list_marker(first_marker, marker) {
break;
}
let item_start = cursor;
let mut item_end = cursor;
let mut item_tight = true;
let mut item_blank_offsets: Vec<usize> = Vec::new();
let mut content = String::new();
let mut lazy_flags: Vec<bool> = Vec::new();
let mut open_fence = None;
let first_content = list_marker_first_content(lines[cursor].text, marker);
let mut last_content_line: Option<String> = Some(first_content.as_ref().into());
let mut paragraph_open = list_item_paragraph_stays_open(None, &first_content, options);
let mut item_started_blank = first_content.trim().is_empty();
push_line(&mut content, &first_content);
lazy_flags.push(false);
update_list_item_fence(&first_content, &mut open_fence);
cursor += 1;
while cursor < lines.len() {
if lines[cursor].text.trim().is_empty() {
if open_fence.is_some() {
let stripped = strip_list_continuation(
lines[cursor].text,
marker.content_indent,
first_marker.indent,
);
push_line(&mut content, &stripped);
lazy_flags.push(false);
update_list_item_fence(&stripped, &mut open_fence);
item_end = cursor;
cursor += 1;
continue;
}
let next = next_nonblank_line(lines, cursor + 1);
if item_started_blank
|| next >= lines.len()
|| sibling_list_marker_at_line(
lines[next].text,
first_marker,
marker.content_indent,
)
|| leading_indent_columns(lines[next].text) < marker.content_indent
{
if next < lines.len()
&& sibling_list_marker_at_line(
lines[next].text,
first_marker,
marker.content_indent,
)
{
item_tight = false;
}
cursor = next;
break;
}
item_blank_offsets.push(content.len() + usize::from(!content.is_empty()));
paragraph_open = false;
push_line(&mut content, "");
lazy_flags.push(false);
item_end = cursor;
cursor += 1;
continue;
}
item_started_blank = false;
if sibling_list_marker_at_line(lines[cursor].text, first_marker, marker.content_indent)
{
break;
}
if leading_indent_columns(lines[cursor].text) < marker.content_indent
&& !same_list_marker_line(lines[cursor].text, first_marker)
&& list_marker_info(lines[cursor].text).is_some()
{
break;
}
if leading_indent_columns(lines[cursor].text) < marker.content_indent {
if likely_block_start(lines[cursor].text, options) || !paragraph_open {
break;
}
}
let lazy = paragraph_open
&& leading_indent_columns(lines[cursor].text) < marker.content_indent;
let stripped = strip_list_continuation(
lines[cursor].text,
marker.content_indent,
first_marker.indent,
);
let starts_table = last_content_line.as_deref().is_some_and(|previous| {
table_can_start_source(
previous,
&stripped,
options.constructs.indented_code,
options.constructs.spoiler,
)
});
paragraph_open = if starts_table {
false
} else {
list_item_paragraph_stays_open(Some(paragraph_open), &stripped, options)
};
push_line(&mut content, &stripped);
lazy_flags.push(lazy);
update_list_item_fence(&stripped, &mut open_fence);
last_content_line = Some(stripped.into_owned());
item_end = cursor;
cursor += 1;
}
let child_base = lines[item_start].start + marker.content_indent;
if !lines[item_end].eol.is_empty() && !ends_with_line_ending(&content) {
content.push_str(lines[item_end].eol);
}
if container_closed_after_unclosed_fence(lines, cursor, item_end, &content, options) {
content.push('\n');
}
let mut child_lines = collect_lines(&content, child_base);
for (child, &lazy) in child_lines.iter_mut().zip(lazy_flags.iter()) {
child.lazy = lazy;
}
let mut children = parse_blocks_from_lines(
&child_lines,
false,
options,
definitions,
diagnostics,
depth + 1,
);
let checked = if options.constructs.gfm_task_list_item {
take_task_marker_from_children(&mut children)
} else {
None
};
if item_tight
&& blank_separates_top_level_blocks(&item_blank_offsets, &children, child_base)
{
item_tight = false;
}
tight = tight && item_tight;
items.push(ListItem {
meta: NodeMeta::new(Some(Span::new(
lines[item_start].start,
lines[item_end].end_with_eol,
))),
checked,
children,
});
}
Some((
Block::List(List {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[cursor - 1].end_with_eol,
))),
ordered: first_marker.ordered,
start: first_marker.start,
delimiter: first_marker.delimiter,
tight,
children: items,
}),
cursor,
))
}
fn blank_separates_top_level_blocks(
blank_offsets: &[usize],
children: &[Block],
child_base: usize,
) -> bool {
if blank_offsets.is_empty() || children.len() < 2 {
return false;
}
let Some(&first_blank) = blank_offsets.iter().min() else {
return false;
};
children.iter().any(|child| {
block_span(child).is_some_and(|span| span.start.saturating_sub(child_base) > first_blank)
})
}
fn block_span(block: &Block) -> Option<Span> {
let meta = match block {
Block::Paragraph(node) => &node.meta,
Block::Heading(node) => &node.meta,
Block::ThematicBreak(node) => &node.meta,
Block::BlockQuote(node) => &node.meta,
Block::Alert(node) => &node.meta,
Block::List(node) => &node.meta,
Block::DescriptionList(node) => &node.meta,
Block::CodeBlock(node) => &node.meta,
Block::HtmlBlock(node) => &node.meta,
Block::HtmlContainer(node) => &node.meta,
Block::Definition(node) => &node.meta,
Block::FootnoteDefinition(node) => &node.meta,
Block::Table(node) => &node.meta,
Block::MathBlock(node) => &node.meta,
Block::Frontmatter(node) => &node.meta,
Block::MdxEsm(node) => &node.meta,
Block::MdxExpression(node) => &node.meta,
Block::MdxJsx(node) => &node.meta,
Block::LeafDirective(node) => &node.meta,
Block::ContainerDirective(node) => &node.meta,
};
meta.span
}
fn list_item_paragraph_stays_open(
previous_open: Option<bool>,
line: &str,
options: &SyntaxOptions,
) -> bool {
if line.trim().is_empty() {
return false;
}
if previous_open == Some(false) {
return false;
}
block_quote_content_paragraph_open(line, options)
}
fn parse_description_list(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<(Block, usize)> {
if !options.constructs.description_list || !is_description_term_line(lines[index].text, options)
{
return None;
}
let mut cursor = index;
let mut items = Vec::new();
let mut tight = true;
let mut list_end = lines[index].end_with_eol;
while cursor < lines.len() {
if !is_description_term_line(lines[cursor].text, options) {
break;
}
let Some(term) = description_term(lines, cursor, options) else {
break;
};
let term_line = lines[cursor];
let mut details = Vec::new();
let item_start = term_line.start;
let mut item_end = lines[term.term_end].end_with_eol;
tight = tight && !term.blank_after_term;
cursor = term.marker_index;
loop {
let Some(marker) = description_marker(lines[cursor].text) else {
break;
};
let (detail, next, detail_tight) = parse_description_details(
lines,
cursor,
marker,
options,
definitions,
diagnostics,
depth,
)?;
tight = tight && detail_tight;
item_end = detail
.meta
.span
.map(|span| span.end)
.unwrap_or(lines[cursor].end_with_eol);
details.push(detail);
cursor = next;
let next_nonblank = next_nonblank_line(lines, cursor);
if next_nonblank < lines.len()
&& description_marker(lines[next_nonblank].text).is_some()
{
if next_nonblank != cursor {
tight = false;
}
cursor = next_nonblank;
continue;
}
break;
}
if details.is_empty() {
return None;
}
list_end = item_end;
items.push(DescriptionItem {
meta: NodeMeta::new(Some(Span::new(item_start, item_end))),
term: parse_inlines(
&term.source,
term.source_offset,
options,
definitions,
diagnostics,
),
details,
});
let next_item = next_nonblank_line(lines, cursor);
if next_item >= lines.len() {
cursor = next_item;
break;
}
if description_term(lines, next_item, options).is_some() {
if next_item != cursor {
tight = false;
}
cursor = next_item;
continue;
}
cursor = next_item;
break;
}
(!items.is_empty()).then_some((
Block::DescriptionList(DescriptionList {
meta: NodeMeta::new(Some(Span::new(lines[index].start, list_end))),
tight,
children: items,
}),
cursor,
))
}
fn parse_description_details(
lines: &[Line<'_>],
index: usize,
marker: DescriptionMarker<'_>,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<(DescriptionDetails, usize, bool)> {
let mut content = String::new();
push_line(&mut content, marker.content);
let mut cursor = index + 1;
let mut end = lines[index].end_with_eol;
let mut tight = true;
let mut paragraph_open = paragraph_stays_open(marker.content, options);
while cursor < lines.len() {
if lines[cursor].text.trim().is_empty() {
let next = next_nonblank_line(lines, cursor + 1);
if next >= lines.len() || description_term(lines, next, options).is_some() {
cursor = next;
break;
}
if description_marker(lines[next].text).is_some() {
tight = false;
cursor = next;
break;
}
if strip_indent_continuation(lines[next].text).is_none() {
break;
}
push_line(&mut content, "");
paragraph_open = false;
tight = false;
end = lines[cursor].end_with_eol;
cursor += 1;
continue;
}
if description_marker(lines[cursor].text).is_some()
|| description_term(lines, cursor, options).is_some()
{
break;
}
let continuation = if let Some(continuation) = strip_indent_continuation(lines[cursor].text)
{
continuation
} else if paragraph_open && !likely_block_start(lines[cursor].text, options) {
trim_ascii_start(lines[cursor].text)
} else {
break;
};
paragraph_open = paragraph_stays_open(continuation, options);
push_line(&mut content, continuation);
end = lines[cursor].end_with_eol;
cursor += 1;
}
if content.trim().is_empty() {
return None;
}
Some((
DescriptionDetails {
meta: NodeMeta::new(Some(Span::new(lines[index].start, end))),
children: parse_blocks(
&content,
lines[index].start + marker.content_offset,
false,
options,
definitions,
diagnostics,
depth + 1,
),
},
cursor,
tight,
))
}
fn description_term(
lines: &[Line<'_>],
term_index: usize,
options: &SyntaxOptions,
) -> Option<DescriptionTerm> {
if term_index >= lines.len() || !is_description_term_line(lines[term_index].text, options) {
return None;
}
let mut source = String::new();
let mut term_end = term_index;
let mut cursor = term_index;
while cursor < lines.len() && is_description_term_line(lines[cursor].text, options) {
if !source.is_empty() {
source.push('\n');
}
source.push_str(trim_ascii_start(lines[cursor].text).trim_end());
term_end = cursor;
cursor += 1;
}
let mut marker_index = cursor;
let mut blank_after_term = false;
while marker_index < lines.len() && lines[marker_index].text.trim().is_empty() {
blank_after_term = true;
marker_index += 1;
}
(marker_index < lines.len() && description_marker(lines[marker_index].text).is_some()).then(
|| DescriptionTerm {
marker_index,
term_end,
blank_after_term,
source,
source_offset: lines[term_index].start + leading_trim_bytes(lines[term_index].text),
},
)
}
fn is_description_term_line(line: &str, options: &SyntaxOptions) -> bool {
leading_indent_columns(line) <= 3
&& !line.trim().is_empty()
&& description_marker(line).is_none()
&& !likely_block_start(line, options)
}
fn description_marker(line: &str) -> Option<DescriptionMarker<'_>> {
let (columns, bytes) = leading_indent(line);
if columns > 2 || !matches!(line.as_bytes().get(bytes), Some(b':' | b'~')) {
return None;
}
if line
.as_bytes()
.get(bytes + 1)
.is_some_and(|byte| !matches!(*byte, b' ' | b'\t'))
{
return None;
}
let mut content_offset = bytes + 1;
while line
.as_bytes()
.get(content_offset)
.is_some_and(|byte| matches!(*byte, b' ' | b'\t'))
{
content_offset += 1;
}
Some(DescriptionMarker {
content_offset,
content: &line[content_offset..],
})
}
fn paragraph_stays_open(line: &str, options: &SyntaxOptions) -> bool {
!line.trim().is_empty() && !likely_block_start(line, options)
}
fn strip_indent_continuation(input: &str) -> Option<&str> {
input
.strip_prefix(" ")
.or_else(|| input.strip_prefix('\t'))
}
fn parse_atx_heading(
line: Line<'_>,
options: &SyntaxOptions,
definitions: &[String],
) -> Option<Block> {
let text = trim_up_to_three_spaces(line.text)?;
let depth = text
.as_bytes()
.iter()
.take_while(|byte| **byte == b'#')
.count();
if depth == 0 || depth > 6 {
return None;
}
if text
.as_bytes()
.get(depth)
.is_some_and(|byte| !matches!(*byte, b' ' | b'\t'))
&& text.len() != depth
{
return None;
}
let after_opening = &text[depth..];
let content_start_in_text = depth + leading_trim_bytes(after_opening);
let content = trim_closing_hashes(after_opening.trim_start());
let content_start = line.start + (line.text.len() - text.len()) + content_start_in_text;
Some(Block::Heading(Heading {
meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
depth: depth as u8,
kind: HeadingKind::Atx,
children: parse_inlines(
content,
content_start,
options,
definitions,
&mut Vec::new(),
),
}))
}
fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
let text = trim_up_to_three_spaces(line.text)?.trim();
let mut marker = None;
let mut count = 0;
for char in text.chars() {
if char == ' ' || char == '\t' {
continue;
}
let current = match char {
'-' => ThematicBreakMarker::Dash,
'*' => ThematicBreakMarker::Asterisk,
'_' => ThematicBreakMarker::Underscore,
_ => return None,
};
if marker.is_some_and(|marker| marker != current) {
return None;
}
marker = Some(current);
count += 1;
}
if count >= 3 {
Some(Block::ThematicBreak(ThematicBreak {
meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
marker: marker?,
}))
} else {
None
}
}
fn parse_definition(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
allow_subsequent_indent: bool,
) -> Option<(Block, usize)> {
let line = lines[index];
let text = trim_definition_start(line.text, allow_subsequent_indent)?;
if !text.starts_with('[') {
return None;
}
let mut accumulated = String::from(text);
let mut label_end_line = index;
let close = loop {
if let Some(close) = find_reference_label_end(&accumulated, 0) {
if accumulated.as_bytes().get(close + 1) == Some(&b':') {
break close;
}
return None;
}
let next = label_end_line + 1;
if next >= lines.len() || lines[next].text.trim().is_empty() {
return None;
}
if likely_block_start(lines[next].text, options)
|| setext_underline_depth(lines[next].text).is_some()
|| table_can_start(lines, next, options)
{
return None;
}
accumulated.push('\n');
accumulated.push_str(lines[next].text);
label_end_line = next;
};
let label = String::from(&accumulated[1..close]);
if normalize_label(&label).is_empty() {
return None;
}
let label = label.as_str();
let mut source = String::from(&accumulated[close + 2..]);
let mut cursor = label_end_line;
let mut best_without_title = None;
loop {
if let Some(resource) = parse_definition_destination_title(&source) {
if resource.title.is_some() {
return Some((
Block::Definition(Definition {
meta: NodeMeta::new(Some(Span::new(
line.start,
lines[cursor].end_with_eol,
))),
label: label.into(),
identifier: normalize_label(label),
destination: resource.destination,
destination_kind: resource.destination_kind,
title: resource.title,
title_kind: resource.title_kind,
}),
cursor + 1,
));
}
best_without_title = Some((resource, cursor + 1));
let next = cursor + 1;
if next >= lines.len()
|| lines[next].text.trim().is_empty()
|| !line_can_start_definition_title(lines[next].text)
{
break;
}
}
let next = cursor + 1;
if next >= lines.len() || lines[next].text.trim().is_empty() {
break;
}
if likely_block_start(lines[next].text, options)
|| setext_underline_depth(lines[next].text).is_some()
{
break;
}
source.push('\n');
source.push_str(lines[next].text);
cursor = next;
}
let (resource, next) = best_without_title?;
let end = lines[next - 1].end_with_eol;
Some((
Block::Definition(Definition {
meta: NodeMeta::new(Some(Span::new(line.start, end))),
label: label.into(),
identifier: normalize_label(label),
destination: resource.destination,
destination_kind: resource.destination_kind,
title: resource.title,
title_kind: resource.title_kind,
}),
next,
))
}
fn trim_definition_start(input: &str, allow_subsequent_indent: bool) -> Option<&str> {
if let Some(trimmed) = trim_up_to_three_spaces(input) {
return Some(trimmed);
}
if allow_subsequent_indent {
let (columns, bytes) = leading_indent(input);
if columns == 4 {
return Some(&input[bytes..]);
}
}
None
}
fn parse_footnote_definition(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Option<(Block, usize)> {
if !options.constructs.footnote_definition {
return None;
}
let line = lines[index];
let text = line.text.trim();
if !text.starts_with("[^") {
return None;
}
let close = find_footnote_definition_label_end(text)?;
let label = &text[2..close];
if !is_footnote_label(label) {
return None;
}
let rest = text[close + 2..].trim();
let mut content = String::new();
push_line(&mut content, rest);
let mut cursor = index + 1;
let mut end = line.end_with_eol;
let mut paragraph_open = paragraph_stays_open(rest, options);
while cursor < lines.len() {
if lines[cursor].text.trim().is_empty() {
let next = next_nonblank_line(lines, cursor + 1);
if next >= lines.len() || !is_footnote_continuation(lines[next].text) {
break;
}
push_line(&mut content, "");
paragraph_open = false;
end = lines[cursor].end_with_eol;
cursor += 1;
continue;
}
let continuation = if let Some(continuation) = strip_indent_continuation(lines[cursor].text)
{
continuation
} else if paragraph_open && !likely_block_start(lines[cursor].text, options) {
trim_ascii_start(lines[cursor].text)
} else {
break;
};
paragraph_open = paragraph_stays_open(continuation, options);
push_line(&mut content, continuation);
end = lines[cursor].end_with_eol;
cursor += 1;
}
Some((
Block::FootnoteDefinition(FootnoteDefinition {
meta: NodeMeta::new(Some(Span::new(line.start, end))),
label: label.into(),
identifier: normalize_label(label),
children: parse_blocks(
&content,
line.end.saturating_sub(rest.len()),
false,
options,
definitions,
diagnostics,
depth + 1,
),
}),
cursor,
))
}
fn is_footnote_continuation(input: &str) -> bool {
strip_indent_continuation(input).is_some()
}
fn parse_leaf_directive(
line: Line<'_>,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> Option<Block> {
if !options.constructs.directive_leaf {
return None;
}
let trimmed = line.text.trim_start();
if trimmed.starts_with(":::") || !trimmed.starts_with("::") {
return None;
}
let opener_base = line.start + (line.text.len() - trimmed.len()) + 2;
let Some((name, label_source, attributes, _)) = parse_directive_opener(&trimmed[2..]) else {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidDirectiveName,
Span::new(line.start, line.end),
"leaf directive must have a valid name",
));
return None;
};
let label = label_source
.map(|source| {
parse_inlines(
source,
opener_base + name.len() + 1,
options,
definitions,
diagnostics,
)
})
.unwrap_or_default();
Some(Block::LeafDirective(LeafDirective {
meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
name,
label,
attributes,
}))
}
fn parse_html_container(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
container_closes: &mut BracketMemo,
) -> Option<(Block, usize)> {
if !options.constructs.html_container {
return None;
}
let (opening, leading_summary) =
parse_details_container_opening_line(lines[index], options, definitions, diagnostics)?;
let close_index = container_closes.resolve(lines.len() + 1, index + 1, |cursor| {
html_container_close_step(lines, cursor, "details")
})?;
let closing =
parse_html_container_tag_line(lines[close_index], "details", HtmlContainerTag::Closing)?;
let children = parse_details_container_children(
&lines[index + 1..close_index],
leading_summary,
options,
definitions,
diagnostics,
depth,
);
Some((
Block::HtmlContainer(HtmlContainer {
meta: NodeMeta::new(Some(Span::new(
opening
.meta
.span
.map(|span| span.start)
.unwrap_or(lines[index].start),
lines[close_index].end_with_eol,
))),
opening,
content: HtmlContainerContent::Blocks(children),
closing,
}),
close_index + 1,
))
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum HtmlContainerTag {
Opening,
Closing,
}
#[derive(Clone, Copy)]
struct HtmlContainerFence {
marker: FenceMarker,
length: usize,
}
fn parse_details_container_children(
lines: &[Line<'_>],
leading_summary: Option<Block>,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Vec<Block> {
let mut children = Vec::new();
if let Some(summary) = leading_summary {
children.push(summary);
children.extend(parse_blocks_from_lines(
lines,
false,
options,
definitions,
diagnostics,
depth + 1,
));
return children;
}
let Some(summary_index) = lines.iter().position(|line| !line.text.trim().is_empty()) else {
return children;
};
if let Some((summary, next)) =
parse_summary_container(lines, summary_index, options, definitions, diagnostics)
{
children.push(summary);
children.extend(parse_blocks_from_lines(
&lines[next..],
false,
options,
definitions,
diagnostics,
depth + 1,
));
return children;
}
parse_blocks_from_lines(lines, false, options, definitions, diagnostics, depth + 1)
}
fn parse_summary_container(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(Block, usize)> {
let line = lines[index];
let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
parse_summary_container_source(
trimmed,
line.start + indent_bytes,
options,
definitions,
diagnostics,
)
.map(|block| (block, index + 1))
}
fn parse_details_container_opening_line(
line: Line<'_>,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(HtmlTag, Option<Block>)> {
let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
let (open_end, name) = parse_html_tag(trimmed, 0)?;
if !name.eq_ignore_ascii_case("details")
|| html_tag_is_closing(trimmed, 0)
|| html_tag_is_self_closing(&trimmed[..open_end])
{
return None;
}
let tag_start = line.start + indent_bytes;
let rest_start = open_end + leading_ascii_whitespace_len(&trimmed[open_end..]);
let summary = if trimmed[rest_start..].is_empty() {
None
} else {
Some(parse_summary_container_source(
&trimmed[rest_start..],
tag_start + rest_start,
options,
definitions,
diagnostics,
)?)
};
Some((
HtmlTag {
meta: NodeMeta::new(Some(Span::new(tag_start, tag_start + open_end))),
name: "details".into(),
raw: trimmed[..open_end].into(),
},
summary,
))
}
fn parse_summary_container_source(
source: &str,
base_offset: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> Option<Block> {
let (open_end, open_name) = parse_html_tag(source, 0)?;
if !open_name.eq_ignore_ascii_case("summary")
|| html_tag_is_closing(source, 0)
|| html_tag_is_self_closing(&source[..open_end])
{
return None;
}
let close_start = source[open_end..]
.find("</")
.map(|offset| open_end + offset)?;
let (close_end, close_name) = parse_html_tag(source, close_start)?;
if !close_name.eq_ignore_ascii_case("summary")
|| !html_tag_is_closing(source, close_start)
|| !source[close_end..].trim().is_empty()
{
return None;
}
let content = &source[open_end..close_start];
let content_base = base_offset + open_end;
let opening = HtmlTag {
meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + open_end))),
name: "summary".into(),
raw: source[..open_end].into(),
};
let closing = HtmlTag {
meta: NodeMeta::new(Some(Span::new(
base_offset + close_start,
base_offset + close_end,
))),
name: "summary".into(),
raw: source[close_start..close_end].into(),
};
Some(Block::HtmlContainer(HtmlContainer {
meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + source.len()))),
opening,
content: HtmlContainerContent::Inlines(parse_inlines(
content,
content_base,
options,
definitions,
diagnostics,
)),
closing,
}))
}
fn html_container_close_step(lines: &[Line<'_>], cursor: usize, tag: &str) -> BracketStep {
let Some(line) = lines.get(cursor) else {
return BracketStep::End;
};
if let Some(fence) = html_container_fence_opens(line.text) {
let after_fence = (cursor + 1..lines.len())
.find(|close| html_container_fence_closes(lines[*close].text, fence))
.map_or(lines.len(), |close| close + 1);
return BracketStep::Pass(after_fence);
}
if parse_html_container_tag_line(*line, tag, HtmlContainerTag::Closing).is_some() {
return BracketStep::Close(cursor + 1);
}
if parse_html_container_opening_line(*line, tag).is_some() {
return BracketStep::Open(cursor + 1);
}
BracketStep::Pass(cursor + 1)
}
fn parse_html_container_tag_line(
line: Line<'_>,
tag: &str,
kind: HtmlContainerTag,
) -> Option<HtmlTag> {
let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
let (end, name) = parse_html_tag(trimmed, 0)?;
if !name.eq_ignore_ascii_case(tag) || !trimmed[end..].trim().is_empty() {
return None;
}
let closing = html_tag_is_closing(trimmed, 0);
if (kind == HtmlContainerTag::Opening && closing)
|| (kind == HtmlContainerTag::Closing && !closing)
|| html_tag_is_self_closing(&trimmed[..end])
{
return None;
}
let start = line.start + indent_bytes;
Some(HtmlTag {
meta: NodeMeta::new(Some(Span::new(start, start + end))),
name: tag.into(),
raw: trimmed[..end].into(),
})
}
fn parse_html_container_opening_line(line: Line<'_>, tag: &str) -> Option<HtmlTag> {
let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
let (end, name) = parse_html_tag(trimmed, 0)?;
if !name.eq_ignore_ascii_case(tag)
|| html_tag_is_closing(trimmed, 0)
|| html_tag_is_self_closing(&trimmed[..end])
{
return None;
}
let rest_start = end + leading_ascii_whitespace_len(&trimmed[end..]);
if !trimmed[rest_start..].is_empty() && !html_container_line_has_summary(&trimmed[rest_start..])
{
return None;
}
let start = line.start + indent_bytes;
Some(HtmlTag {
meta: NodeMeta::new(Some(Span::new(start, start + end))),
name: tag.into(),
raw: trimmed[..end].into(),
})
}
fn html_container_line_has_summary(input: &str) -> bool {
let Some((open_end, name)) = parse_html_tag(input, 0) else {
return false;
};
if !name.eq_ignore_ascii_case("summary")
|| html_tag_is_closing(input, 0)
|| html_tag_is_self_closing(&input[..open_end])
{
return false;
}
let Some(close_start) = input[open_end..].find("</").map(|offset| open_end + offset) else {
return false;
};
let Some((close_end, close_name)) = parse_html_tag(input, close_start) else {
return false;
};
close_name.eq_ignore_ascii_case("summary")
&& html_tag_is_closing(input, close_start)
&& input[close_end..].trim().is_empty()
}
fn trim_html_container_line(input: &str) -> Option<(&str, usize)> {
let trimmed = trim_up_to_three_spaces(input)?;
Some((trimmed, input.len() - trimmed.len()))
}
fn leading_ascii_whitespace_len(input: &str) -> usize {
input
.as_bytes()
.iter()
.take_while(|byte| byte.is_ascii_whitespace())
.count()
}
fn html_tag_is_closing(input: &str, index: usize) -> bool {
input.as_bytes().get(index + 1) == Some(&b'/')
}
fn html_tag_is_self_closing(input: &str) -> bool {
input.trim_end().ends_with("/>")
}
fn html_container_fence_opens(line: &str) -> Option<HtmlContainerFence> {
let trimmed = trim_up_to_three_spaces(line)?;
let (marker, length) = fence_start(trimmed)?;
Some(HtmlContainerFence { marker, length })
}
fn html_container_fence_closes(line: &str, fence: HtmlContainerFence) -> bool {
trim_up_to_three_spaces(line)
.is_some_and(|trimmed| fence_close(trimmed, fence.marker, fence.length))
}
fn parse_html_block(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
) -> Option<(Block, usize)> {
if !options.constructs.html_block {
return None;
}
let trimmed = trim_up_to_three_spaces(lines[index].text)?;
let kind = html_block_start(trimmed)?;
let mut value = String::new();
let mut cursor = index;
match kind {
HtmlBlockKind::RawTag => {
while cursor < lines.len() {
push_line(&mut value, lines[cursor].text);
if ["script", "pre", "style", "textarea"]
.iter()
.any(|tag| line_contains_raw_closing_tag(lines[cursor].text, tag))
{
cursor += 1;
break;
}
cursor += 1;
}
}
HtmlBlockKind::BlockTag => {
while cursor < lines.len() && !lines[cursor].text.trim().is_empty() {
push_line(&mut value, lines[cursor].text);
cursor += 1;
}
}
HtmlBlockKind::Until(end) => {
while cursor < lines.len() {
push_line(&mut value, lines[cursor].text);
if lines[cursor].text.contains(end) {
cursor += 1;
break;
}
cursor += 1;
}
}
HtmlBlockKind::UntilBlank => {
while cursor < lines.len() && !lines[cursor].text.trim().is_empty() {
push_line(&mut value, lines[cursor].text);
cursor += 1;
}
}
}
Some((
Block::HtmlBlock(HtmlBlock {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[cursor - 1].end_with_eol,
))),
value,
}),
cursor,
))
}
fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
let trimmed = input.trim_end();
if !trimmed.starts_with('<') {
return None;
}
if raw_html_tag_start(trimmed) {
return Some(HtmlBlockKind::RawTag);
}
if trimmed.starts_with("<!--") {
return Some(HtmlBlockKind::Until("-->"));
}
if trimmed.starts_with("<?") {
return Some(HtmlBlockKind::Until("?>"));
}
if is_declaration_start(trimmed) {
return Some(HtmlBlockKind::Until(">"));
}
if trimmed.starts_with("<![CDATA[") {
return Some(HtmlBlockKind::Until("]]>"));
}
if html_block_tag_start(trimmed) {
return Some(HtmlBlockKind::BlockTag);
}
let Some((end, _tag_name)) = parse_html_tag(trimmed, 0) else {
return None;
};
let rest = trimmed[end..].trim();
if rest.is_empty() {
Some(HtmlBlockKind::UntilBlank)
} else {
None
}
}
pub(crate) fn line_starts_html_block(input: &str) -> bool {
trim_up_to_three_spaces(input)
.and_then(html_block_start)
.is_some()
}
fn line_starts_html_container(input: &str) -> bool {
let line = Line {
text: input,
eol: "",
start: 0,
end: input.len(),
end_with_eol: input.len(),
lazy: false,
};
parse_html_container_opening_line(line, "details").is_some()
}
fn raw_html_tag_start(input: &str) -> bool {
for tag in ["script", "pre", "style", "textarea"] {
if html_raw_open_tag_prefix(input, tag) {
return true;
}
}
false
}
fn html_raw_open_tag_prefix(input: &str, tag: &str) -> bool {
let Some(rest) = input.strip_prefix('<') else {
return false;
};
if rest.starts_with('/') || rest.len() < tag.len() {
return false;
}
let rest_bytes = rest.as_bytes();
let tag_bytes = tag.as_bytes();
if !rest_bytes
.get(..tag_bytes.len())
.is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
{
return false;
}
match rest_bytes.get(tag.len()) {
None => true,
Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
Some(b'/') => {
rest_bytes.get(tag.len() + 1) == Some(&b'>') && rest_bytes.get(tag.len() + 2).is_none()
}
_ => false,
}
}
fn line_contains_raw_closing_tag(input: &str, tag: &str) -> bool {
let bytes = input.as_bytes();
let tag_bytes = tag.as_bytes();
let mut cursor = 0;
while cursor + 2 + tag_bytes.len() <= bytes.len() {
let tag_start = cursor + 2;
let tag_end = tag_start + tag_bytes.len();
if bytes.get(cursor) == Some(&b'<')
&& bytes.get(cursor + 1) == Some(&b'/')
&& bytes
.get(tag_start..tag_end)
.is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
{
match bytes.get(tag_end) {
Some(b'>') => return true,
Some(byte) if byte.is_ascii_whitespace() => {
let mut after_space = tag_end;
while bytes
.get(after_space)
.is_some_and(|byte| byte.is_ascii_whitespace())
{
after_space += 1;
}
if bytes.get(after_space) == Some(&b'>') {
return true;
}
}
_ => {}
}
}
cursor += 1;
}
false
}
fn html_block_tag_start(input: &str) -> bool {
let bytes = input.as_bytes();
if bytes.first() != Some(&b'<') {
return false;
}
let mut cursor = 1;
if bytes.get(cursor) == Some(&b'/') {
cursor += 1;
}
let name_start = cursor;
if !bytes
.get(cursor)
.is_some_and(|byte| byte.is_ascii_alphabetic())
{
return false;
}
cursor += 1;
while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
cursor += 1;
}
let name = &input[name_start..cursor];
if !html_block_tag(name) {
return false;
}
match bytes.get(cursor) {
None | Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => true,
_ => false,
}
}
fn html_block_tag(tag: &str) -> bool {
matches!(
tag.to_ascii_lowercase().as_str(),
"address"
| "article"
| "aside"
| "base"
| "basefont"
| "blockquote"
| "body"
| "caption"
| "center"
| "col"
| "colgroup"
| "dd"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "dt"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "form"
| "frame"
| "frameset"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "head"
| "header"
| "hr"
| "html"
| "iframe"
| "legend"
| "li"
| "link"
| "main"
| "menu"
| "menuitem"
| "nav"
| "noframes"
| "ol"
| "optgroup"
| "option"
| "p"
| "param"
| "search"
| "section"
| "summary"
| "table"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "title"
| "tr"
| "track"
| "ul"
)
}
fn is_declaration_start(input: &str) -> bool {
input
.as_bytes()
.get(2)
.is_some_and(|byte| input.starts_with("<!") && byte.is_ascii_alphabetic())
}
fn parse_mdx_flow(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
diagnostics: &mut Vec<Diagnostic>,
flow: &mut MdxFlowScan,
) -> Option<(Block, usize)> {
if options.constructs.mdx_esm {
if let Some((block, next)) = parse_mdx_esm_flow(lines, index, diagnostics) {
return Some((block, next));
}
}
let line = lines[index];
let trimmed = line.text.trim_start();
if options.constructs.mdx_expression_block && trimmed.starts_with('{') {
let open_byte = line.text.len() - trimmed.len();
if let Some((close_line, close_byte)) = flow.expression_close(lines, index, open_byte) {
return Some((
Block::MdxExpression(MdxExpression {
meta: NodeMeta::new(Some(Span::new(line.start, lines[close_line].end))),
value: collect_mdx_expression_value(
lines, index, open_byte, close_line, close_byte,
),
}),
close_line + 1,
));
}
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidMdx,
Span::new(line.start + open_byte, lines.last()?.end_with_eol),
"MDX expression block is missing a closing brace",
));
}
if options.constructs.mdx_jsx_block && trimmed.starts_with('<') {
let start_byte = line.text.len() - trimmed.len();
if let Some(close_line) = flow.jsx_close_line(lines, index, start_byte) {
return Some((
Block::MdxJsx(MdxJsx {
meta: NodeMeta::new(Some(Span::new(line.start, lines[close_line].end))),
value: collect_line_range(lines, index, close_line),
}),
close_line + 1,
));
}
if let Some(root) = mdx_jsx_tag_start(line.text, start_byte) {
if !root.closing {
if let Some(self_closing) = flow.jsx_tag_self_closing(lines, index, start_byte) {
if !self_closing {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidMdx,
Span::new(line.start + start_byte, lines.last()?.end_with_eol),
"MDX JSX block is missing a closing tag",
));
}
}
}
}
}
None
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct MdxEsmState {
brace_depth: usize,
bracket_depth: usize,
paren_depth: usize,
block_comment: bool,
quote: Option<u8>,
escaped: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MdxJsxTag<'a> {
Fragment,
Named(&'a str),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct MdxJsxTagStart<'a> {
tag: MdxJsxTag<'a>,
closing: bool,
}
fn parse_mdx_esm_flow(
lines: &[Line<'_>],
index: usize,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(Block, usize)> {
if !is_mdx_esm_start(lines[index].text) {
return None;
}
let mut value = String::new();
let mut state = MdxEsmState::default();
let mut cursor = index;
while cursor < lines.len() {
let line = lines[cursor].text;
if cursor > index && !is_mdx_esm_continuation(line, &state) {
break;
}
if cursor > index {
value.push('\n');
}
value.push_str(line);
update_mdx_esm_state(line, &mut state);
cursor += 1;
}
if cursor >= lines.len() && state_has_open_mdx_esm_construct(&state) {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidMdx,
Span::new(lines[index].start, lines[cursor - 1].end_with_eol),
"MDX ESM block is missing a closing delimiter",
));
}
Some((
Block::MdxEsm(MdxEsm {
meta: NodeMeta::new(Some(Span::new(lines[index].start, lines[cursor - 1].end))),
value,
}),
cursor,
))
}
fn is_mdx_esm_start(line: &str) -> bool {
line.starts_with("import ") || line.starts_with("export ")
}
fn is_mdx_esm_continuation(line: &str, state: &MdxEsmState) -> bool {
if state_has_open_mdx_esm_construct(state) {
return true;
}
let trimmed = line.trim_start();
if trimmed.is_empty() {
return false;
}
is_mdx_esm_start(line) || trimmed.starts_with("//") || trimmed.starts_with("/*")
}
fn state_has_open_mdx_esm_construct(state: &MdxEsmState) -> bool {
state.brace_depth > 0
|| state.bracket_depth > 0
|| state.paren_depth > 0
|| state.block_comment
|| state.quote == Some(b'`')
}
fn update_mdx_esm_state(line: &str, state: &mut MdxEsmState) {
let bytes = line.as_bytes();
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
if state.block_comment {
if byte == b'*' && bytes.get(index + 1) == Some(&b'/') {
state.block_comment = false;
index += 1;
}
index += 1;
continue;
}
if let Some(delimiter) = state.quote {
if state.escaped {
state.escaped = false;
} else if byte == b'\\' {
state.escaped = true;
} else if byte == delimiter {
state.quote = None;
}
index += 1;
continue;
}
match byte {
b'\'' | b'"' | b'`' => state.quote = Some(byte),
b'/' if bytes.get(index + 1) == Some(&b'/') => break,
b'/' if bytes.get(index + 1) == Some(&b'*') => {
state.block_comment = true;
index += 1;
}
b'{' => state.brace_depth += 1,
b'}' => state.brace_depth = state.brace_depth.saturating_sub(1),
b'[' => state.bracket_depth += 1,
b']' => state.bracket_depth = state.bracket_depth.saturating_sub(1),
b'(' => state.paren_depth += 1,
b')' => state.paren_depth = state.paren_depth.saturating_sub(1),
_ => {}
}
index += 1;
}
}
const MDX_BRACE_STATES: usize = 6;
const MDX_NORMAL: usize = 0;
const MDX_SINGLE_QUOTED: usize = 1;
const MDX_DOUBLE_QUOTED: usize = 2;
const MDX_TEMPLATE: usize = 3;
const MDX_LINE_COMMENT: usize = 4;
const MDX_BLOCK_COMMENT: usize = 5;
fn after_escaped_byte(text: &str, escape: usize, lines_joined: bool) -> usize {
let mut escaped = escape + 1;
if lines_joined {
while text.as_bytes().get(escaped) == Some(&b'\n') {
escaped += 1;
}
}
(escaped + 1).min(text.len())
}
fn mdx_expression_step(input: &str, node: usize, lines_joined: bool) -> BracketStep {
let bytes = input.as_bytes();
let (cursor, state) = (node / MDX_BRACE_STATES, node % MDX_BRACE_STATES);
let at = |position: usize, state: usize| position.min(bytes.len()) * MDX_BRACE_STATES + state;
let Some(&byte) = bytes.get(cursor) else {
return BracketStep::End;
};
let following = bytes.get(cursor + 1).copied();
match state {
MDX_NORMAL => match byte {
b'\'' => BracketStep::Pass(at(cursor + 1, MDX_SINGLE_QUOTED)),
b'"' => BracketStep::Pass(at(cursor + 1, MDX_DOUBLE_QUOTED)),
b'`' => BracketStep::Pass(at(cursor + 1, MDX_TEMPLATE)),
b'/' if following == Some(b'/') => BracketStep::Pass(at(cursor + 2, MDX_LINE_COMMENT)),
b'/' if following == Some(b'*') => BracketStep::Pass(at(cursor + 2, MDX_BLOCK_COMMENT)),
b'{' => BracketStep::Open(at(cursor + 1, MDX_NORMAL)),
b'}' => BracketStep::Close(at(cursor + 1, MDX_NORMAL)),
_ => BracketStep::Pass(at(cursor + 1, MDX_NORMAL)),
},
MDX_LINE_COMMENT => BracketStep::Pass(at(
cursor + 1,
if byte == b'\n' {
MDX_NORMAL
} else {
MDX_LINE_COMMENT
},
)),
MDX_BLOCK_COMMENT => {
if byte == b'*' && following == Some(b'/') {
BracketStep::Pass(at(cursor + 2, MDX_NORMAL))
} else {
BracketStep::Pass(at(cursor + 1, MDX_BLOCK_COMMENT))
}
}
quoted => {
let delimiter = [b'\'', b'"', b'`'][quoted - MDX_SINGLE_QUOTED];
if byte == b'\\' {
BracketStep::Pass(at(after_escaped_byte(input, cursor, lines_joined), quoted))
} else if byte == delimiter {
BracketStep::Pass(at(cursor + 1, MDX_NORMAL))
} else {
BracketStep::Pass(at(cursor + 1, quoted))
}
}
}
}
fn collect_mdx_expression_value(
lines: &[Line<'_>],
start_line: usize,
open_byte: usize,
close_line: usize,
close_byte: usize,
) -> String {
let mut value = String::new();
let mut cursor = start_line;
while cursor <= close_line {
if cursor > start_line {
value.push('\n');
}
let line = lines[cursor].text;
let segment = if cursor == start_line && cursor == close_line {
&line[open_byte + 1..close_byte]
} else if cursor == start_line {
&line[open_byte + 1..]
} else if cursor == close_line {
&line[..close_byte]
} else {
line
};
value.push_str(segment);
cursor += 1;
}
value
}
fn mdx_jsx_tag_start(input: &str, start: usize) -> Option<MdxJsxTagStart<'_>> {
let bytes = input.as_bytes();
if bytes.get(start) != Some(&b'<') {
return None;
}
match bytes.get(start + 1) {
Some(b'>') => {
return Some(MdxJsxTagStart {
tag: MdxJsxTag::Fragment,
closing: false,
});
}
Some(b'/') if bytes.get(start + 2) == Some(&b'>') => {
return Some(MdxJsxTagStart {
tag: MdxJsxTag::Fragment,
closing: true,
});
}
Some(b'!' | b'?') | None => return None,
_ => {}
}
let closing = bytes.get(start + 1) == Some(&b'/');
let name_start = start + if closing { 2 } else { 1 };
if !bytes
.get(name_start)
.is_some_and(|byte| is_mdx_jsx_name_start_byte(*byte))
{
return None;
}
let mut name_end = name_start + 1;
while bytes
.get(name_end)
.is_some_and(|byte| is_mdx_jsx_name_byte(*byte))
{
name_end += 1;
}
if name_end == name_start {
return None;
}
if bytes
.get(name_end)
.is_some_and(|byte| !is_mdx_jsx_name_delimiter(*byte))
{
return None;
}
Some(MdxJsxTagStart {
tag: MdxJsxTag::Named(&input[name_start..name_end]),
closing,
})
}
fn previous_nonspace_before_text(input: &str, byte_index: usize) -> Option<u8> {
input.as_bytes()[..byte_index]
.iter()
.rev()
.copied()
.find(|byte| !byte.is_ascii_whitespace())
}
const JSX_TAG_QUOTE_STATES: usize = 3;
fn jsx_tag_end_step(
text: &str,
node: usize,
lines_joined: bool,
expression_close: &mut impl FnMut(usize) -> Option<usize>,
) -> Step {
let (cursor, quote) = (node / JSX_TAG_QUOTE_STATES, node % JSX_TAG_QUOTE_STATES);
let at = |position: usize, quote: usize| Step::Next(position * JSX_TAG_QUOTE_STATES + quote);
let Some(&byte) = text.as_bytes().get(cursor) else {
return Step::Done(None);
};
if quote != 0 {
let delimiter = if quote == 1 { b'\'' } else { b'"' };
return if byte == b'\\' {
at(after_escaped_byte(text, cursor, lines_joined), quote)
} else if byte == delimiter {
at(cursor + 1, 0)
} else {
at(cursor + 1, quote)
};
}
match byte {
b'\'' => at(cursor + 1, 1),
b'"' => at(cursor + 1, 2),
b'{' => match expression_close(cursor) {
Some(close) => at(close + 1, 0),
None => Step::Done(None),
},
b'>' => Step::Done(Some(cursor)),
_ => at(cursor + 1, 0),
}
}
struct JsxIndex {
starts: Vec<usize>,
closing: Vec<bool>,
ends: Vec<Option<usize>>,
self_closing: Vec<bool>,
next_same_name: Vec<usize>,
matches: BracketMemo,
}
impl JsxIndex {
fn new(text: &str, lines_joined: bool, expressions: &mut BracketMemo) -> Self {
let bytes = text.as_bytes();
let mut starts = Vec::new();
let mut closing = Vec::new();
let mut names = Vec::new();
for (position, byte) in bytes.iter().enumerate() {
if *byte == b'<' {
if let Some(tag) = mdx_jsx_tag_start(text, position) {
starts.push(position);
closing.push(tag.closing);
names.push(match tag.tag {
MdxJsxTag::Fragment => None,
MdxJsxTag::Named(name) => Some(name),
});
}
}
}
let mut tag_ends = PathMemo::default();
let mut self_closing_by_end = BTreeMap::new();
let mut ends = Vec::with_capacity(starts.len());
let mut self_closing = Vec::with_capacity(starts.len());
for &start in &starts {
let end = tag_ends.resolve(
(text.len() + 1) * JSX_TAG_QUOTE_STATES,
(start + 1) * JSX_TAG_QUOTE_STATES,
|node| {
jsx_tag_end_step(text, node, lines_joined, &mut |open| {
expressions
.resolve(
(text.len() + 1) * MDX_BRACE_STATES,
(open + 1) * MDX_BRACE_STATES,
|node| mdx_expression_step(text, node, lines_joined),
)
.map(|node| node / MDX_BRACE_STATES)
})
},
);
ends.push(end);
self_closing.push(end.is_some_and(|end| {
*self_closing_by_end
.entry(end)
.or_insert_with(|| previous_nonspace_before_text(text, end) == Some(b'/'))
}));
}
let mut name_ids = BTreeMap::new();
for name in &names {
let next_id = name_ids.len();
name_ids.entry(*name).or_insert(next_id);
}
let count = starts.len();
let mut maps = PersistentMap::new(name_ids.len());
let mut versions = alloc::vec![PersistentMap::EMPTY; count];
let mut next_same_name = alloc::vec![count; count];
for tag in (0..count).rev() {
let Some(end) = ends[tag] else {
continue;
};
let after = starts.partition_point(|start| *start <= end);
let rest = versions.get(after).copied().unwrap_or(PersistentMap::EMPTY);
let name = name_ids[&names[tag]];
next_same_name[tag] = maps.get(rest, name).unwrap_or(count);
versions[tag] = maps.insert(rest, name, tag);
}
Self {
starts,
closing,
ends,
self_closing,
next_same_name,
matches: BracketMemo::default(),
}
}
fn element_end(&mut self, start: usize) -> Option<usize> {
let tag = self.starts.binary_search(&start).ok()?;
if self.closing[tag] {
return None;
}
let end = self.ends[tag]?;
if self.self_closing[tag] {
return Some(end);
}
let count = self.starts.len();
let (closing, self_closing, next) =
(&self.closing, &self.self_closing, &self.next_same_name);
let close = self.matches.resolve(count + 1, next[tag], |tag| {
if tag == count {
BracketStep::End
} else if closing[tag] {
BracketStep::Close(next[tag])
} else if self_closing[tag] {
BracketStep::Pass(next[tag])
} else {
BracketStep::Open(next[tag])
}
})?;
self.ends[close]
}
fn tag_end(&self, start: usize) -> Option<(usize, bool)> {
let tag = self.starts.binary_search(&start).ok()?;
Some((self.ends[tag]?, self.self_closing[tag]))
}
}
#[derive(Default)]
struct MdxFlowScan {
joined: Option<JoinedLines>,
}
struct JoinedLines {
text: String,
line_starts: Vec<usize>,
expressions: BracketMemo,
jsx: Option<JsxIndex>,
}
impl JoinedLines {
fn new(lines: &[Line<'_>]) -> Self {
let mut text = String::new();
let mut line_starts = Vec::with_capacity(lines.len());
for (index, line) in lines.iter().enumerate() {
if index > 0 {
text.push('\n');
}
line_starts.push(text.len());
text.push_str(line.text);
}
Self {
text,
line_starts,
expressions: BracketMemo::default(),
jsx: None,
}
}
fn locate(&self, position: usize) -> (usize, usize) {
let line = self.line_starts.partition_point(|start| *start <= position) - 1;
(line, position - self.line_starts[line])
}
fn jsx(&mut self) -> &mut JsxIndex {
let Self {
text,
expressions,
jsx,
..
} = self;
jsx.get_or_insert_with(|| JsxIndex::new(text, true, expressions))
}
}
impl MdxFlowScan {
fn joined(&mut self, lines: &[Line<'_>]) -> &mut JoinedLines {
self.joined.get_or_insert_with(|| JoinedLines::new(lines))
}
fn expression_close(
&mut self,
lines: &[Line<'_>],
index: usize,
open_byte: usize,
) -> Option<(usize, usize)> {
let joined = self.joined(lines);
let open = joined.line_starts[index] + open_byte;
let text = &joined.text;
let start = (open + 1) * MDX_BRACE_STATES;
let close =
joined
.expressions
.resolve((text.len() + 1) * MDX_BRACE_STATES, start, |node| {
mdx_expression_step(text, node, true)
})?
/ MDX_BRACE_STATES;
let (line, byte) = joined.locate(close);
lines[line].text[byte + 1..]
.trim()
.is_empty()
.then_some((line, byte))
}
fn jsx_close_line(
&mut self,
lines: &[Line<'_>],
index: usize,
start_byte: usize,
) -> Option<usize> {
let joined = self.joined(lines);
let start = joined.line_starts[index] + start_byte;
let end = joined.jsx().element_end(start)?;
Some(joined.locate(end).0)
}
fn jsx_tag_self_closing(
&mut self,
lines: &[Line<'_>],
index: usize,
start_byte: usize,
) -> Option<bool> {
let joined = self.joined(lines);
let start = joined.line_starts[index] + start_byte;
joined
.jsx()
.tag_end(start)
.map(|(_, self_closing)| self_closing)
}
}
fn is_mdx_jsx_name_start_byte(byte: u8) -> bool {
byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
}
fn is_mdx_jsx_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b':' | b'_' | b'-' | b'$')
}
fn is_mdx_jsx_name_delimiter(byte: u8) -> bool {
byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>' | b'{' | b'}')
}
fn collect_line_range(lines: &[Line<'_>], start: usize, end: usize) -> String {
let mut value = String::new();
let mut cursor = start;
while cursor <= end {
if cursor > start {
value.push('\n');
}
value.push_str(lines[cursor].text);
cursor += 1;
}
value
}
fn parse_indented_code(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
) -> Option<(Block, usize)> {
if !options.constructs.indented_code || strip_indented_code_prefix(lines[index].text).is_none()
{
return None;
}
let mut value = String::new();
let mut cursor = index;
let mut content_end = index;
let mut content_end_len = 0usize;
while cursor < lines.len() {
if let Some(text) = strip_indented_code_prefix(lines[cursor].text) {
ensure_line_separator(&mut value);
value.push_str(text);
value.push_str(lines[cursor].eol);
if !text.trim().is_empty() {
content_end = cursor;
content_end_len = value.len();
}
cursor += 1;
continue;
}
if !lines[cursor].text.trim().is_empty() {
break;
}
ensure_line_separator(&mut value);
value.push_str(lines[cursor].eol);
cursor += 1;
}
value.truncate(content_end_len);
Some((
Block::CodeBlock(CodeBlock {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[content_end].end_with_eol,
))),
kind: CodeBlockKind::Indented,
info: None,
value,
}),
cursor,
))
}
fn strip_indented_code_prefix(input: &str) -> Option<&str> {
let mut column = 0usize;
for (index, byte) in input.as_bytes().iter().enumerate() {
match *byte {
b' ' => {
column += 1;
if column == 4 {
return Some(&input[index + 1..]);
}
}
b'\t' => {
column += 4 - (column % 4);
if column >= 4 {
return Some(&input[index + 1..]);
}
}
_ => return None,
}
}
None
}
fn parse_table(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(Block, usize)> {
if !options.constructs.gfm_table || index + 1 >= lines.len() {
return None;
}
let delimiter = table_indent_line(lines[index + 1].text, options.constructs.indented_code)?;
if list_marker_info(delimiter).is_some() {
return None;
}
if !table_has_separator(lines[index].text, delimiter, options.constructs.spoiler) {
return None;
}
let alignments = parse_table_delimiter(delimiter, options.constructs.spoiler)?;
let headers = split_table_row(lines[index].text, options.constructs.spoiler);
if headers.len() != alignments.len() {
return None;
}
let mut rows = Vec::new();
rows.push(TableRow {
meta: NodeMeta::new(Some(Span::new(lines[index].start, lines[index].end))),
cells: headers
.iter()
.map(|cell| TableCell {
meta: NodeMeta::default(),
children: parse_inlines(
cell.trim(),
lines[index].start,
options,
definitions,
diagnostics,
),
})
.collect(),
});
let mut cursor = index + 2;
while cursor < lines.len() {
let Some(row) = table_indent_line(lines[cursor].text, options.constructs.indented_code)
else {
break;
};
if row.trim().is_empty() || table_body_line_ends_table(lines[cursor].text, options) {
break;
}
let cells = split_table_row(row, options.constructs.spoiler);
rows.push(TableRow {
meta: NodeMeta::new(Some(Span::new(lines[cursor].start, lines[cursor].end))),
cells: alignments
.iter()
.enumerate()
.map(|(cell_index, _)| {
let value = cells.get(cell_index).map(String::as_str).unwrap_or("");
TableCell {
meta: NodeMeta::default(),
children: parse_inlines(
value.trim(),
lines[cursor].start,
options,
definitions,
diagnostics,
),
}
})
.collect(),
});
cursor += 1;
}
Some((
Block::Table(Table {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[cursor - 1].end_with_eol,
))),
alignments,
rows,
}),
cursor,
))
}
fn parse_setext_heading(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
) -> Option<(Block, usize)> {
if index + 1 >= lines.len() || lines[index].text.trim().is_empty() {
return None;
}
let mut underline_index = index + 1;
loop {
let underline_depth = if lines[underline_index].lazy {
None
} else {
setext_underline_depth(lines[underline_index].text)
};
if let Some(depth) = underline_depth {
let mut value = String::new();
for line in &lines[index..underline_index] {
push_line(&mut value, trim_ascii_start(line.text));
}
return Some((
Block::Heading(Heading {
meta: NodeMeta::new(Some(Span::new(
lines[index].start,
lines[underline_index].end,
))),
depth,
kind: HeadingKind::Setext,
children: parse_inlines(
&value,
lines[index].start,
options,
definitions,
&mut Vec::new(),
),
}),
underline_index + 1,
));
}
let line = lines[underline_index].text;
if line.trim().is_empty()
|| table_can_start(lines, underline_index, options)
|| likely_block_start(line, options)
{
return None;
}
underline_index += 1;
if underline_index >= lines.len() {
return None;
}
}
}
fn setext_underline_depth(input: &str) -> Option<u8> {
let underline = trim_up_to_three_spaces(input)?.trim();
match underline {
text if !text.is_empty() && text.chars().all(|char| char == '=') => Some(1),
text if !text.is_empty() && text.chars().all(|char| char == '-') => Some(2),
_ => None,
}
}
fn parse_paragraph(
lines: &[Line<'_>],
index: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> (Block, usize) {
let mut value = String::new();
let start = lines[index].start;
let mut cursor = index;
while cursor < lines.len() {
if lines[cursor].text.trim().is_empty() {
break;
}
if cursor > index && !lines[cursor].lazy {
if table_can_start(lines, cursor, options) {
break;
}
if likely_block_start(lines[cursor].text, options) {
break;
}
}
if !value.is_empty() {
value.push('\n');
}
value.push_str(trim_ascii_start(lines[cursor].text));
cursor += 1;
}
let end = lines[cursor - 1].end;
(
Block::Paragraph(Paragraph {
meta: NodeMeta::new(Some(Span::new(start, end))),
children: parse_inlines(&value, start, options, definitions, diagnostics),
}),
cursor,
)
}
#[derive(Clone, Copy)]
struct DelimMarker {
node_index: usize,
marker: u8,
length: usize,
can_open: bool,
can_close: bool,
single_open: bool,
single_close: bool,
strike: bool,
recloses: usize,
enclosed_from: usize,
position: usize,
line: usize,
}
impl DelimMarker {
fn nests(&self) -> bool {
self.marker != b'|'
}
}
const NIL: usize = usize::MAX;
const MAX_EMPHASIS_NESTING: usize = 16;
#[derive(Clone, Copy, Default)]
struct Nesting {
emphasis: usize,
total: usize,
}
impl Nesting {
fn max(self, other: Self) -> Self {
Self {
emphasis: self.emphasis.max(other.emphasis),
total: self.total.max(other.total),
}
}
}
struct DelimiterLinks {
prev: Vec<usize>,
next: Vec<usize>,
live: Vec<bool>,
nesting_after: Vec<Nesting>,
}
impl DelimiterLinks {
fn new(count: usize) -> Self {
Self {
prev: (0..count).map(|index| index.wrapping_sub(1)).collect(),
next: (1..=count)
.map(|index| if index < count { index } else { NIL })
.collect(),
live: alloc::vec![true; count],
nesting_after: alloc::vec![Nesting::default(); count],
}
}
fn unlink(&mut self, index: usize) {
let (before, after) = (self.prev[index], self.next[index]);
self.live[index] = false;
if before != NIL {
self.next[before] = after;
self.nesting_after[before] = self.nesting_after[before].max(self.nesting_after[index]);
}
if after != NIL {
self.prev[after] = before;
}
}
fn close_span(&mut self, opener: usize, closer: usize) -> Nesting {
let mut nesting = self.nesting_after[opener];
let mut inner = self.next[opener];
while inner != closer {
nesting = nesting.max(self.nesting_after[inner]);
self.live[inner] = false;
inner = self.next[inner];
}
self.next[opener] = closer;
self.prev[closer] = opener;
nesting
}
}
struct InlineList {
slots: Vec<Option<Inline>>,
prev: Vec<usize>,
next: Vec<usize>,
head: usize,
}
impl InlineList {
fn new(nodes: Vec<Inline>) -> Self {
let count = nodes.len();
Self {
slots: nodes.into_iter().map(Some).collect(),
prev: (0..count).map(|slot| slot.wrapping_sub(1)).collect(),
next: (1..=count)
.map(|slot| if slot < count { slot } else { NIL })
.collect(),
head: if count == 0 { NIL } else { 0 },
}
}
fn node_mut(&mut self, slot: usize) -> &mut Inline {
self.slots[slot]
.as_mut()
.expect("delimiter placeholder slot is live")
}
fn wrap_between(
&mut self,
before: usize,
after: usize,
wrap: impl FnOnce(Vec<Inline>) -> Inline,
) {
let mut children = Vec::new();
let mut cursor = self.next[before];
while cursor != after {
children.push(self.slots[cursor].take().expect("linked slot is live"));
cursor = self.next[cursor];
}
let slot = self.slots.len();
self.slots.push(Some(wrap(children)));
self.prev.push(before);
self.next.push(after);
self.next[before] = slot;
self.prev[after] = slot;
}
fn remove(&mut self, slot: usize) {
let (prev, next) = (self.prev[slot], self.next[slot]);
if prev == NIL {
self.head = next;
} else {
self.next[prev] = next;
}
if next != NIL {
self.prev[next] = prev;
}
self.slots[slot] = None;
}
fn into_vec(mut self) -> Vec<Inline> {
let mut nodes = Vec::new();
let mut cursor = self.head;
while cursor != NIL {
nodes.push(self.slots[cursor].take().expect("linked slot is live"));
cursor = self.next[cursor];
}
nodes
}
}
#[derive(Default)]
struct LineCounter {
position: usize,
line: usize,
}
impl LineCounter {
fn line_at(&mut self, input: &str, position: usize) -> usize {
self.line += input.as_bytes()[self.position..position]
.iter()
.filter(|byte| matches!(byte, b'\n' | b'\r'))
.count();
self.position = position;
self.line
}
}
#[derive(Clone, Copy, Default)]
struct DelimRoles {
can_open: bool,
can_close: bool,
single_open: bool,
single_close: bool,
strike: bool,
recloses: usize,
}
fn push_text(nodes: &mut Vec<Inline>, start: usize, value: &str) {
nodes.push(Inline::Text(Text {
meta: NodeMeta::new(Some(Span::new(start, start + value.len()))),
value: value.into(),
}));
}
#[allow(clippy::too_many_arguments)]
fn close_bracket(
input: &str,
base_offset: usize,
close: usize,
options: &SyntaxOptions,
definitions: &[String],
nodes: &mut Vec<Inline>,
delimiters: &mut Vec<DelimMarker>,
brackets: &mut Brackets,
pass: &mut InlinePass,
) -> Option<usize> {
let mut opener = brackets.openers.pop()?;
let depth = pass.state.depth + brackets.openers.len();
let link_active = brackets.openers.len() >= brackets.link_floor;
brackets.link_floor = brackets.link_floor.min(brackets.openers.len());
if opener.kind == BracketKind::InlineFootnote {
if !input[opener.position + 2..close].trim().is_empty() {
let (children, marks) =
take_label(input, options, nodes, delimiters, brackets, &opener, depth);
nodes.truncate(opener.node_index - 1);
if let Some(caret) = opener.caret {
delimiters.truncate(caret);
}
nodes.push(Inline::InlineFootnote(InlineFootnote {
meta: NodeMeta::new(Some(Span::new(
base_offset + opener.position,
base_offset + close + 1,
))),
children,
}));
brackets.formed.push((nodes.len() - 1, marks + 1));
return Some(close + 1);
}
if let Some(caret) = opener.caret {
delimiters[caret].single_close = true;
}
opener.kind = BracketKind::Link;
opener.position += 1;
}
if opener.kind == BracketKind::Image {
let target = match_link_target(
&mut pass.scan,
input,
opener.position + 2,
close,
true,
definitions,
);
if let Some((end, target)) = target {
let (children, marks) =
take_label(input, options, nodes, delimiters, brackets, &opener, depth);
nodes.truncate(opener.node_index - 1);
let span = Span::new(base_offset + opener.position, base_offset + end);
nodes.push(link_node(target, true, span, children));
brackets.formed.push((nodes.len() - 1, marks + 1));
return Some(end);
}
if let Some((end, wikilink)) = opener.wikilink.take() {
if end > close {
drop_label(nodes, delimiters, brackets, &opener);
nodes.push(wikilink);
return Some(end);
}
}
opener.kind = BracketKind::Link;
opener.position += 1;
}
if link_active {
let target = match_link_target(
&mut pass.scan,
input,
opener.position + 1,
close,
false,
definitions,
);
if let Some((end, target)) = target {
let (children, marks) =
take_label(input, options, nodes, delimiters, brackets, &opener, depth);
nodes.truncate(opener.node_index);
let span = Span::new(base_offset + opener.position, base_offset + end);
nodes.push(link_node(target, false, span, children));
brackets.formed.push((nodes.len() - 1, marks + 1));
brackets.close_links();
return Some(end);
}
}
let holds_formed = brackets
.formed
.last()
.is_some_and(|&(node, _)| node > opener.node_index);
if options.constructs.footnote_reference
&& !holds_formed
&& input[opener.position + 1..].starts_with('^')
{
let label = &input[opener.position + 2..close];
if is_footnote_label(label) {
drop_label(nodes, delimiters, brackets, &opener);
nodes.push(Inline::FootnoteReference(FootnoteReference {
meta: NodeMeta::new(Some(Span::new(
base_offset + opener.position,
base_offset + close + 1,
))),
label: label.into(),
identifier: normalize_label(label),
}));
return Some(close + 1);
}
}
None
}
fn drop_label(
nodes: &mut Vec<Inline>,
delimiters: &mut Vec<DelimMarker>,
brackets: &mut Brackets,
opener: &BracketOpener,
) {
nodes.truncate(opener.node_index);
delimiters.truncate(opener.delimiter_bottom);
let kept = brackets
.formed
.partition_point(|&(node, _)| node < opener.node_index);
brackets.formed.truncate(kept);
}
#[allow(clippy::too_many_arguments)]
fn push_delimiter(
nodes: &mut Vec<Inline>,
delimiters: &mut Vec<DelimMarker>,
lines: &mut LineCounter,
input: &str,
index: usize,
base_offset: usize,
marker: u8,
length: usize,
roles: DelimRoles,
) {
let node_index = nodes.len();
nodes.push(Inline::Text(Text {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + index + length,
))),
value: String::from(marker as char).repeat(length),
}));
delimiters.push(DelimMarker {
node_index,
marker,
length,
can_open: roles.can_open,
can_close: roles.can_close,
single_open: roles.single_open,
single_close: roles.single_close,
strike: roles.strike,
recloses: roles.recloses,
enclosed_from: NIL,
position: index,
line: lines.line_at(input, index),
});
}
fn emphasis_roles(
input: &str,
index: usize,
length: usize,
marker: u8,
strikethrough: bool,
bounded_before: bool,
bounded_after: bool,
) -> (bool, bool) {
let flanking = delimiter_flanking_within(input, index, length, bounded_before, bounded_after);
let (mut can_open, mut can_close) = if marker == b'_' {
(
flanking.left
&& (!flanking.right || flanking.previous.is_some_and(|c| c.is_ascii_punctuation())),
flanking.right
&& (!flanking.left || flanking.next.is_some_and(|c| c.is_ascii_punctuation())),
)
} else {
(flanking.left, flanking.right)
};
if strikethrough && marker != b'~' {
if flanking.next == Some('~') {
can_open = true;
}
if flanking.previous == Some('~') {
can_close = true;
}
}
(can_open, can_close)
}
fn assign_emphasis_roles(
input: &str,
delimiters: &mut [DelimMarker],
strikethrough: bool,
underline: bool,
) {
let underline_mark = |run: &DelimMarker| underline && run.marker == b'_' && run.length == 2;
let mut opens_span = alloc::vec![false; delimiters.len()];
let mut closes_span = alloc::vec![false; delimiters.len()];
let mut open_marks: [Vec<usize>; 3] = [Vec::new(), Vec::new(), Vec::new()];
let mut pending: [Option<usize>; 3] = [None; 3];
let mut span_close = alloc::vec![NIL; delimiters.len()];
for (index, run) in delimiters.iter().enumerate() {
if underline_mark(run) {
let open = &mut open_marks[2];
if can_close_underscore(input, run.position, 2) {
if let Some(opener) = open.pop() {
opens_span[opener] = true;
closes_span[index] = true;
span_close[opener] = index;
continue;
}
}
if can_open_underscore(input, run.position, 2) {
open.push(index);
}
continue;
}
if matches!(run.marker, b'+' | b'=') {
let open = &mut open_marks[usize::from(run.marker == b'=')];
let mut closed = 0;
if run.can_close {
while closed <= run.recloses {
let Some(opener) = open.pop() else {
break;
};
opens_span[opener] = true;
closes_span[index] = true;
span_close[opener] = index;
closed += 1;
}
}
if run.can_open && run.length >= 2 * closed + 2 {
open.push(index);
}
continue;
}
let Some(kind) = first_closer_kind(run) else {
continue;
};
if let Some(opener) = pending[kind] {
let opener_run = &delimiters[opener];
if opener_run.line != run.line {
pending[kind] = None;
} else if first_closer_can_close(run) {
pending[kind] = None;
if opener_run.position + opener_run.length != run.position {
opens_span[opener] = true;
closes_span[index] = true;
span_close[opener] = index;
if run.marker == b'|' && run.can_open && run.length >= 4 {
pending[kind] = Some(index);
}
continue;
}
}
}
if pending[kind].is_none() && first_closer_can_open(run) {
pending[kind] = Some(index);
}
}
for index in 0..delimiters.len() {
let run = delimiters[index];
if !matches!(run.marker, b'*' | b'_') && !run.strike {
continue;
}
let bounded_before = index > 0 && {
let before = &delimiters[index - 1];
before.position + before.length == run.position && opens_span[index - 1]
};
let bounded_after = index + 1 < delimiters.len() && {
let after = &delimiters[index + 1];
after.position == run.position + run.length && closes_span[index + 1]
};
let (can_open, can_close) = emphasis_roles(
input,
run.position,
run.length,
run.marker,
strikethrough,
bounded_before,
bounded_after,
);
delimiters[index].can_open = can_open;
delimiters[index].can_close = can_close;
}
let mut open_spans: Vec<usize> = Vec::new();
for (index, run) in delimiters.iter_mut().enumerate() {
while open_spans
.last()
.is_some_and(|&opener| span_close[opener] <= index)
{
open_spans.pop();
}
if let Some(&opener) = open_spans.last() {
run.enclosed_from = opener;
}
if span_close[index] != NIL {
open_spans.push(index);
}
}
}
fn double_mark_roles(input: &str, index: usize, length: usize, marker: u8) -> DelimRoles {
let after_escaped_mark =
index > 0 && input.as_bytes()[index - 1] == marker && is_escaped_at(input, index - 1);
let can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
let recloses = if can_close {
(1..length / 2)
.take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
.count()
} else {
0
};
DelimRoles {
can_open: can_open_delimited(input, index + length - 2, 2),
can_close,
recloses,
..DelimRoles::default()
}
}
#[derive(Clone, Copy)]
struct EmphasisContext {
underline: bool,
depth: usize,
}
fn process_emphasis(
nodes: Vec<Inline>,
mut delimiters: Vec<DelimMarker>,
context: EmphasisContext,
formed: &[(usize, usize)],
) -> (Vec<Inline>, usize) {
let mut deepest = formed.iter().map(|&(_, depth)| depth).max().unwrap_or(0);
if delimiters.is_empty() {
let mut nodes = nodes;
merge_adjacent_text(&mut nodes);
return (nodes, deepest);
}
let mut nodes = InlineList::new(nodes);
let mut links = DelimiterLinks::new(delimiters.len());
for &(node, depth) in formed {
let before = delimiters.partition_point(|run| run.node_index < node);
if before > 0 {
let gap = &mut links.nesting_after[before - 1].total;
*gap = (*gap).max(depth);
}
}
let mut openers_bottom: [Option<usize>; NESTED_MARKERS * 6] = [None; NESTED_MARKERS * 6];
let mut span_bottom: Vec<Vec<(usize, usize)>> = Vec::new();
let mut pending: [Option<usize>; 3] = [None; 3];
let mut closer_idx = 0;
let mut visited = NIL;
while closer_idx < delimiters.len() {
let closer = delimiters[closer_idx];
let mut first = None;
let kind = first_closer_kind(&closer);
if let Some(kind) = kind {
if visited != closer_idx {
if let Some(opener) = pending[kind] {
let opener_run = delimiters[opener];
let usable = links.live[opener]
&& first_closer_can_open(&opener_run)
&& opener_run.line == closer.line;
if !usable {
pending[kind] = None;
} else if first_closer_can_close(&closer) {
if opener_run.position + opener_run.length == closer.position {
pending[kind] = None;
} else {
first = Some(opener);
}
}
}
}
}
visited = closer_idx;
let mut nested = None;
if closer.can_close && closer.nests() {
nested = nested_opener(
&delimiters,
&links,
&openers_bottom,
&span_bottom,
closer_idx,
first.map(|opener| opener + 1),
);
}
let choice = match (nested, first) {
(Some(nested), Some(first)) => Some((nested.max(first), nested > first)),
(Some(nested), None) => Some((nested, true)),
(None, Some(first)) => Some((first, false)),
(None, None) => None,
};
let Some((opener_idx, is_nested)) = choice else {
if closer.can_close && closer.nests() {
let key = openers_bottom_key(&closer);
match span_floor(&closer) {
Some(span) => {
if span_bottom.len() <= span {
span_bottom.resize_with(span + 1, Vec::new);
}
match span_bottom[span].iter_mut().find(|(k, _)| *k == key) {
Some(bound) => bound.1 = closer_idx,
None => span_bottom[span].push((key, closer_idx)),
}
}
None => openers_bottom[key] = Some(closer_idx),
}
}
if let Some(kind) = kind {
if pending[kind].is_none() && first_closer_can_open(&closer) {
pending[kind] = Some(closer_idx);
}
}
if !closer.can_open && !closer.single_open {
links.unlink(closer_idx);
}
closer_idx += 1;
continue;
};
if !is_nested {
if let Some(kind) = kind {
pending[kind] = None;
}
}
let (used, wrap) = pair_shape(
&delimiters[opener_idx],
&delimiters[closer_idx],
is_nested,
context,
);
let inner = links.close_span(opener_idx, closer_idx);
let fits = context.depth + inner.total < MAX_INLINE_NESTING
&& (wrap.is_mark() || inner.emphasis < MAX_EMPHASIS_NESTING);
if fits {
apply_emphasis(
&mut nodes,
&mut delimiters,
opener_idx,
closer_idx,
used,
wrap,
);
links.nesting_after[opener_idx] = Nesting {
emphasis: if wrap.is_mark() {
0
} else {
inner.emphasis + 1
},
total: inner.total + 1,
};
deepest = deepest.max(inner.total + 1);
} else {
delimiters[opener_idx].length = 0;
delimiters[closer_idx].length = 0;
links.nesting_after[opener_idx] = inner;
}
if matches!(delimiters[opener_idx].marker, b'+' | b'=' | b'|') {
delimiters[opener_idx].can_open = false;
}
if is_nested && matches!(closer.marker, b'+' | b'=') {
let run = &mut delimiters[closer_idx];
run.can_close = run.recloses > 0;
run.recloses = run.recloses.saturating_sub(1);
}
if !is_nested {
if closer.marker == b'~' {
delimiters[closer_idx].can_open = false;
delimiters[closer_idx].can_close = false;
}
delimiters[closer_idx].single_close = false;
}
if delimiters[opener_idx].length == 0 {
links.unlink(opener_idx);
}
let leftover = delimiters[closer_idx];
if leftover.length == 0 {
links.unlink(closer_idx);
closer_idx += 1;
} else if !is_nested {
if let Some(kind) = kind {
if first_closer_can_open(&leftover) {
pending[kind] = Some(closer_idx);
}
}
if !first_closer_can_open(&leftover) {
links.unlink(closer_idx);
}
closer_idx += 1;
}
}
let mut nodes = nodes.into_vec();
merge_adjacent_text(&mut nodes);
(nodes, deepest)
}
const NESTED_MARKERS: usize = 5;
fn nested_opener(
delimiters: &[DelimMarker],
links: &DelimiterLinks,
openers_bottom: &[Option<usize>],
span_bottom: &[Vec<(usize, usize)>],
closer_idx: usize,
floor: Option<usize>,
) -> Option<usize> {
let closer = &delimiters[closer_idx];
let key = openers_bottom_key(closer);
let mut bottom = openers_bottom[key].max(floor);
if let Some(span) = span_floor(closer) {
let span_floor = span_bottom
.get(span)
.and_then(|bounds| bounds.iter().find(|(k, _)| *k == key))
.map_or(span + 1, |&(_, bound)| bound);
bottom = bottom.max(Some(span_floor));
}
let mut search = links.prev[closer_idx];
while search != NIL {
if bottom.is_some_and(|bottom| search < bottom) {
return None;
}
let candidate = &delimiters[search];
if candidate.marker == closer.marker
&& candidate.can_open
&& emphasis_delimiters_match(candidate, closer)
{
return Some(search);
}
search = links.prev[search];
}
None
}
fn span_floor(closer: &DelimMarker) -> Option<usize> {
(closer.can_open && closer.enclosed_from != NIL).then_some(closer.enclosed_from)
}
fn first_closer_kind(run: &DelimMarker) -> Option<usize> {
match run.marker {
b'|' => Some(0),
b'~' if run.single_open || run.single_close => Some(1),
b'^' => Some(2),
_ => None,
}
}
fn first_closer_can_open(run: &DelimMarker) -> bool {
if run.marker == b'|' {
run.can_open && run.length >= 2
} else {
run.single_open
}
}
fn first_closer_can_close(run: &DelimMarker) -> bool {
if run.marker == b'|' {
run.can_close && run.length >= 2
} else {
run.single_close
}
}
fn pair_shape(
opener: &DelimMarker,
closer: &DelimMarker,
nested: bool,
context: EmphasisContext,
) -> (usize, EmphasisWrap) {
match closer.marker {
b'~' if nested => {
let marker = if closer.length >= 2 {
DeleteMarker::DoubleTilde
} else {
DeleteMarker::SingleTilde
};
(closer.length, EmphasisWrap::Delete(marker))
}
b'~' => (1, EmphasisWrap::Subscript),
b'^' => (1, EmphasisWrap::Superscript),
b'+' => (2, EmphasisWrap::Insert),
b'=' => (2, EmphasisWrap::Mark),
b'|' => (2, EmphasisWrap::Spoiler),
marker => {
let strong = opener.length >= 2 && closer.length >= 2;
if !strong {
(1, EmphasisWrap::Emphasis)
} else if marker == b'_' && context.underline {
(2, EmphasisWrap::Underline)
} else {
(2, EmphasisWrap::Strong)
}
}
}
}
fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
let mut write = 0;
for read in 0..nodes.len() {
if read != write {
nodes.swap(read, write);
}
if write > 0 {
let (head, tail) = nodes.split_at_mut(write);
if let (Inline::Text(previous), Inline::Text(current)) =
(&mut head[write - 1], &tail[0])
{
previous.value.push_str(¤t.value);
if let (Some(previous_span), Some(current_span)) =
(previous.meta.span.as_mut(), current.meta.span)
{
previous_span.end = current_span.end;
}
continue;
}
}
write += 1;
}
nodes.truncate(write);
for node in nodes.iter_mut() {
match node {
Inline::Emphasis(node) => merge_adjacent_text(&mut node.children),
Inline::Strong(node) => merge_adjacent_text(&mut node.children),
Inline::Delete(node) => merge_adjacent_text(&mut node.children),
Inline::Underline(node) => merge_adjacent_text(&mut node.children),
Inline::Insert(node) => merge_adjacent_text(&mut node.children),
Inline::Mark(node) => merge_adjacent_text(&mut node.children),
Inline::Spoiler(node) => merge_adjacent_text(&mut node.children),
Inline::Subscript(node) => merge_adjacent_text(&mut node.children),
Inline::Superscript(node) => merge_adjacent_text(&mut node.children),
_ => {}
}
}
}
fn openers_bottom_key(closer: &DelimMarker) -> usize {
let marker = match closer.marker {
b'_' => 1,
b'~' => 2,
b'+' => 3,
b'=' => 4,
_ => 0,
};
let both = usize::from(closer.can_open && closer.can_close);
let modulo = closer.length % 3;
((marker * 2) + both) * 3 + modulo
}
fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
match opener.marker {
b'~' => opener.length == closer.length,
b'+' | b'=' => opener.length >= 2 && closer.length >= 2,
_ => {
let opener_both = opener.can_open && opener.can_close;
let closer_both = closer.can_open && closer.can_close;
if opener_both || closer_both {
let sum = opener.length + closer.length;
if sum % 3 == 0 && !(opener.length % 3 == 0 && closer.length % 3 == 0) {
return false;
}
}
true
}
}
}
#[derive(Clone, Copy)]
enum EmphasisWrap {
Emphasis,
Strong,
Delete(DeleteMarker),
Underline,
Insert,
Mark,
Spoiler,
Subscript,
Superscript,
}
impl EmphasisWrap {
fn is_mark(self) -> bool {
!matches!(self, Self::Emphasis | Self::Strong | Self::Delete(_))
}
}
fn apply_emphasis(
nodes: &mut InlineList,
delimiters: &mut [DelimMarker],
opener_idx: usize,
closer_idx: usize,
used: usize,
wrap: EmphasisWrap,
) {
let opener_node = delimiters[opener_idx].node_index;
let closer_node = delimiters[closer_idx].node_index;
let span_start = nodes
.node_mut(opener_node)
.span()
.map(|span| span.end - used);
let span_end = nodes
.node_mut(closer_node)
.span()
.map(|span| span.start + used);
trim_delimiter_text_tail(nodes.node_mut(opener_node), used);
trim_delimiter_text_head(nodes.node_mut(closer_node), used);
delimiters[opener_idx].length -= used;
let closer = &mut delimiters[closer_idx];
closer.length -= used;
closer.position += used;
let meta = NodeMeta::new(
span_start
.zip(span_end)
.map(|(start, end)| Span::new(start, end)),
);
nodes.wrap_between(opener_node, closer_node, |children| match wrap {
EmphasisWrap::Strong => Inline::Strong(Strong { meta, children }),
EmphasisWrap::Emphasis => Inline::Emphasis(Emphasis { meta, children }),
EmphasisWrap::Delete(marker) => Inline::Delete(Delete {
meta,
marker,
children,
}),
EmphasisWrap::Underline => Inline::Underline(Underline { meta, children }),
EmphasisWrap::Insert => Inline::Insert(Insert { meta, children }),
EmphasisWrap::Mark => Inline::Mark(Mark { meta, children }),
EmphasisWrap::Spoiler => Inline::Spoiler(Spoiler { meta, children }),
EmphasisWrap::Subscript => Inline::Subscript(Subscript { meta, children }),
EmphasisWrap::Superscript => Inline::Superscript(Superscript { meta, children }),
});
if delimiters[closer_idx].length == 0 {
nodes.remove(closer_node);
}
if delimiters[opener_idx].length == 0 {
nodes.remove(opener_node);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BracketKind {
Link,
Image,
InlineFootnote,
}
struct BracketOpener {
kind: BracketKind,
node_index: usize,
position: usize,
delimiter_bottom: usize,
caret: Option<usize>,
wikilink: Option<(usize, Inline)>,
}
enum LinkTarget {
Resource(ParsedLinkResource),
Reference {
identifier: String,
kind: ReferenceKind,
},
}
fn match_link_target(
scan: &mut InlineScan,
input: &str,
label_start: usize,
close: usize,
image: bool,
definitions: &[String],
) -> Option<(usize, LinkTarget)> {
let label = &input[label_start..close];
let after = close + 1;
if input.as_bytes().get(after) == Some(&b'(') {
match parse_link_resource(&mut scan.lookups, input, after) {
Some((end, resource)) => return Some((end, LinkTarget::Resource(resource))),
None if image => return None,
None => {}
}
}
let reference_close = if input.as_bytes().get(after) == Some(&b'[') {
scan.reference_label_end(after)
} else {
None
};
if let Some(reference_close) = reference_close {
let reference = &input[after + 1..reference_close];
let identifier = if reference.is_empty() {
label
} else {
reference
};
return definition_exists(definitions, identifier).then(|| {
let kind = if reference.is_empty() {
ReferenceKind::Collapsed
} else {
ReferenceKind::Full
};
(
reference_close + 1,
LinkTarget::Reference {
identifier: identifier.into(),
kind,
},
)
});
}
definition_exists(definitions, label).then(|| {
(
after,
LinkTarget::Reference {
identifier: label.into(),
kind: ReferenceKind::Shortcut,
},
)
})
}
fn link_node(target: LinkTarget, image: bool, span: Span, mut children: Vec<Inline>) -> Inline {
let meta = NodeMeta::new(Some(span));
if !image {
demote_links(&mut children);
}
match (target, image) {
(LinkTarget::Resource(resource), false) => Inline::Link(Link {
meta,
destination: resource.destination,
destination_kind: resource.destination_kind,
title: resource.title,
title_kind: resource.title_kind,
children,
}),
(LinkTarget::Resource(resource), true) => Inline::Image(Image {
meta,
destination: resource.destination,
destination_kind: resource.destination_kind,
title: resource.title,
title_kind: resource.title_kind,
alt: children,
}),
(LinkTarget::Reference { identifier, kind }, false) => {
Inline::LinkReference(LinkReference {
meta,
identifier: normalize_label(&identifier),
label: identifier,
kind,
children,
})
}
(LinkTarget::Reference { identifier, kind }, true) => {
Inline::ImageReference(ImageReference {
meta,
identifier: normalize_label(&identifier),
label: identifier,
kind,
alt: children,
})
}
}
}
fn demote_links(nodes: &mut Vec<Inline>) {
for node in nodes.iter_mut() {
match node {
Inline::Autolink(autolink) => {
let value = match &autolink.kind {
AutolinkKind::Angle => autolink.destination.clone(),
AutolinkKind::GfmLiteral { original } => original.clone(),
};
*node = Inline::Text(Text {
meta: autolink.meta.clone(),
value,
});
}
Inline::Emphasis(node) => demote_links(&mut node.children),
Inline::Strong(node) => demote_links(&mut node.children),
Inline::Underline(node) => demote_links(&mut node.children),
Inline::Delete(node) => demote_links(&mut node.children),
Inline::Insert(node) => demote_links(&mut node.children),
Inline::Mark(node) => demote_links(&mut node.children),
Inline::Spoiler(node) => demote_links(&mut node.children),
Inline::Subscript(node) => demote_links(&mut node.children),
Inline::Superscript(node) => demote_links(&mut node.children),
Inline::InlineFootnote(node) => demote_links(&mut node.children),
Inline::TextDirective(node) => demote_links(&mut node.label),
_ => {}
}
}
merge_adjacent_text(nodes);
}
struct Brackets {
openers: Vec<BracketOpener>,
link_floor: usize,
overflow: usize,
formed: Vec<(usize, usize)>,
}
impl Brackets {
fn new() -> Self {
Self {
openers: Vec::new(),
link_floor: 0,
overflow: 0,
formed: Vec::new(),
}
}
fn close_links(&mut self) {
self.link_floor = self.openers.len();
}
}
fn take_label(
input: &str,
options: &SyntaxOptions,
nodes: &mut Vec<Inline>,
delimiters: &mut Vec<DelimMarker>,
brackets: &mut Brackets,
opener: &BracketOpener,
depth: usize,
) -> (Vec<Inline>, usize) {
let first = opener.node_index + 1;
let children: Vec<Inline> = nodes.drain(first..).collect();
let mut runs = delimiters.split_off(opener.delimiter_bottom);
for run in &mut runs {
run.node_index -= first;
}
let split = brackets.formed.partition_point(|&(node, _)| node < first);
let formed: Vec<(usize, usize)> = brackets
.formed
.split_off(split)
.into_iter()
.map(|(node, marks)| (node - first, marks))
.collect();
assign_emphasis_roles(
input,
&mut runs,
options.constructs.gfm_strikethrough,
options.constructs.underline,
);
process_emphasis(
children,
runs,
EmphasisContext {
underline: options.constructs.underline,
depth,
},
&formed,
)
}
fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
if let Inline::Text(text) = node {
let new_len = text.value.len().saturating_sub(count);
text.value.truncate(new_len);
if let Some(span) = text.meta.span.as_mut() {
span.end = span.end.saturating_sub(count);
}
}
}
fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
if let Inline::Text(text) = node {
let count = count.min(text.value.len());
text.value.truncate(text.value.len() - count);
if let Some(span) = text.meta.span.as_mut() {
span.start += count;
}
}
}
trait Lookups {
fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize>;
fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize>;
fn angle_destination_stop(&mut self, from: usize) -> Option<usize>;
}
struct DirectLookups<'a> {
input: &'a str,
}
impl Lookups for DirectLookups<'_> {
fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
self.input[from..].find(pattern).map(|offset| from + offset)
}
fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
find_code_span_close(self.input, start, len)
}
fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
(from..self.input.len()).find(|index| is_angle_destination_stop(self.input, *index))
}
}
const CACHED_PATTERNS: [&str; 7] = [">", "-->", "?>", "]]>", "\"", "'", "`$"];
struct InlineLookups<'a> {
input: &'a str,
patterns: [Positions; CACHED_PATTERNS.len()],
backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
angle_destination_stops: Positions,
}
impl<'a> InlineLookups<'a> {
fn new(input: &'a str) -> Self {
Self {
input,
patterns: Default::default(),
backtick_runs: None,
angle_destination_stops: Positions::default(),
}
}
}
impl Lookups for InlineLookups<'_> {
fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
let input = self.input;
let Some(slot) = CACHED_PATTERNS.iter().position(|cached| *cached == pattern) else {
return DirectLookups { input }.find(pattern, from);
};
self.patterns[slot].first_at_or_after(from, || pattern_starts(input, pattern))
}
fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
let input = self.input;
let runs = self
.backtick_runs
.get_or_insert_with(|| backtick_runs_by_length(input));
let starts = runs.get(&len)?;
starts
.get(starts.partition_point(|run| *run < start))
.copied()
}
fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
let input = self.input;
self.angle_destination_stops.first_at_or_after(from, || {
(0..input.len())
.filter(|index| is_angle_destination_stop(input, *index))
.collect()
})
}
}
fn backtick_runs_by_length(input: &str) -> BTreeMap<usize, Vec<usize>> {
let mut runs: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
let mut index = 0;
while index < input.len() {
if input.as_bytes()[index] == b'`' {
let len = backtick_run_len(input, index);
runs.entry(len).or_default().push(index);
index += len;
} else {
index += 1;
}
}
runs
}
fn backtick_run_len(input: &str, index: usize) -> usize {
input.as_bytes()[index..]
.iter()
.take_while(|byte| **byte == b'`')
.count()
}
fn is_angle_destination_stop(input: &str, index: usize) -> bool {
match input.as_bytes()[index] {
b'<' | b'>' => !is_escaped_at(input, index),
b'\n' | b'\r' => true,
_ => false,
}
}
struct InlineScan<'a> {
input: &'a str,
lookups: InlineLookups<'a>,
label_ends: BracketMemo,
reference_label_ends: PathMemo,
wikilink_closes: PathMemo,
directive_attribute_closes: PathMemo,
mdx_expression_closes: BracketMemo,
jsx: Option<JsxIndex>,
single_tilde_delete_closes: Positions,
literal_autolinks: LiteralAutolinkScan,
}
impl<'a> InlineScan<'a> {
fn new(input: &'a str) -> Self {
Self {
input,
lookups: InlineLookups::new(input),
label_ends: BracketMemo::default(),
reference_label_ends: PathMemo::default(),
wikilink_closes: PathMemo::default(),
directive_attribute_closes: PathMemo::default(),
mdx_expression_closes: BracketMemo::default(),
jsx: None,
single_tilde_delete_closes: Positions::default(),
literal_autolinks: LiteralAutolinkScan::default(),
}
}
fn link_label_end(&mut self, open: usize) -> Option<usize> {
let input = self.input;
if input.as_bytes().get(open) != Some(&b'[') {
return None;
}
let lookups = &mut self.lookups;
self.label_ends
.resolve(input.len() + 1, open + 1, |cursor| {
link_label_step(lookups, input, cursor)
})
}
fn reference_label_end(&mut self, open: usize) -> Option<usize> {
let input = self.input;
if input.as_bytes().get(open) != Some(&b'[') {
return None;
}
let close = self
.reference_label_ends
.resolve(input.len() + 1, open + 1, |cursor| {
reference_label_step(input, cursor)
})?;
reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
}
fn wikilink_close(&mut self, start: usize) -> Option<usize> {
let input = self.input;
self.wikilink_closes
.resolve(input.len() + 1, start, |cursor| {
wikilink_close_step(input, cursor)
})
}
fn mdx_expression_close(&mut self, open: usize) -> Option<usize> {
let input = self.input;
if input.as_bytes().get(open) != Some(&b'{') {
return None;
}
self.mdx_expression_closes
.resolve(
(input.len() + 1) * MDX_BRACE_STATES,
(open + 1) * MDX_BRACE_STATES,
|node| mdx_expression_step(input, node, false),
)
.map(|node| node / MDX_BRACE_STATES)
}
fn mdx_jsx_end(&mut self, start: usize) -> Option<usize> {
let input = self.input;
let expressions = &mut self.mdx_expression_closes;
self.jsx
.get_or_insert_with(|| JsxIndex::new(input, false, expressions))
.element_end(start)
.map(|end| end + 1)
}
fn single_tilde_delete_close(&mut self, start: usize) -> Option<usize> {
let input = self.input;
self.single_tilde_delete_closes
.first_at_or_after(start, || {
(0..input.len())
.filter(|index| is_single_tilde_delete_close(input, *index))
.collect()
})
}
fn directive_attributes_close(&mut self, open: usize) -> Option<usize> {
let input = self.input;
if input.as_bytes().get(open) != Some(&b'{') {
return None;
}
self.directive_attribute_closes.resolve(
(input.len() + 1) * DIRECTIVE_QUOTE_STATES,
(open + 1) * DIRECTIVE_QUOTE_STATES,
|node| directive_attributes_step(input, node),
)
}
}
fn parse_inlines(
input: &str,
base_offset: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<Inline> {
parse_inlines_with_context(
input,
base_offset,
options,
definitions,
diagnostics,
&mut InlineState::default(),
)
}
const MAX_INLINE_NESTING: usize = 32;
#[derive(Default)]
struct InlineState {
depth: usize,
}
struct InlinePass<'p, 'a> {
state: &'p mut InlineState,
scan: InlineScan<'a>,
}
fn parse_inlines_with_context(
input: &str,
base_offset: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
state: &mut InlineState,
) -> Vec<Inline> {
if state.depth >= MAX_INLINE_NESTING {
if input.is_empty() {
return Vec::new();
}
return vec![Inline::Text(Text {
meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + input.len()))),
value: input.into(),
})];
}
state.depth += 1;
let nodes = parse_inline_content(input, base_offset, options, definitions, diagnostics, state);
state.depth -= 1;
nodes
}
fn parse_inline_content(
input: &str,
base_offset: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
state: &mut InlineState,
) -> Vec<Inline> {
let bytes = input.as_bytes();
let mut nodes = Vec::new();
let mut text_start = 0;
let mut text = String::new();
let mut index = 0;
let mut delimiters: Vec<DelimMarker> = Vec::new();
let mut lines = LineCounter::default();
let mut brackets = Brackets::new();
let mut superscript_open: Option<(usize, usize)> = None;
let mut pass = InlinePass {
state,
scan: InlineScan::new(input),
};
while index < bytes.len() {
if bytes[index] == b'\\' {
if let Some((next_index, char)) = next_char(input, index + 1) {
if char.is_ascii_punctuation() {
if options.parse.preserve_character_escapes {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Escape(Escape {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + next_index,
))),
value: char,
}));
index = next_index;
text_start = index;
continue;
}
if text.is_empty() {
text_start = base_offset + index;
}
text.push(char);
index = next_index;
continue;
}
}
}
if bytes[index] == b'&' {
if let Some((end, value)) = parse_character_reference(input, index) {
if options.parse.preserve_character_references {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::CharacterReference(CharacterReference {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + end,
))),
reference: input[index..end].into(),
value,
}));
index = end;
text_start = index;
continue;
}
if text.is_empty() {
text_start = base_offset + index;
}
text.push_str(&value);
index = end;
continue;
}
}
if bytes[index] == b'\n' {
if text.ends_with('\\') {
text.pop();
flush_text(
&mut nodes,
&mut text,
text_start,
base_offset + index.saturating_sub(1),
);
nodes.push(Inline::LineBreak(LineBreak {
meta: NodeMeta::new(Some(Span::new(
base_offset + index.saturating_sub(1),
base_offset + index + 1,
))),
kind: LineBreakKind::Backslash,
}));
index += 1;
text_start = index;
continue;
}
let trailing_spaces = trailing_space_count(&text);
if is_hard_break_suffix(&text, trailing_spaces) {
text.truncate(text.len() - trailing_spaces);
flush_text(
&mut nodes,
&mut text,
text_start,
base_offset + index.saturating_sub(trailing_spaces),
);
nodes.push(Inline::LineBreak(LineBreak {
meta: NodeMeta::new(Some(Span::new(
base_offset + index.saturating_sub(trailing_spaces),
base_offset + index + 1,
))),
kind: LineBreakKind::Spaces,
}));
index += 1;
text_start = index;
continue;
}
if trailing_spaces > 0 {
text.truncate(text.len() - trailing_spaces);
}
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::SoftBreak(SoftBreak {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + index + 1,
))),
}));
index += 1;
text_start = index;
continue;
}
if bytes[index] == b'`' {
if let Some((end, code_span)) =
parse_code_span_with(&mut pass.scan.lookups, input, index)
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Code(CodeInline {
meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
value: code_span.value,
raw: code_span.raw,
fence_length: code_span.fence_length,
}));
index = end;
text_start = index;
continue;
} else {
let run = bytes[index..]
.iter()
.take_while(|byte| **byte == b'`')
.count();
if text.is_empty() {
text_start = base_offset + index;
}
for _ in 0..run {
text.push('`');
}
index += run;
continue;
}
}
if options.constructs.spoiler && bytes[index] == b'|' {
let run_len = delimiter_byte_run_len(input, index, b'|');
if run_len >= 2 {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let roles = DelimRoles {
can_open: true,
can_close: index == 0 || bytes[index - 1] != b'|',
..DelimRoles::default()
};
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
b'|',
run_len,
roles,
);
index += run_len;
text_start = index;
continue;
}
}
if bytes[index] == b'*' && delimiter_byte_run_start(input, index, b'*') == index {
let run_len = delimiter_byte_run_len(input, index, b'*');
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let roles = DelimRoles::default();
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
b'*',
run_len,
roles,
);
index += run_len;
text_start = index;
continue;
}
if bytes[index] == b'_' && delimiter_byte_run_start(input, index, b'_') == index {
if options.constructs.gfm_autolink_literal || options.constructs.relaxed_autolinks {
if let Some((end, destination)) = parse_literal_autolink(
input,
index,
options.constructs.gfm_autolink_literal,
options.constructs.relaxed_autolinks,
&mut pass.scan.literal_autolinks,
) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Autolink(Autolink {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + end,
))),
destination,
kind: AutolinkKind::GfmLiteral {
original: input[index..end].into(),
},
}));
index = end;
text_start = index;
continue;
}
}
let run_len = delimiter_byte_run_len(input, index, b'_');
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let roles = DelimRoles::default();
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
b'_',
run_len,
roles,
);
index += run_len;
text_start = index;
continue;
}
if (options.constructs.insert && bytes[index] == b'+')
|| (options.constructs.highlight && bytes[index] == b'=')
{
let marker = bytes[index];
let run_len = delimiter_byte_run_len(input, index, marker);
if run_len >= 2 {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let roles = double_mark_roles(input, index, run_len, marker);
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
marker,
run_len,
roles,
);
index += run_len;
text_start = index;
continue;
}
}
let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
let closes_superscript = options.constructs.superscript
&& bytes[index] == b'^'
&& superscript_open.is_some_and(|(run, position)| {
delimiters.get(run).is_some_and(|run| {
run.position == position && run.line == lines.line_at(input, index)
}) && position + 1 != index
});
if options.constructs.inline_footnote
&& options.constructs.footnote_reference
&& bracket_room
&& !closes_superscript
&& bytes[index] == b'^'
&& bytes.get(index + 1) == Some(&b'[')
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let caret = if options.constructs.superscript {
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
b'^',
1,
DelimRoles::default(),
);
Some(delimiters.len() - 1)
} else {
push_text(&mut nodes, base_offset + index, "^");
None
};
push_text(&mut nodes, base_offset + index + 1, "[");
brackets.openers.push(BracketOpener {
kind: BracketKind::InlineFootnote,
node_index: nodes.len() - 1,
position: index,
delimiter_bottom: delimiters.len(),
caret,
wikilink: None,
});
index += 2;
text_start = index;
continue;
}
if options.constructs.superscript && bytes[index] == b'^' {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let roles = DelimRoles {
single_open: !(options.constructs.inline_footnote
&& bytes.get(index + 1) == Some(&b'[')),
single_close: true,
..DelimRoles::default()
};
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
b'^',
1,
roles,
);
superscript_open = if !closes_superscript && roles.single_open {
Some((delimiters.len() - 1, index))
} else {
None
};
index += 1;
text_start = index;
continue;
}
if (options.constructs.gfm_strikethrough || options.constructs.subscript)
&& bytes[index] == b'~'
{
let run_len = delimiter_byte_run_len(input, index, b'~');
let mut roles = DelimRoles::default();
if options.constructs.gfm_strikethrough
&& delimiter_byte_run_start(input, index, b'~') == index
&& (run_len == 2 || (run_len == 1 && options.parse.single_tilde_strikethrough))
{
roles.strike = true;
}
if options.constructs.subscript {
roles.single_open = starts_exact_byte_run(input, index, b'~', 1)
&& !single_tilde_delete_takes_precedence(options, input, index, &mut pass.scan);
roles.single_close = true;
}
if roles.strike || roles.single_open || roles.single_close {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
push_delimiter(
&mut nodes,
&mut delimiters,
&mut lines,
input,
index,
base_offset,
b'~',
run_len,
roles,
);
index += run_len;
text_start = index;
continue;
}
}
if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') && bracket_room {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let wikilink = parse_wikilink(input, index + 1, base_offset, options, &mut pass.scan);
if let Some((end, wikilink)) = wikilink.clone() {
if pass.scan.link_label_end(index + 1).is_none() {
push_text(&mut nodes, base_offset + index, "!");
nodes.push(wikilink);
index = end;
text_start = index;
continue;
}
}
push_text(&mut nodes, base_offset + index, "!");
push_text(&mut nodes, base_offset + index + 1, "[");
brackets.openers.push(BracketOpener {
kind: BracketKind::Image,
node_index: nodes.len() - 1,
position: index,
delimiter_bottom: delimiters.len(),
caret: None,
wikilink,
});
index += 2;
text_start = index;
continue;
}
if bytes[index] == b'[' {
if let Some((end, wikilink)) =
parse_wikilink(input, index, base_offset, options, &mut pass.scan)
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(wikilink);
index = end;
text_start = index;
continue;
}
if bracket_room {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
push_text(&mut nodes, base_offset + index, "[");
brackets.openers.push(BracketOpener {
kind: BracketKind::Link,
node_index: nodes.len() - 1,
position: index,
delimiter_bottom: delimiters.len(),
caret: None,
wikilink: None,
});
index += 1;
text_start = index;
continue;
}
brackets.overflow += 1;
}
if bytes[index] == b']' && brackets.overflow > 0 {
brackets.overflow -= 1;
} else if bytes[index] == b']' && !brackets.openers.is_empty() {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
if let Some(end) = close_bracket(
input,
base_offset,
index,
options,
definitions,
&mut nodes,
&mut delimiters,
&mut brackets,
&mut pass,
) {
index = end;
text_start = index;
continue;
}
}
if bytes[index] == b'$' && options.constructs.math_inline {
if let Some((end, value, kind)) =
parse_math_inline(&mut pass.scan.lookups, input, index)
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Math(MathInline {
meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
value,
kind,
}));
index = end;
text_start = index;
continue;
}
let run = bytes[index..]
.iter()
.take_while(|byte| **byte == b'$')
.count();
if run > 1 {
if text.is_empty() {
text_start = base_offset + index;
}
text.push_str(&input[index..index + run]);
index += run;
continue;
}
}
if options.constructs.gfm_autolink_literal || options.constructs.relaxed_autolinks {
if let Some((end, destination)) = parse_literal_autolink(
input,
index,
options.constructs.gfm_autolink_literal,
options.constructs.relaxed_autolinks,
&mut pass.scan.literal_autolinks,
) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Autolink(Autolink {
meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
destination,
kind: AutolinkKind::GfmLiteral {
original: input[index..end].into(),
},
}));
index = end;
text_start = index;
continue;
}
}
if bytes[index] == b'<' {
if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
let raw = &input[index..end];
if is_autolink(raw) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Autolink(Autolink {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + end,
))),
destination: raw[1..raw.len() - 1].into(),
kind: AutolinkKind::Angle,
}));
index = end;
text_start = index;
continue;
}
}
if options.constructs.mdx_jsx_inline {
if let Some((end, raw)) = pass
.scan
.mdx_jsx_end(index)
.map(|end| (end, String::from(&input[index..end])))
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::MdxJsx(MdxJsxInline {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + end,
))),
value: raw,
}));
index = end;
text_start = index;
continue;
}
}
if options.constructs.html_inline {
if let Some(end) = html_inline_end(&mut pass.scan.lookups, input, index) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Html(HtmlInline {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + end,
))),
value: input[index..end].into(),
}));
index = end;
text_start = index;
continue;
}
}
}
if bytes[index] == b'{' && options.constructs.mdx_expression_inline {
if let Some(end) = pass.scan.mdx_expression_close(index) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::MdxExpression(MdxExpressionInline {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + end + 1,
))),
value: input[index + 1..end].into(),
}));
index = end + 1;
text_start = index;
continue;
} else {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidMdx,
Span::new(base_offset + index, base_offset + input.len()),
"MDX expression is missing a closing brace",
));
}
}
if bytes[index] == b':' && options.constructs.shortcode {
if let Some((end, name)) = parse_shortcode(input, index) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(Inline::Shortcode(Shortcode {
meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
name,
}));
index = end;
text_start = index;
continue;
}
}
if bytes[index] == b':'
&& options.constructs.directive_text
&& pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING
{
pass.state.depth += brackets.openers.len();
let directive = parse_text_directive(
input,
index,
base_offset,
options,
definitions,
diagnostics,
&mut pass,
);
pass.state.depth -= brackets.openers.len();
if let Some((end, directive)) = directive {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
if let Inline::TextDirective(node) = &directive {
if contains_link_inline(&node.label) {
brackets.close_links();
}
let depth = inline_nesting(&node.label) + 1;
brackets.formed.push((nodes.len(), depth));
}
nodes.push(directive);
index = end;
text_start = index;
continue;
}
}
let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
if text.is_empty() {
text_start = base_offset + index;
}
text.push(char);
index = next_index;
}
flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
for opener in &brackets.openers {
if let Some(caret) = opener.caret {
delimiters[caret].single_close = true;
}
}
assign_emphasis_roles(
input,
&mut delimiters,
options.constructs.gfm_strikethrough,
options.constructs.underline,
);
process_emphasis(
nodes,
delimiters,
EmphasisContext {
underline: options.constructs.underline,
depth: pass.state.depth - 1,
},
&brackets.formed,
)
.0
}
fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
if input[index..].starts_with("::") {
return None;
}
let mut cursor = index + 1;
while let Some((next, char)) = next_char(input, cursor) {
if char == ':' {
if cursor == index + 1 {
return None;
}
return Some((next, input[index + 1..cursor].into()));
}
if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
return None;
}
cursor = next;
}
None
}
fn parse_wikilink(
input: &str,
index: usize,
base_offset: usize,
options: &SyntaxOptions,
scan: &mut InlineScan,
) -> Option<(usize, Inline)> {
let configured_order = if options.constructs.wikilink_title_after_pipe {
WikiLinkLabelOrder::AfterPipe
} else if options.constructs.wikilink_title_before_pipe {
WikiLinkLabelOrder::BeforePipe
} else {
return None;
};
if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
{
return None;
}
let close = scan.wikilink_close(index + 2)?;
let source = &input[index + 2..close];
if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
return None;
}
let (target_source, label_source, label_order) =
if let Some(separator) = find_wikilink_separator(source) {
match configured_order {
WikiLinkLabelOrder::AfterPipe => (
&source[..separator],
&source[separator + 1..],
WikiLinkLabelOrder::AfterPipe,
),
WikiLinkLabelOrder::BeforePipe => (
&source[separator + 1..],
&source[..separator],
WikiLinkLabelOrder::BeforePipe,
),
}
} else {
(source, source, configured_order)
};
let target = unescape_string(target_source);
if target.is_empty() {
return None;
}
let label = unescape_string(label_source);
let end = close + 2;
Some((
end,
Inline::WikiLink(WikiLink {
meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
target,
label,
label_order,
}),
))
}
fn wikilink_close_step(input: &str, cursor: usize) -> Step {
let bytes = input.as_bytes();
match bytes.get(cursor) {
None | Some(b'\n' | b'\r') => Step::Done(None),
Some(b'\\') => {
let escaped = cursor + 1;
Step::Next(next_char(input, escaped).map_or(escaped, |(after_escape, _)| after_escape))
}
Some(b']') if bytes.get(cursor + 1) == Some(&b']') => Step::Done(Some(cursor)),
Some(_) => Step::Next(next_char(input, cursor).map_or(input.len(), |(next, _)| next)),
}
}
fn find_wikilink_separator(input: &str) -> Option<usize> {
let bytes = input.as_bytes();
let mut cursor = 0;
while cursor < input.len() {
match bytes[cursor] {
b'\\' => {
cursor += 1;
if cursor < input.len() {
cursor = next_char(input, cursor)?.0;
}
}
b'|' => return Some(cursor),
_ => cursor = next_char(input, cursor)?.0,
}
}
None
}
fn trailing_space_count(input: &str) -> usize {
input
.as_bytes()
.iter()
.rev()
.take_while(|byte| matches!(**byte, b' ' | b'\t'))
.count()
}
fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
let bytes = input.as_bytes();
trailing >= 2
&& bytes[bytes.len() - trailing..]
.iter()
.all(|byte| *byte == b' ')
}
fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
if input.as_bytes().get(open) != Some(&b'[') {
return None;
}
let close = path_walk(open + 1, |cursor| reference_label_step(input, cursor))?;
reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
}
fn reference_label_step(input: &str, cursor: usize) -> Step {
let Some((next, char)) = next_char(input, cursor) else {
return Step::Done(None);
};
match char {
'\\' => Step::Next(next_char(input, next).map_or(next, |(after_escape, _)| after_escape)),
'[' => Step::Done(None),
']' => Step::Done(Some(cursor)),
_ => Step::Next(next),
}
}
fn inline_nesting(inlines: &[Inline]) -> usize {
let mut deepest = 0;
let mut pending = vec![(inlines, 0)];
while let Some((nodes, depth)) = pending.pop() {
for node in nodes {
if is_inline_container(node) {
deepest = deepest.max(depth + 1);
pending.push((node.children(), depth + 1));
}
}
}
deepest
}
fn is_inline_container(inline: &Inline) -> bool {
matches!(
inline,
Inline::Emphasis(_)
| Inline::Strong(_)
| Inline::Underline(_)
| Inline::Delete(_)
| Inline::Insert(_)
| Inline::Mark(_)
| Inline::Subscript(_)
| Inline::Superscript(_)
| Inline::Spoiler(_)
| Inline::Link(_)
| Inline::Image(_)
| Inline::LinkReference(_)
| Inline::ImageReference(_)
| Inline::InlineFootnote(_)
| Inline::TextDirective(_)
)
}
fn contains_link_inline(inlines: &[Inline]) -> bool {
inlines.iter().any(|inline| {
matches!(inline, Inline::Link(_) | Inline::LinkReference(_))
|| contains_link_inline(inline.children())
})
}
fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
if input.as_bytes().get(open) != Some(&b'[') {
return None;
}
let mut lookups = DirectLookups { input };
bracket_walk(open + 1, |cursor| {
link_label_step(&mut lookups, input, cursor)
})
}
fn link_label_step(lookups: &mut impl Lookups, input: &str, cursor: usize) -> BracketStep {
let Some((next, char)) = next_char(input, cursor) else {
return BracketStep::End;
};
match char {
'\\' => {
BracketStep::Pass(next_char(input, next).map_or(next, |(after_escape, _)| after_escape))
}
'`' => BracketStep::Pass(code_span_end(lookups, input, cursor).unwrap_or(next)),
'<' => BracketStep::Pass(
autolink_end(lookups, input, cursor)
.or_else(|| html_inline_end(lookups, input, cursor))
.unwrap_or(next),
),
'[' => BracketStep::Open(next),
']' => BracketStep::Close(next),
_ => BracketStep::Pass(next),
}
}
fn parse_text_directive(
input: &str,
index: usize,
base_offset: usize,
options: &SyntaxOptions,
definitions: &[String],
diagnostics: &mut Vec<Diagnostic>,
pass: &mut InlinePass,
) -> Option<(usize, Inline)> {
if input[index..].starts_with("::") {
return None;
}
if index > 0 {
let previous = input[..index].chars().next_back()?;
if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
return None;
}
}
let opener_source = &input[index + 1..];
let opener_offset = index + 1;
let opener = parse_directive_opener_with(opener_source, |close, open| {
let found = match close {
DirectiveClose::Label => pass.scan.link_label_end(opener_offset + open),
DirectiveClose::Attributes => {
pass.scan.directive_attributes_close(opener_offset + open)
}
};
found.map(|position| position - opener_offset)
});
let (name, label_source, attributes, consumed) = match opener {
Some(opener) => opener,
None => {
if directive_opener_looks_malformed(opener_source) {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCode::InvalidDirectiveName,
Span::new(base_offset + index, base_offset + input.len()),
"text directive opener is malformed",
));
}
return None;
}
};
let label = label_source
.map(|source| {
parse_inlines_with_context(
source,
base_offset + index + 1 + name.len() + 1,
options,
definitions,
diagnostics,
pass.state,
)
})
.unwrap_or_default();
Some((
index + 1 + consumed,
Inline::TextDirective(TextDirective {
meta: NodeMeta::new(Some(Span::new(
base_offset + index,
base_offset + index + 1 + consumed,
))),
name,
label,
attributes,
}),
))
}
fn parse_directive_opener(
input: &str,
) -> Option<(String, Option<&str>, Vec<DirectiveAttribute>, usize)> {
parse_directive_opener_with(input, |close, open| match close {
DirectiveClose::Label => find_link_label_end(input, open),
DirectiveClose::Attributes => find_directive_attributes_close(input, open),
})
}
#[derive(Clone, Copy)]
enum DirectiveClose {
Label,
Attributes,
}
fn parse_directive_opener_with(
input: &str,
mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
) -> Option<(String, Option<&str>, Vec<DirectiveAttribute>, usize)> {
let mut index = 0;
while let Some((next, char)) = next_char(input, index) {
if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
index = next;
} else {
break;
}
}
let name = &input[..index];
if !is_directive_name(name) {
return None;
}
let mut label = None;
let mut attributes = Vec::new();
let mut consumed = index;
if input.as_bytes().get(consumed) == Some(&b'[') {
let close = find_close(DirectiveClose::Label, consumed)?;
label = Some(&input[consumed + 1..close]);
consumed = close + 1;
}
if input.as_bytes().get(consumed) == Some(&b'{') {
let close = find_close(DirectiveClose::Attributes, consumed)?;
attributes = parse_attributes(&input[consumed + 1..close]);
consumed = close + 1;
}
Some((name.into(), label, attributes, consumed))
}
fn directive_opener_looks_malformed(input: &str) -> bool {
let mut index = 0;
while let Some((next, char)) = next_char(input, index) {
if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
index = next;
} else {
break;
}
}
index > 0
&& is_directive_name(&input[..index])
&& matches!(input.as_bytes().get(index), Some(b'[' | b'{'))
}
const DIRECTIVE_QUOTE_STATES: usize = 3;
fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
if input.as_bytes().get(open) != Some(&b'{') {
return None;
}
path_walk((open + 1) * DIRECTIVE_QUOTE_STATES, |node| {
directive_attributes_step(input, node)
})
}
fn directive_attributes_step(input: &str, node: usize) -> Step {
let bytes = input.as_bytes();
let (cursor, quote) = (node / DIRECTIVE_QUOTE_STATES, node % DIRECTIVE_QUOTE_STATES);
let at = |position: usize, quote: usize| Step::Next(position * DIRECTIVE_QUOTE_STATES + quote);
let Some(&byte) = bytes.get(cursor) else {
return Step::Done(None);
};
if byte == b'\\' {
return at((cursor + 2).min(bytes.len()), quote);
}
match (quote, byte) {
(0, b'"') => at(cursor + 1, 1),
(0, b'\'') => at(cursor + 1, 2),
(0, b'}') => Step::Done(Some(cursor)),
(1, b'"') | (2, b'\'') => at(cursor + 1, 0),
_ => at(cursor + 1, quote),
}
}
fn parse_attributes(input: &str) -> Vec<DirectiveAttribute> {
let mut attributes = Vec::new();
let mut cursor = 0;
while cursor < input.len() {
cursor = skip_spaces(input, cursor);
if cursor >= input.len() {
break;
}
if input.as_bytes().get(cursor) == Some(&b'#') {
let (id, next) = parse_attribute_token(input, cursor + 1);
if !id.is_empty() {
attributes.push(DirectiveAttribute {
name: "id".into(),
value: Some(id.into()),
});
}
cursor = next;
continue;
}
if input.as_bytes().get(cursor) == Some(&b'.') {
let (class, next) = parse_attribute_token(input, cursor + 1);
if !class.is_empty() {
attributes.push(DirectiveAttribute {
name: "class".into(),
value: Some(class.into()),
});
}
cursor = next;
continue;
}
let (name, next) = parse_attribute_name(input, cursor);
if name.is_empty() {
break;
}
cursor = skip_spaces(input, next);
if input.as_bytes().get(cursor) == Some(&b'=') {
cursor = skip_spaces(input, cursor + 1);
if let Some((value, next)) = parse_attribute_value(input, cursor) {
attributes.push(DirectiveAttribute {
name: name.into(),
value: Some(value),
});
cursor = next;
} else {
attributes.push(DirectiveAttribute {
name: name.into(),
value: Some(String::new()),
});
}
} else {
attributes.push(DirectiveAttribute {
name: name.into(),
value: None,
});
}
}
attributes
}
fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
let mut cursor = index;
while let Some((next, char)) = next_char(input, cursor) {
if char.is_whitespace() {
break;
}
cursor = next;
}
(&input[index..cursor], cursor)
}
fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
let mut cursor = index;
while let Some((next, char)) = next_char(input, cursor) {
if char.is_whitespace() || char == '=' {
break;
}
cursor = next;
}
(&input[index..cursor], cursor)
}
fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
let quote = input.as_bytes().get(index).copied();
if matches!(quote, Some(b'"' | b'\'')) {
let quote = quote?;
let mut cursor = index + 1;
while cursor < input.len() {
let (next, char) = next_char(input, cursor)?;
if char as u8 == quote && !is_escaped_at(input, cursor) {
return Some((unescape_ascii_punctuation(&input[index + 1..cursor]), next));
}
cursor = next;
}
return None;
}
let (value, next) = parse_attribute_token(input, index);
Some((
unescape_selected(value, |char| matches!(char, '\\' | '&')),
next,
))
}
struct CodeSpanSource {
value: String,
raw: String,
fence_length: usize,
}
fn parse_code_span_with(
lookups: &mut impl Lookups,
input: &str,
index: usize,
) -> Option<(usize, CodeSpanSource)> {
let len = backtick_run_len(input, index);
let search_start = index + len;
let close = lookups.code_span_close(search_start, len)?;
let raw = &input[search_start..close];
Some((
close + len,
CodeSpanSource {
value: normalize_code_span(raw),
raw: raw.into(),
fence_length: len,
},
))
}
fn code_span_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
let len = backtick_run_len(input, index);
Some(lookups.code_span_close(index + len, len)? + len)
}
fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
let bytes = input.as_bytes();
let mut cursor = start;
while cursor < bytes.len() {
if bytes[cursor] != b'`' {
cursor = next_char(input, cursor)
.map(|(next, _)| next)
.unwrap_or(bytes.len());
continue;
}
let run_len = bytes[cursor..]
.iter()
.take_while(|byte| **byte == b'`')
.count();
if run_len == marker_len {
return Some(cursor);
}
cursor += run_len;
}
None
}
fn normalize_code_span(input: &str) -> String {
let mut normalized = String::new();
let mut cursor = 0;
while cursor < input.len() {
let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
if char == '\r' {
if input.as_bytes().get(next) == Some(&b'\n') {
cursor = next + 1;
} else {
cursor = next;
}
normalized.push(' ');
continue;
}
if char == '\n' {
normalized.push(' ');
cursor = next;
continue;
}
normalized.push(char);
cursor = next;
}
if normalized.starts_with(' ')
&& normalized.ends_with(' ')
&& normalized.chars().any(|char| char != ' ')
{
normalized[1..normalized.len() - 1].into()
} else {
normalized
}
}
fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
delimiter_flanking(input, index, marker_len).left
}
fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
delimiter_flanking(input, index, marker_len).right
}
fn is_single_tilde_delete_close(input: &str, index: usize) -> bool {
input.as_bytes()[index] == b'~'
&& !is_escaped_at(input, index)
&& single_tilde_can_close_delete(input, index)
}
fn single_tilde_can_open_delete(input: &str, index: usize) -> bool {
starts_exact_byte_run(input, index, b'~', 1)
&& can_open_delimited(input, index, 1)
&& !tilde_is_alphanumeric_interior(input, index)
}
fn single_tilde_can_close_delete(input: &str, index: usize) -> bool {
starts_exact_byte_run(input, index, b'~', 1)
&& can_close_delimited(input, index, 1)
&& !tilde_is_alphanumeric_interior(input, index)
}
fn single_tilde_delete_takes_precedence(
options: &SyntaxOptions,
input: &str,
index: usize,
scan: &mut InlineScan,
) -> bool {
options.constructs.gfm_strikethrough
&& options.parse.single_tilde_strikethrough
&& single_tilde_can_open_delete(input, index)
&& scan.single_tilde_delete_close(index + 1).is_some()
}
fn tilde_is_alphanumeric_interior(input: &str, index: usize) -> bool {
let previous = input[..index].chars().next_back();
let next = input[index + 1..].chars().next();
previous.is_some_and(|char| char.is_alphanumeric())
&& next.is_some_and(|char| char.is_alphanumeric())
}
fn starts_exact_byte_run(input: &str, index: usize, marker: u8, len: usize) -> bool {
input.as_bytes().get(index) == Some(&marker)
&& delimiter_byte_run_start(input, index, marker) == index
&& delimiter_byte_run_len(input, index, marker) == len
}
fn delimiter_byte_run_start(input: &str, index: usize, marker: u8) -> usize {
let bytes = input.as_bytes();
let mut start = index;
while start > 0 && bytes[start - 1] == marker && !is_escaped_at(input, start - 1) {
start -= 1;
}
start
}
fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
let bytes = input.as_bytes();
let mut cursor = index;
while bytes.get(cursor) == Some(&marker) {
cursor += 1;
}
cursor - index
}
fn can_open_underscore(input: &str, index: usize, marker_len: usize) -> bool {
let flanking = delimiter_flanking(input, index, marker_len);
flanking.left
&& (!flanking.right || flanking.previous.is_some_and(|c| c.is_ascii_punctuation()))
}
fn can_close_underscore(input: &str, index: usize, marker_len: usize) -> bool {
let flanking = delimiter_flanking(input, index, marker_len);
flanking.right && (!flanking.left || flanking.next.is_some_and(|c| c.is_ascii_punctuation()))
}
#[derive(Clone, Copy)]
struct DelimiterFlanking {
left: bool,
right: bool,
previous: Option<char>,
next: Option<char>,
}
fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
delimiter_flanking_within(input, index, marker_len, false, false)
}
fn delimiter_flanking_within(
input: &str,
index: usize,
marker_len: usize,
bounded_before: bool,
bounded_after: bool,
) -> DelimiterFlanking {
let previous = if bounded_before {
None
} else {
input[..index].chars().next_back().map(source_char)
};
let next = if bounded_after {
None
} else {
input[index + marker_len..].chars().next().map(source_char)
};
let previous_whitespace = previous.is_none_or(char::is_whitespace);
let next_whitespace = next.is_none_or(char::is_whitespace);
let previous_punctuation = previous.is_some_and(is_flanking_punctuation);
let next_punctuation = next.is_some_and(is_flanking_punctuation);
let left = next.is_some()
&& !next_whitespace
&& !(next_punctuation && !previous_whitespace && !previous_punctuation);
let right = previous.is_some()
&& !previous_whitespace
&& !(previous_punctuation && !next_whitespace && !next_punctuation);
DelimiterFlanking {
left,
right,
previous,
next,
}
}
fn parse_math_inline(
lookups: &mut impl Lookups,
input: &str,
index: usize,
) -> Option<(usize, String, MathInlineKind)> {
if let Some((end, value)) = parse_math_code_inline(lookups, input, index) {
return Some((end, value, MathInlineKind::Code));
}
let bytes = input.as_bytes();
let open_dollars = bytes[index..]
.iter()
.take_while(|byte| **byte == b'$')
.count();
if open_dollars == 0 || open_dollars > 2 {
return None;
}
let content_start = index + open_dollars;
let close = scan_to_closing_dollar(input, content_start, open_dollars)?;
let content_end = close - open_dollars;
if content_end <= content_start {
return None;
}
let raw = &input[content_start..content_end];
let value = if open_dollars == 1 {
normalize_math_text(raw)
} else {
raw.into()
};
let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
Some((close, value, MathInlineKind::Dollar { dollars }))
}
fn scan_to_closing_dollar(input: &str, start: usize, open_dollars: usize) -> Option<usize> {
let bytes = input.as_bytes();
if open_dollars == 1 && bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
return None;
}
let mut cursor = start;
loop {
while cursor < bytes.len() && bytes[cursor] != b'$' {
cursor += 1;
}
if cursor >= bytes.len() {
return None;
}
let prev = bytes[cursor - 1];
if open_dollars == 1 && is_math_space(prev) {
return None;
}
if open_dollars == 1 && prev == b'\\' {
cursor += 1;
continue;
}
let run = bytes[cursor..]
.iter()
.take(open_dollars)
.take_while(|byte| **byte == b'$')
.count();
if open_dollars == 1 && bytes.get(cursor + run).is_some_and(u8::is_ascii_digit) {
return None;
}
if run == open_dollars {
return Some(cursor + run);
}
cursor += run;
}
}
fn is_math_space(byte: u8) -> bool {
matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
}
fn normalize_math_text(input: &str) -> String {
let mut normalized = String::new();
let mut cursor = 0;
while cursor < input.len() {
let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
if char == '\r' {
if input.as_bytes().get(next) == Some(&b'\n') {
cursor = next + 1;
} else {
cursor = next;
}
normalized.push(' ');
continue;
}
if char == '\n' {
normalized.push(' ');
cursor = next;
continue;
}
normalized.push(char);
cursor = next;
}
if normalized.starts_with(' ')
&& normalized.ends_with(' ')
&& normalized.chars().any(|char| char != ' ')
{
normalized[1..normalized.len() - 1].into()
} else {
normalized
}
}
fn parse_math_code_inline(
lookups: &mut impl Lookups,
input: &str,
index: usize,
) -> Option<(usize, String)> {
if !input[index..].starts_with("$`") {
return None;
}
let search_start = index + 2;
let close = lookups.find("`$", search_start)?;
if close == search_start {
return None;
}
Some((close + 2, input[search_start..close].into()))
}
fn parse_link_resource(
lookups: &mut impl Lookups,
input: &str,
open: usize,
) -> Option<(usize, ParsedLinkResource)> {
let bytes = input.as_bytes();
if bytes.get(open) != Some(&b'(') {
return None;
}
let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
if bytes.get(cursor) == Some(&b')') {
return Some((
cursor + 1,
ParsedLinkResource {
destination: String::new(),
destination_kind: LinkDestinationKind::Omitted,
title: None,
title_kind: None,
},
));
}
if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
let (title, title_kind, next) = parse_link_title(input, cursor)?;
cursor = skip_link_resource_space(input, next)?;
if bytes.get(cursor) == Some(&b')') {
return Some((
cursor + 1,
ParsedLinkResource {
destination: String::new(),
destination_kind: LinkDestinationKind::Omitted,
title: Some(title),
title_kind: Some(title_kind),
},
));
}
return None;
}
let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
cursor = after_destination;
if bytes.get(cursor) == Some(&b')') {
return Some((
cursor + 1,
ParsedLinkResource {
destination,
destination_kind,
title: None,
title_kind: None,
},
));
}
if !had_space {
return None;
}
let (title, title_kind, next) = parse_link_title(input, cursor)?;
cursor = skip_link_resource_space(input, next)?;
if bytes.get(cursor) == Some(&b')') {
Some((
cursor + 1,
ParsedLinkResource {
destination,
destination_kind,
title: Some(title),
title_kind: Some(title_kind),
},
))
} else {
None
}
}
fn parse_link_destination(
lookups: &mut impl Lookups,
input: &str,
index: usize,
) -> Option<(String, LinkDestinationKind, usize)> {
if input.as_bytes().get(index) == Some(&b'<') {
let stop = lookups.angle_destination_stop(index + 1)?;
if input.as_bytes()[stop] != b'>' {
return None;
}
return Some((
unescape_ascii_punctuation(&input[index + 1..stop]),
LinkDestinationKind::Angle,
stop + 1,
));
}
let mut cursor = index;
let mut depth = 0usize;
while cursor < input.len() {
let (next, char) = next_char(input, cursor)?;
if (char == ' ' || source_char(char).is_ascii_control()) && depth == 0 {
break;
}
if char == '(' && !is_escaped_at(input, cursor) {
depth += 1;
if depth > 32 {
return None;
}
} else if char == ')' && !is_escaped_at(input, cursor) {
if depth == 0 {
break;
}
depth -= 1;
}
cursor = next;
}
if cursor == index || depth > 0 {
None
} else {
Some((
unescape_ascii_punctuation(&input[index..cursor]),
LinkDestinationKind::Bare,
cursor,
))
}
}
fn parse_link_title(input: &str, index: usize) -> Option<(String, LinkTitleKind, usize)> {
let opener = input.as_bytes().get(index).copied()?;
let (closer, title_kind) = match opener {
b'"' => ('"', LinkTitleKind::DoubleQuote),
b'\'' => ('\'', LinkTitleKind::SingleQuote),
b'(' => (')', LinkTitleKind::Paren),
_ => return None,
};
let mut cursor = index + 1;
while cursor < input.len() {
let (next, char) = next_char(input, cursor)?;
if char == closer && !is_escaped_at(input, cursor) {
if contains_blank_line(&input[index + 1..cursor]) {
return None;
}
return Some((
unescape_ascii_punctuation(&input[index + 1..cursor]),
title_kind,
next,
));
}
if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
return None;
}
cursor = next;
}
None
}
fn contains_blank_line(input: &str) -> bool {
if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
return false;
}
let lines = collect_lines(input, 0);
let interior = lines.len().saturating_sub(1);
lines
.iter()
.take(interior)
.skip(1)
.any(|line| line.text.trim().is_empty())
}
fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
}
fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
let mut line_breaks = 0usize;
let mut had_space = false;
while input
.as_bytes()
.get(index)
.is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
{
had_space = true;
match input.as_bytes()[index] {
b'\n' => {
line_breaks += 1;
if line_breaks > 1 {
return None;
}
index += 1;
}
b'\r' => {
line_breaks += 1;
if line_breaks > 1 {
return None;
}
if input.as_bytes().get(index + 1) == Some(&b'\n') {
index += 2;
} else {
index += 1;
}
}
_ => index += 1,
}
}
Some((index, had_space))
}
pub(crate) fn parse_character_reference(input: &str, index: usize) -> Option<(usize, String)> {
let rest = input.get(index..)?;
if let Some(rest) = rest
.strip_prefix("&#x")
.or_else(|| rest.strip_prefix("&#X"))
{
let digits = find_reference_terminator(rest, 6)?;
if digits == 0 || !rest[..digits].bytes().all(|byte| byte.is_ascii_hexdigit()) {
return None;
}
let value = u32::from_str_radix(&rest[..digits], 16).ok()?;
return Some((
index + 3 + digits + 1,
character_reference_value(value).into(),
));
}
if let Some(rest) = rest.strip_prefix("&#") {
let digits = find_reference_terminator(rest, 7)?;
if digits == 0 || !rest[..digits].bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let value = rest[..digits].parse::<u32>().ok()?;
return Some((
index + 2 + digits + 1,
character_reference_value(value).into(),
));
}
let name_end = find_reference_terminator(rest, 32)?;
if name_end == 0 {
return None;
}
let name = &rest[1..name_end];
named_character_reference(name).map(|value| (index + name_end + 1, value.into()))
}
fn find_reference_terminator(rest: &str, max_len: usize) -> Option<usize> {
rest.as_bytes()
.iter()
.take(max_len + 1)
.position(|byte| *byte == b';')
}
pub(crate) fn character_reference_value(value: u32) -> char {
if value == 0 {
'\u{FFFD}'
} else {
char::from_u32(value).unwrap_or('\u{FFFD}')
}
}
pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
let bytes = input.as_bytes();
let mut cursor = index;
let mut count = 0;
while cursor > 0 && bytes[cursor - 1] == b'\\' {
count += 1;
cursor -= 1;
}
count % 2 == 1
}
fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
let (destination, destination_kind, next) =
parse_link_destination(&mut DirectLookups { input }, input, cursor)?;
cursor = next;
let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
cursor = next;
if cursor >= input.len() {
return Some(ParsedLinkResource {
destination,
destination_kind,
title: None,
title_kind: None,
});
}
if !had_space {
return None;
}
let (title, title_kind, next) = parse_link_title(input, cursor)?;
let after_title = skip_link_resource_space(input, next)?;
(after_title == input.len()).then_some(ParsedLinkResource {
destination,
destination_kind,
title: Some(title),
title_kind: Some(title_kind),
})
}
fn line_can_start_definition_title(input: &str) -> bool {
let trimmed = input.trim_start();
matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
}
fn unescape_ascii_punctuation(input: &str) -> String {
unescape_selected(input, |char| char.is_ascii_punctuation())
}
fn unescape_string(input: &str) -> String {
unescape_selected(input, |char| char.is_ascii_punctuation() || char == '&')
}
fn unescape_selected(input: &str, should_unescape: impl Fn(char) -> bool) -> String {
let mut output = String::new();
let mut cursor = 0;
while cursor < input.len() {
if input.as_bytes().get(cursor) == Some(&b'&') {
if let Some((end, value)) = parse_character_reference(input, cursor) {
output.push_str(&value);
cursor = end;
continue;
}
}
let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
if char == '\\' {
if let Some((after_escape, escaped)) = next_char(input, next) {
if should_unescape(escaped) {
output.push(escaped);
} else {
output.push(char);
output.push(escaped);
}
cursor = after_escape;
} else {
output.push(char);
cursor = next;
}
} else {
output.push(char);
cursor = next;
}
}
output
}
fn push_line(output: &mut String, line: &str) {
if !output.is_empty() {
output.push('\n');
}
output.push_str(line);
}
fn ensure_line_separator(output: &mut String) {
if !output.is_empty() && !ends_with_line_ending(output) {
output.push('\n');
}
}
fn ends_with_line_ending(input: &str) -> bool {
input.ends_with('\n') || input.ends_with('\r')
}
fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
if !text.is_empty() {
nodes.push(Inline::Text(Text {
meta: NodeMeta::new(Some(Span::new(text_start, end))),
value: core::mem::take(text),
}));
}
}
fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
let char = input[index..].chars().next()?;
Some((index + char.len_utf8(), char))
}
fn is_flanking_punctuation(value: char) -> bool {
value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
}
pub(crate) fn normalize_label(label: &str) -> String {
label
.replace('ẞ', "ss")
.replace('\0', "\u{FFFD}")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_uppercase()
.to_lowercase()
}
fn definition_exists(definitions: &[String], label: &str) -> bool {
if label.is_empty() || !reference_label_is_within_limit(label) {
return false;
}
definitions.binary_search(&normalize_label(label)).is_ok()
}
fn reference_label_is_within_limit(label: &str) -> bool {
label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
}
fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
let (columns, bytes) = leading_indent(input);
if columns <= 3 {
Some(&input[bytes..])
} else {
None
}
}
fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
let marker = match input.as_bytes().first()? {
b'`' => FenceMarker::Backtick,
b'~' => FenceMarker::Tilde,
_ => return None,
};
let byte = match marker {
FenceMarker::Backtick => b'`',
FenceMarker::Tilde => b'~',
};
let length = input
.as_bytes()
.iter()
.take_while(|item| **item == byte)
.count();
if length >= 3 {
Some((marker, length))
} else {
None
}
}
fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
let byte = match marker {
FenceMarker::Backtick => b'`',
FenceMarker::Tilde => b'~',
};
let count = input
.as_bytes()
.iter()
.take_while(|item| **item == byte)
.count();
count >= length && input[count..].trim().is_empty()
}
fn trim_closing_hashes(input: &str) -> &str {
let input = input.trim_end();
let hash_start = input.trim_end_matches('#').len();
if hash_start == input.len() {
return input;
}
if hash_start == 0 {
return "";
}
let before = &input[..hash_start];
if before.ends_with(' ') || before.ends_with('\t') {
before.trim_end()
} else {
input
}
}
fn list_marker_info(input: &str) -> Option<ListMarkerInfo<'_>> {
let trimmed = trim_up_to_three_spaces(input)?;
let indent = input.len() - trimmed.len();
let bytes = trimmed.as_bytes();
match bytes.first()? {
b'-' | b'*' | b'+' if is_list_padding_byte(bytes.get(1).copied()) => {
let delimiter = match bytes[0] {
b'-' => ListDelimiter::Dash,
b'*' => ListDelimiter::Asterisk,
_ => ListDelimiter::Plus,
};
let (content_offset, content_indent) = list_content_offset(trimmed, 1, indent);
Some(ListMarkerInfo {
ordered: false,
start: None,
delimiter,
indent,
marker_len: 1,
content_indent,
content: &trimmed[content_offset..],
})
}
byte if byte.is_ascii_digit() => {
let mut end = 0;
while bytes.get(end).is_some_and(|byte| byte.is_ascii_digit()) {
end += 1;
}
if end > 9 {
return None;
}
let delimiter = match bytes.get(end)? {
b'.' => ListDelimiter::Period,
b')' => ListDelimiter::Paren,
_ => return None,
};
if !is_list_padding_byte(bytes.get(end + 1).copied()) {
return None;
}
let start = trimmed[..end].parse().ok()?;
let marker_len = end + 1;
let (content_offset, content_indent) = list_content_offset(trimmed, marker_len, indent);
Some(ListMarkerInfo {
ordered: true,
start: Some(start),
delimiter,
indent,
marker_len,
content_indent,
content: &trimmed[content_offset..],
})
}
_ => None,
}
}
fn list_content_offset(input: &str, marker_len: usize, indent: usize) -> (usize, usize) {
let bytes = input.as_bytes();
if bytes.get(marker_len).is_none() {
return (marker_len, indent + marker_len + 1);
}
let mut cursor = marker_len;
let mut column = indent + marker_len;
let marker_end_column = column;
while let Some(byte) = bytes.get(cursor) {
match *byte {
b' ' => column += 1,
b'\t' => column += 4 - (column % 4),
_ => break,
}
cursor += 1;
}
if cursor >= bytes.len() {
return (cursor, marker_end_column + 1);
}
let padding_columns = column.saturating_sub(marker_end_column);
if padding_columns > 0 && padding_columns <= 4 {
(cursor, column)
} else {
(marker_len + 1, marker_end_column + 1)
}
}
fn list_marker_first_content<'a>(input: &'a str, marker: ListMarkerInfo<'a>) -> Cow<'a, str> {
let Some(trimmed) = trim_up_to_three_spaces(input) else {
return Cow::Borrowed(marker.content);
};
let after_marker = &trimmed[marker.marker_len..];
if after_marker.starts_with('\t') {
strip_leading_indent_columns_from(after_marker, 1, marker.indent + marker.marker_len)
} else {
Cow::Borrowed(marker.content)
}
}
fn is_list_padding_byte(byte: Option<u8>) -> bool {
matches!(byte, None | Some(b' ' | b'\t'))
}
fn same_list_marker(left: ListMarkerInfo<'_>, right: ListMarkerInfo<'_>) -> bool {
left.ordered == right.ordered && left.delimiter == right.delimiter
}
fn sibling_list_marker_at_line(
input: &str,
first_marker: ListMarkerInfo<'_>,
content_indent: usize,
) -> bool {
list_marker_info(input).is_some_and(|candidate| {
same_list_marker(first_marker, candidate) && candidate.indent < content_indent
})
}
fn same_list_marker_line(input: &str, first_marker: ListMarkerInfo<'_>) -> bool {
list_marker_info(input).is_some_and(|candidate| same_list_marker(first_marker, candidate))
}
fn next_nonblank_line(lines: &[Line<'_>], mut index: usize) -> usize {
while index < lines.len() && lines[index].text.trim().is_empty() {
index += 1;
}
index
}
fn leading_indent(input: &str) -> (usize, usize) {
let mut column = 0usize;
let mut bytes = 0usize;
for byte in input.as_bytes() {
match *byte {
b' ' => column += 1,
b'\t' => column += 4 - (column % 4),
_ => break,
}
bytes += 1;
}
(column, bytes)
}
fn leading_indent_columns(input: &str) -> usize {
leading_indent(input).0
}
fn strip_leading_indent_columns(input: &str, max_columns: usize) -> Cow<'_, str> {
strip_leading_indent_columns_from(input, max_columns, 0)
}
fn strip_leading_indent_columns_from(
input: &str,
max_columns: usize,
start_column: usize,
) -> Cow<'_, str> {
let mut column = start_column;
let target_column = start_column + max_columns;
for (index, byte) in input.as_bytes().iter().enumerate() {
let next = match *byte {
b' ' => column + 1,
b'\t' => column + (4 - (column % 4)),
_ => return Cow::Borrowed(&input[index..]),
};
if next > target_column {
if *byte == b'\t' && column < target_column {
let residual = next - target_column;
let mut owned = String::with_capacity(residual + input.len() - (index + 1));
for _ in 0..residual {
owned.push(' ');
}
let mut rest_column = next;
let mut rest_index = index + 1;
while let Some(rest_byte) = input.as_bytes().get(rest_index) {
match *rest_byte {
b' ' => {
owned.push(' ');
rest_column += 1;
rest_index += 1;
}
b'\t' => {
let width = 4 - (rest_column % 4);
for _ in 0..width {
owned.push(' ');
}
rest_column += width;
rest_index += 1;
}
_ => break,
}
}
owned.push_str(&input[rest_index..]);
return Cow::Owned(owned);
}
return Cow::Borrowed(&input[index..]);
}
column = next;
}
Cow::Borrowed("")
}
fn strip_list_continuation(input: &str, content_indent: usize, list_indent: usize) -> Cow<'_, str> {
let (indent_columns, indent_bytes) = leading_indent(input);
if indent_columns >= content_indent {
strip_leading_indent_columns(input, content_indent)
} else if indent_columns > list_indent {
Cow::Borrowed(&input[indent_bytes..])
} else {
Cow::Borrowed(trim_ascii_start(input))
}
}
fn take_task_marker_from_children(children: &mut [Block]) -> Option<bool> {
let Some(Block::Paragraph(paragraph)) = children.first_mut() else {
return None;
};
take_task_marker_from_inlines(&mut paragraph.children)
}
fn take_task_marker_from_inlines(inlines: &mut Vec<Inline>) -> Option<bool> {
let Some(Inline::Text(text)) = inlines.first() else {
return None;
};
let first = text.value.clone();
if let Some((checked, consumed)) = task_marker_inline_prefix(&first) {
if !first[consumed..].is_empty() || inlines_have_content_after(inlines, 1) {
remove_text_prefix(inlines, consumed);
return Some(checked);
}
}
if let Some(checked) = task_marker_at_text_end(&first) {
if inlines
.get(1)
.is_some_and(|inline| matches!(inline, Inline::SoftBreak(_)))
&& inlines_have_content_after(inlines, 2)
{
inlines.remove(1);
inlines.remove(0);
return Some(checked);
}
}
if task_marker_split_open(&first)
&& inlines
.get(1)
.is_some_and(|inline| matches!(inline, Inline::SoftBreak(_)))
{
let Some(Inline::Text(next)) = inlines.get(2) else {
return None;
};
if let Some((checked, consumed)) = task_marker_split_close_prefix(&next.value) {
if !next.value[consumed..].is_empty() || inlines_have_content_after(inlines, 3) {
inlines.remove(1);
inlines.remove(0);
remove_text_prefix(inlines, consumed);
return Some(checked);
}
}
}
None
}
fn task_marker_inline_prefix(input: &str) -> Option<(bool, usize)> {
let start = leading_trim_bytes(input);
let rest = &input[start..];
let checked = task_marker_checked(rest)?;
let after_marker = start + 3;
match input.as_bytes().get(after_marker) {
Some(b' ' | b'\t') => Some((checked, after_marker + 1)),
_ => None,
}
}
fn task_marker_at_text_end(input: &str) -> Option<bool> {
let start = leading_trim_bytes(input);
let rest = &input[start..];
let checked = task_marker_checked(rest)?;
if rest.len() == 3 {
Some(checked)
} else {
None
}
}
fn task_marker_split_open(input: &str) -> bool {
let start = leading_trim_bytes(input);
input[start..] == *"["
}
fn task_marker_split_close_prefix(input: &str) -> Option<(bool, usize)> {
match input.as_bytes().get(..2)? {
b"] " => Some((false, 2)),
b"]\t" => Some((false, 2)),
b"x]" | b"X]" if matches!(input.as_bytes().get(2), Some(b' ' | b'\t')) => Some((true, 3)),
_ => None,
}
}
fn task_marker_checked(input: &str) -> Option<bool> {
if input.starts_with("[ ]") {
Some(false)
} else if input.starts_with("[x]") || input.starts_with("[X]") {
Some(true)
} else {
None
}
}
fn remove_text_prefix(inlines: &mut Vec<Inline>, consumed: usize) {
if let Some(Inline::Text(text)) = inlines.first_mut() {
text.value = text.value[consumed..].into();
if text.value.is_empty() {
inlines.remove(0);
}
}
}
fn inlines_have_content_after(inlines: &[Inline], start: usize) -> bool {
inlines.iter().skip(start).any(|inline| match inline {
Inline::Text(text) => !text.value.is_empty(),
Inline::SoftBreak(_) | Inline::LineBreak(_) => false,
_ => true,
})
}
fn update_list_item_fence(line: &str, open_fence: &mut Option<(FenceMarker, usize)>) {
let Some(trimmed) = trim_up_to_three_spaces(line) else {
return;
};
if let Some((marker, length)) = *open_fence {
if fence_close(trimmed, marker, length) {
*open_fence = None;
}
return;
}
if let Some((marker, length)) = fence_start(trimmed) {
*open_fence = Some((marker, length));
}
}
fn trim_ascii_start(input: &str) -> &str {
input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
}
fn leading_trim_bytes(input: &str) -> usize {
input.len() - trim_ascii_start(input).len()
}
fn parse_table_delimiter(input: &str, spoiler: bool) -> Option<Vec<TableAlignment>> {
let cells = split_table_row(input, spoiler);
if cells.is_empty() {
return None;
}
let mut alignments = Vec::new();
for cell in cells {
alignments.push(table_delimiter_alignment(cell.trim())?);
}
Some(alignments)
}
fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
let bytes = cell.as_bytes();
let mut cursor = 0;
let left = bytes.first() == Some(&b':');
if left {
cursor += 1;
}
let dash_start = cursor;
while bytes.get(cursor) == Some(&b'-') {
cursor += 1;
}
if cursor == dash_start {
return None;
}
let right = bytes.get(cursor) == Some(&b':');
if right {
cursor += 1;
}
if cursor != bytes.len() {
return None;
}
Some(match (left, right) {
(true, true) => TableAlignment::Center,
(true, false) => TableAlignment::Left,
(false, true) => TableAlignment::Right,
(false, false) => TableAlignment::None,
})
}
fn table_indent_line(input: &str, indented_code: bool) -> Option<&str> {
if indented_code {
trim_up_to_three_spaces(input)
} else {
Some(input)
}
}
pub(crate) fn table_row_delimiters(row: &str, spoiler: bool) -> Vec<usize> {
scan_table_row(row, spoiler).0
}
fn scan_table_row(row: &str, spoiler: bool) -> (Vec<usize>, Vec<(usize, usize)>) {
let bytes = row.as_bytes();
let mut bars: Vec<TableBar> = Vec::new();
let mut runs: Vec<(usize, usize)> = Vec::new();
let mut backticks = false;
let mut cursor = 0;
while cursor < bytes.len() {
match bytes[cursor] {
b'\\' => {
let backslashes = delimiter_byte_run_len(row, cursor, b'\\');
let pipe = cursor + backslashes;
if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
let text_start = if backslashes == 1 { cursor } else { pipe };
push_table_bar(&mut bars, &mut runs, pipe, text_start, true);
cursor = pipe + 1;
} else {
cursor = pipe;
}
}
b'|' => {
push_table_bar(&mut bars, &mut runs, cursor, cursor, false);
cursor += 1;
}
byte => {
backticks |= byte == b'`';
cursor += 1;
}
}
}
if !spoiler || (!backticks && runs.iter().all(|&(_, length)| length == 1)) {
let delimiters = bars
.iter()
.filter(|bar| !bar.escaped)
.map(|bar| bar.position)
.collect();
return (delimiters, Vec::new());
}
let mut code_delimits = vec![false; bars.len()];
for &(first, length) in &runs {
let mut bar = first;
while bar < first + length {
if bar + 1 < first + length && !bars[bar].escaped && !bars[bar + 1].escaped {
bar += 2;
} else {
code_delimits[bar] = !bars[bar].escaped;
bar += 1;
}
}
}
let mut breaker = 0;
let code_spans = if backticks {
inline_code_spans(row, |open, close| {
while breaker < bars.len() && (bars[breaker].position < open || !code_delimits[breaker])
{
breaker += 1;
}
bars.get(breaker).is_none_or(|bar| close < bar.position)
})
} else {
Vec::new()
};
let mut delimits = vec![false; bars.len()];
let mut held = Vec::new();
let mut opener: Option<usize> = None;
let mut blocked = false;
let mut code = code_spans.iter().peekable();
for (first, length) in runs {
let end = first + length;
let position = bars[first].position;
while code.peek().is_some_and(|&&(_, close)| close < position) {
code.next();
}
if code.peek().is_some_and(|&&(open, _)| open < position) {
delimits[first..end].copy_from_slice(&code_delimits[first..end]);
blocked |= code_delimits[first..end].contains(&true);
continue;
}
if length == 1 {
delimits[first] = true;
blocked |= !bars[first].escaped;
continue;
}
let mut rest = first;
if let Some(opened) = opener.take() {
let escaped = bars[opened..opened + 2]
.iter()
.chain(&bars[first..first + 2])
.any(|bar| bar.escaped);
if escaped && blocked {
delimits[opened..opened + 2].fill(true);
} else {
held.push((bars[opened].position, bars[first + 1].position));
rest = first + 2;
}
}
let mut open_at = end;
if end - rest >= 2 {
open_at = end - 2;
opener = Some(open_at);
blocked = false;
}
delimits[rest..open_at].fill(true);
}
if let Some(opened) = opener {
delimits[opened..opened + 2].fill(true);
}
let mut spoilers = held.iter().copied().peekable();
let mut delimiters = Vec::new();
for (bar, delimit) in bars.iter().zip(delimits) {
while spoilers.peek().is_some_and(|&(_, end)| end < bar.position) {
spoilers.next();
}
let in_spoiler = spoilers
.peek()
.is_some_and(|&(start, end)| start <= bar.position && bar.position <= end);
if delimit && !bar.escaped && !in_spoiler {
delimiters.push(bar.position);
}
}
(delimiters, held)
}
fn inline_code_spans(
text: &str,
mut may_close: impl FnMut(usize, usize) -> bool,
) -> Vec<(usize, usize)> {
let bytes = text.as_bytes();
let mut runs = Vec::new();
let mut cursor = 0;
while cursor < bytes.len() {
match bytes[cursor] {
b'\\' => {
let backslashes = delimiter_byte_run_len(text, cursor, b'\\');
cursor += backslashes;
if bytes.get(cursor) == Some(&b'`') {
let length = delimiter_byte_run_len(text, cursor, b'`');
runs.push((cursor, length, backslashes % 2 == 1));
cursor += length;
}
}
b'`' => {
let length = delimiter_byte_run_len(text, cursor, b'`');
runs.push((cursor, length, false));
cursor += length;
}
_ => cursor += 1,
}
}
let longest = runs.iter().map(|&(_, length, _)| length).max().unwrap_or(0);
let mut by_length = vec![Vec::new(); longest + 1];
for (index, &(_, length, _)) in runs.iter().enumerate() {
by_length[length].push(index);
}
let mut spans = Vec::new();
let mut index = 0;
while index < runs.len() {
let (position, length, escaped) = runs[index];
let (open, length) = if escaped {
(position + 1, length - 1)
} else {
(position, length)
};
let close = by_length[length]
.get(by_length[length].partition_point(|&later| later <= index))
.copied()
.filter(|_| length > 0);
match close {
Some(close) if may_close(open, runs[close].0) => {
spans.push((open, runs[close].0));
index = close + 1;
}
_ => index += 1,
}
}
spans
}
struct TableBar {
position: usize,
escaped: bool,
}
fn push_table_bar(
bars: &mut Vec<TableBar>,
runs: &mut Vec<(usize, usize)>,
position: usize,
text_start: usize,
escaped: bool,
) {
let joins = bars
.last()
.is_some_and(|previous| previous.position + 1 == text_start);
match runs.last_mut() {
Some(run) if joins => run.1 += 1,
_ => runs.push((bars.len(), 1)),
}
bars.push(TableBar { position, escaped });
}
fn split_table_row(input: &str, spoiler: bool) -> Vec<String> {
let trimmed = input.trim();
let delimiters = table_row_delimiters(trimmed, spoiler);
let mut cells = Vec::new();
let mut start = 0;
for &pipe in &delimiters {
cells.push(table_cell_text(&trimmed[start..pipe]));
start = pipe + 1;
}
cells.push(table_cell_text(&trimmed[start..]));
if delimiters.first() == Some(&0) {
cells.remove(0);
}
if delimiters
.last()
.is_some_and(|&pipe| trimmed[pipe + 1..].trim().is_empty())
{
cells.pop();
}
cells
}
fn table_cell_text(source: &str) -> String {
let bytes = source.as_bytes();
let mut cell = String::with_capacity(source.len());
let mut copied = 0;
let mut cursor = 0;
while cursor < bytes.len() {
if bytes[cursor] == b'\\' {
let pipe = cursor + delimiter_byte_run_len(source, cursor, b'\\');
if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
cell.push_str(&source[copied..pipe - 1]);
copied = pipe;
}
cursor = pipe;
} else {
cursor += 1;
}
}
cell.push_str(&source[copied..]);
cell
}
fn table_can_start(lines: &[Line<'_>], index: usize, options: &SyntaxOptions) -> bool {
if !options.constructs.gfm_table || index + 1 >= lines.len() {
return false;
}
table_can_start_source(
lines[index].text,
lines[index + 1].text,
options.constructs.indented_code,
options.constructs.spoiler,
)
}
pub(crate) fn gfm_table_can_start_source(header: &str, delimiter: &str) -> bool {
table_can_start_source(header, delimiter, true, false)
}
fn table_can_start_source(
header: &str,
delimiter: &str,
indented_code: bool,
spoiler: bool,
) -> bool {
let Some(delimiter) = table_indent_line(delimiter, indented_code) else {
return false;
};
if list_marker_info(delimiter).is_some() {
return false;
}
if !table_has_separator(header, delimiter, spoiler) {
return false;
}
let Some(alignments) = parse_table_delimiter(delimiter, spoiler) else {
return false;
};
split_table_row(header, spoiler).len() == alignments.len()
}
fn table_has_separator(header: &str, delimiter: &str, spoiler: bool) -> bool {
let Some(alignments) = parse_table_delimiter(delimiter, spoiler) else {
return true;
};
if alignments.len() == 1 {
return contains_unescaped_pipe(header, spoiler)
|| contains_unescaped_pipe(delimiter, spoiler)
|| delimiter.contains(':');
}
true
}
fn contains_unescaped_pipe(input: &str, spoiler: bool) -> bool {
!table_row_delimiters(input, spoiler).is_empty()
}
fn likely_block_start(input: &str, options: &SyntaxOptions) -> bool {
let Some(trimmed) = trim_up_to_three_spaces(input) else {
return false;
};
trimmed.starts_with('#')
|| trimmed.starts_with('>')
|| trimmed.starts_with("```")
|| trimmed.starts_with("~~~")
|| list_marker_can_interrupt_paragraph(input)
|| parse_thematic_break(Line {
text: input,
eol: "",
start: 0,
end: input.len(),
end_with_eol: input.len(),
lazy: false,
})
.is_some()
|| (options.constructs.html_container && line_starts_html_container(input))
|| (options.constructs.html_block && line_starts_interrupting_html_block(input))
|| (options.constructs.math_block && math_block_fence_length(trimmed).is_some())
|| (options.constructs.directive_container && trimmed.starts_with(":::"))
|| (options.constructs.directive_leaf && trimmed.starts_with("::"))
|| (options.constructs.footnote_definition && line_starts_footnote_definition(trimmed))
}
fn line_starts_footnote_definition(trimmed: &str) -> bool {
trimmed.starts_with("[^")
&& find_footnote_definition_label_end(trimmed)
.is_some_and(|close| is_footnote_label(&trimmed[2..close]))
}
fn list_marker_can_interrupt_paragraph(input: &str) -> bool {
list_marker_info(input).is_some_and(|marker| {
!marker.content.trim().is_empty() && (!marker.ordered || marker.start == Some(1))
})
}
fn table_body_line_ends_table(line: &str, options: &SyntaxOptions) -> bool {
likely_block_start(line, options)
|| list_marker_info(line).is_some()
|| (options.constructs.html_container && line_starts_html_container(line))
|| (options.constructs.html_block && line_starts_html_block(line))
}
fn line_starts_interrupting_html_block(input: &str) -> bool {
match trim_up_to_three_spaces(input).and_then(html_block_start) {
Some(HtmlBlockKind::UntilBlank) | None => false,
Some(_) => true,
}
}
fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
let end = lookups.find(">", index)? + 1;
is_autolink(&input[index..end]).then_some(end)
}
fn html_inline_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
let rest = &input[index..];
if rest.starts_with("<!--") {
return Some(lookups.find("-->", index)? + 3);
}
if rest.starts_with("<?") {
return Some(lookups.find("?>", index)? + 2);
}
if rest.starts_with("<![CDATA[") {
return Some(lookups.find("]]>", index)? + 3);
}
if is_declaration_start(rest) {
return Some(lookups.find(">", index)? + 1);
}
parse_html_tag_with(lookups, input, index).map(|(end, _)| end)
}
fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
parse_html_tag_with(&mut DirectLookups { input }, input, index)
}
fn parse_html_tag_with<'a>(
lookups: &mut impl Lookups,
input: &'a str,
index: usize,
) -> Option<(usize, &'a str)> {
let bytes = input.as_bytes();
if bytes.get(index) != Some(&b'<') {
return None;
}
let closing = bytes.get(index + 1) == Some(&b'/');
let name_start = index + if closing { 2 } else { 1 };
let first = *bytes.get(name_start)?;
if !first.is_ascii_alphabetic() {
return None;
}
let mut cursor = name_start + 1;
while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
cursor += 1;
}
let name = &input[name_start..cursor];
if closing {
cursor = skip_spaces(input, cursor);
if bytes.get(cursor) == Some(&b'>') {
return Some((cursor + 1, name));
}
return None;
}
let mut needs_space = false;
loop {
let before_spaces = cursor;
cursor = skip_spaces(input, cursor);
let had_space = cursor > before_spaces;
match bytes.get(cursor) {
Some(b'>') => return Some((cursor + 1, name)),
Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
Some(byte) if had_space && html_attribute_name_start(*byte) => {
cursor += 1;
while bytes
.get(cursor)
.is_some_and(|byte| html_attribute_name_byte(*byte))
{
cursor += 1;
}
let after_name = cursor;
let after_spaces = skip_spaces(input, cursor);
if bytes.get(after_spaces) == Some(&b'=') {
cursor = skip_spaces(input, after_spaces + 1);
cursor = parse_html_attribute_value(lookups, input, cursor)?;
} else {
cursor = after_name;
}
needs_space = true;
}
Some(_) if needs_space => return None,
_ => return None,
}
}
}
fn parse_html_attribute_value(
lookups: &mut impl Lookups,
input: &str,
index: usize,
) -> Option<usize> {
let bytes = input.as_bytes();
match bytes.get(index)? {
b'"' => Some(lookups.find("\"", index + 1)? + 1),
b'\'' => Some(lookups.find("'", index + 1)? + 1),
b'=' | b'<' | b'>' | b'`' => None,
_ => {
let mut cursor = index;
while bytes.get(cursor).is_some_and(|byte| {
!byte.is_ascii_whitespace()
&& !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
}) {
cursor += 1;
}
if cursor == index {
None
} else {
Some(cursor)
}
}
}
}
fn html_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'-'
}
fn html_attribute_name_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
}
fn html_attribute_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
}
fn skip_spaces(input: &str, mut index: usize) -> usize {
while input
.as_bytes()
.get(index)
.is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
{
index += 1;
}
index
}
fn is_autolink(input: &str) -> bool {
let inner = &input[1..input.len() - 1];
is_uri_autolink(inner) || is_email_autolink(inner)
}
fn is_uri_autolink(input: &str) -> bool {
let Some(colon) = input
.as_bytes()
.iter()
.take(33)
.position(|byte| *byte == b':')
else {
return false;
};
let scheme = &input[..colon];
if scheme.len() < 2 || scheme.len() > 32 {
return false;
}
let mut bytes = scheme.bytes();
if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
return false;
}
if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
return false;
}
input[colon + 1..]
.chars()
.map(source_char)
.all(|char| !matches!(char, '<' | '>') && !char.is_control() && !char.is_whitespace())
}
fn is_email_autolink(input: &str) -> bool {
if input
.chars()
.any(|char| char.is_whitespace() || char == '<')
{
return false;
}
let Some(at) = input.find('@') else {
return false;
};
if at == 0 || at + 1 >= input.len() {
return false;
}
is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
}
fn parse_literal_autolink(
input: &str,
index: usize,
gfm: bool,
relaxed: bool,
scan: &mut LiteralAutolinkScan,
) -> Option<(usize, String)> {
let rest = &input[index..];
if gfm {
if let Some(scheme_len) = rest
.starts_with("http://")
.then_some(7)
.or_else(|| rest.starts_with("https://").then_some(8))
{
if !literal_scheme_prefix_ok(input, index) {
return None;
}
let host = &input[index + scheme_len..];
if !http_literal_host_ok(host) {
if relaxed {
} else {
return None;
}
} else {
let end = autolink_url_end(input, index + scheme_len, index + scheme_len, relaxed);
if end <= index + scheme_len {
return None;
}
if literal_autolink_suppressed_by_link_label(
input,
index,
end,
relaxed,
gfm,
&mut scan.label_openers,
) {
return None;
}
return Some((end, input[index..end].into()));
}
}
if rest
.as_bytes()
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."))
{
if !literal_www_prefix_ok(input, index) {
return None;
}
check_domain(rest, false)?;
let end = autolink_url_end(input, index, index, relaxed);
if end <= index || (!relaxed && end <= index + 3 && !literal_starts_line(input, index))
{
return None;
}
if literal_autolink_suppressed_by_link_label(
input,
index,
end,
relaxed,
gfm,
&mut scan.label_openers,
) {
return None;
}
let mut destination = String::from("http://");
destination.push_str(&input[index..end]);
return Some((end, destination));
}
if let Some(email) = parse_literal_email(input, index, &mut scan.email_local) {
return Some(email);
}
}
if relaxed {
if literal_scheme_prefix_ok(input, index) {
if let Some(after_slashes) =
relaxed_scheme_after_slashes(input, index, &mut scan.scheme)
{
let body_start = index + after_slashes;
let next = input[body_start..].chars().next();
if next.is_none_or(|char| char.is_whitespace()) && after_slashes == 3 {
return None;
}
let end = autolink_url_end(input, body_start, body_start, true);
if end > index {
if literal_autolink_suppressed_by_link_label(
input,
index,
end,
relaxed,
gfm,
&mut scan.label_openers,
) {
return None;
}
return Some((end, input[index..end].into()));
}
}
}
}
None
}
fn relaxed_scheme_after_slashes(
input: &str,
index: usize,
scheme_run: &mut ByteRun,
) -> Option<usize> {
let bytes = input[index..].as_bytes();
if bytes.starts_with(b"://") {
return Some(3);
}
let first = bytes.first()?;
if !first.is_ascii_alphabetic() {
return None;
}
let i = scheme_run.end(input, index, is_relaxed_scheme_byte) - index;
if bytes.get(i..i + 3) == Some(b"://") {
Some(i + 3)
} else {
None
}
}
fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
if index == 0 {
return true;
}
let Some(previous) = input[..index].chars().next_back() else {
return true;
};
!previous.is_ascii_alphabetic()
}
fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
if index == 0 {
return true;
}
let Some(previous) = input[..index].chars().next_back() else {
return true;
};
if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
return true;
}
matches!(previous, ' ' | '\t' | '\n' | '\r')
}
fn literal_starts_line(input: &str, index: usize) -> bool {
index == 0
|| input
.as_bytes()
.get(index - 1)
.is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
}
fn literal_autolink_suppressed_by_link_label(
input: &str,
index: usize,
end: usize,
relaxed: bool,
gfm_autolink_literal: bool,
label_openers: &mut LabelOpenerScan,
) -> bool {
if !label_openers.has_unclosed_opener(input, index) {
return false;
}
if input[end..].starts_with("](") && !link_resource_tail_has_close(input, end + 2) {
return true;
}
!relaxed && !gfm_autolink_literal && input.as_bytes().get(end).is_some_and(|byte| *byte == b']')
}
#[derive(Default)]
struct LabelOpenerScan {
queried: usize,
cursor: usize,
depth: usize,
}
impl LabelOpenerScan {
fn has_unclosed_opener(&mut self, input: &str, index: usize) -> bool {
if index < self.queried {
*self = Self::default();
}
while self.cursor < index {
let Some((next, char)) = next_char(input, self.cursor) else {
break;
};
match char {
'\\' => {
self.cursor = match next_char(input, next) {
Some((_, '\n' | '\r')) | None => next,
Some((after_escape, _)) => after_escape,
};
continue;
}
'\n' | '\r' => self.depth = 0,
'[' => self.depth += 1,
']' => self.depth = self.depth.saturating_sub(1),
_ => {}
}
self.cursor = next;
}
self.queried = index;
self.depth > 0
}
}
fn link_resource_tail_has_close(input: &str, start: usize) -> bool {
let mut cursor = start;
while cursor < input.len() {
let Some((next, char)) = next_char(input, cursor) else {
break;
};
match char {
'\\' => {
cursor = next_char(input, next)
.map(|(after_escape, _)| after_escape)
.unwrap_or(next);
continue;
}
'\n' | '\r' => return false,
')' => return true,
_ => {}
}
cursor = next;
}
false
}
fn http_literal_host_ok(host: &str) -> bool {
if host.starts_with('[') {
return bracketed_ipv6_host_end(host).is_some();
}
match host.chars().next() {
Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => {
check_domain(host, true).is_some()
}
Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
check_domain(host, true).is_some()
}
_ => false,
}
}
fn bracketed_ipv6_host_end(host: &str) -> Option<usize> {
let close = host.find(']')?;
(close > 1).then_some(close + 1)
}
fn is_valid_hostchar(char: char) -> bool {
!char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
}
fn check_domain(data: &str, allow_short: bool) -> Option<usize> {
let mut np = 0usize;
let mut uscore1 = 0usize;
let mut uscore2 = 0usize;
let mut host_len = 0usize;
let mut chars = data.char_indices().peekable();
while let Some((offset, char)) = chars.next() {
let account = offset != 0 && chars.peek().is_some();
match char {
'\\' => {
host_len = offset + char.len_utf8();
if let Some((next_off, next)) = chars.next() {
host_len = next_off + next.len_utf8();
}
}
'_' if account => {
uscore2 += 1;
host_len = offset + char.len_utf8();
}
'.' if account => {
uscore1 = uscore2;
uscore2 = 0;
np += 1;
host_len = offset + char.len_utf8();
}
'_' | '.' | '-' => {
host_len = offset + char.len_utf8();
}
_ => {
if !is_valid_hostchar(source_char(char)) {
break;
}
host_len = offset + char.len_utf8();
}
}
}
if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
return None;
}
if allow_short || np > 0 {
Some(host_len)
} else {
None
}
}
fn autolink_url_end(input: &str, start: usize, trim_from: usize, balanced: bool) -> usize {
let bytes = input.as_bytes();
let mut end = start;
let mut bracket_depth = 0i32;
let mut curly_depth = 0i32;
let mut strict_has_open_bracket = false;
let mut strict_inside_backticks = false;
for (offset, char) in input[start..].char_indices() {
if char.is_whitespace() || char == '<' || is_autolink_terminating_control(char) {
break;
}
if balanced {
match char {
'[' => bracket_depth += 1,
']' => {
if bracket_depth > 0 {
bracket_depth -= 1;
} else {
break;
}
}
'{' => curly_depth += 1,
'}' => {
if curly_depth > 0 {
curly_depth -= 1;
} else {
break;
}
}
_ => {}
}
} else {
match char {
'[' => strict_has_open_bracket = true,
'`' => strict_inside_backticks = !strict_inside_backticks,
']' if !strict_has_open_bracket && !strict_inside_backticks => break,
_ => {}
}
}
if char == '\\' {
if let Some(&next) = bytes.get(start + offset + 1) {
let next_is_escapable_punct = next.is_ascii_punctuation() && next != b'.';
if next_is_escapable_punct {
break;
}
}
}
end = start + offset + char.len_utf8();
}
autolink_delim(input, trim_from, end)
}
fn is_autolink_terminating_control(char: char) -> bool {
matches!(char, '\u{2066}'..='\u{2069}')
}
fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
let bytes = input.as_bytes();
let mut opening = 0usize;
let mut closing = 0usize;
for &byte in &bytes[start..end] {
match byte {
b'(' => opening += 1,
b')' => closing += 1,
_ => {}
}
}
while end > start {
match bytes[end - 1] {
b')' => {
if closing <= opening {
break;
}
closing -= 1;
end -= 1;
}
b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
end -= 1;
}
b';' => {
if let Some(amp) = trailing_hex_entity_run_start(bytes, start, end) {
end = amp;
} else {
let mut new_end = end - 1;
while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
new_end -= 1;
}
if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
end = new_end - 1;
} else {
end -= 1;
}
}
}
_ => break,
}
}
end
}
fn trailing_hex_entity_run_start(bytes: &[u8], start: usize, end: usize) -> Option<usize> {
if end <= start || bytes[end - 1] != b';' {
return None;
}
let mut cursor = end - 1;
while cursor > start && bytes[cursor - 1].is_ascii_hexdigit() {
cursor -= 1;
}
if cursor == end - 1 || cursor < start + 3 {
return None;
}
let x = bytes[cursor - 1];
if (x == b'x' || x == b'X') && bytes[cursor - 2] == b'#' && bytes[cursor - 3] == b'&' {
Some(cursor - 3)
} else {
None
}
}
#[derive(Default)]
struct LiteralAutolinkScan {
email_local: ByteRun,
scheme: ByteRun,
label_openers: LabelOpenerScan,
}
#[derive(Default)]
struct ByteRun {
start: usize,
end: Option<usize>,
}
impl ByteRun {
fn end(&mut self, input: &str, index: usize, in_run: fn(u8) -> bool) -> usize {
if let Some(end) = self.end {
if self.start <= index && index <= end {
return end;
}
}
let end = input.as_bytes()[index..]
.iter()
.position(|byte| !in_run(*byte))
.map_or(input.len(), |offset| index + offset);
self.start = index;
self.end = Some(end);
end
}
}
fn is_email_local_or_scheme_byte(byte: u8) -> bool {
is_gfm_email_local_byte(byte) || byte == b':'
}
fn is_relaxed_scheme_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')
}
fn parse_literal_email(
input: &str,
index: usize,
local_run: &mut ByteRun,
) -> Option<(usize, String)> {
let rest = &input[index..];
let at_index = local_run.end(input, index, is_email_local_or_scheme_byte);
if input.as_bytes().get(at_index) != Some(&b'@') {
return None;
}
let at = at_index - index;
if at == 0 {
return None;
}
let local = &rest[..at];
let (auto_mailto, is_xmpp) = classify_email_local(local);
if !email_left_boundary_ok(input, index, auto_mailto) {
return None;
}
if !email_local_is_valid(local, auto_mailto) {
return None;
}
let domain_start = index + at + 1;
let domain_end = literal_email_domain_end(input, domain_start, is_xmpp)?;
let trimmed = autolink_delim(input, domain_start, domain_end);
if trimmed <= domain_start {
return None;
}
let domain = &input[domain_start..trimmed];
if !is_gfm_email_domain(domain, is_xmpp) {
return None;
}
let mut destination = String::new();
if auto_mailto {
destination.push_str("mailto:");
}
destination.push_str(&input[index..trimmed]);
Some((trimmed, destination))
}
fn classify_email_local(local: &str) -> (bool, bool) {
if let Some(rest) = strip_ci_prefix(local, "mailto:") {
if !rest.is_empty() {
return (false, false);
}
}
if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
if !rest.is_empty() {
return (false, true);
}
}
(true, false)
}
fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
let bytes = input.as_bytes();
let plen = prefix.len();
if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
Some(&input[plen..])
} else {
None
}
}
fn email_left_boundary_ok(input: &str, index: usize, auto_mailto: bool) -> bool {
if index == 0 {
return true;
}
let Some(previous) = input[..index].chars().next_back() else {
return true;
};
if previous.is_ascii_alphanumeric() {
if auto_mailto
&& input[index..].starts_with('+')
&& prefix_ends_with_gfm_email(input, index)
{
return true;
}
return false;
}
if auto_mailto && previous == '/' {
return false;
}
true
}
fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
let start = input[..end]
.rfind(char::is_whitespace)
.map_or(0, |offset| offset + 1);
let candidate = &input[start..end];
let Some(at) = candidate.rfind('@') else {
return false;
};
email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
}
fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
let body = if auto_mailto {
local
} else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
rest
} else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
rest
} else {
local
};
!body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
}
fn is_gfm_email_local_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
}
fn is_email_local_part(input: &str) -> bool {
!input.is_empty()
&& input
.split('.')
.all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
}
fn is_email_atext(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'/'
| b'='
| b'?'
| b'^'
| b'_'
| b'`'
| b'{'
| b'|'
| b'}'
| b'~'
| b'-'
)
}
fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
let bytes = input.as_bytes();
let mut end = index;
let mut np = 0usize;
while end < bytes.len() {
let byte = bytes[end];
if byte.is_ascii_alphanumeric() {
end += 1;
} else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
np += 1;
end += 1;
} else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
end += 1;
} else {
break;
}
}
if end <= index {
return None;
}
let len = end - index;
let last = bytes[end - 1];
if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
return None;
}
Some(end)
}
fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
if input.is_empty() {
return false;
}
let host = if is_xmpp {
input.split('/').next().unwrap_or(input)
} else {
input
};
if !host.contains('.') {
return false;
}
let last = host.as_bytes()[host.len() - 1];
if matches!(last, b'-' | b'_') {
return false;
}
host.split('.').all(|label| {
!label.is_empty()
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
})
}
fn is_email_domain(input: &str, min_labels: usize) -> bool {
let mut label_count = 0usize;
for label in input.split('.') {
label_count += 1;
let bytes = label.as_bytes();
if bytes.is_empty()
|| bytes.len() > 63
|| !bytes
.first()
.is_some_and(|byte| byte.is_ascii_alphanumeric())
|| !bytes
.last()
.is_some_and(|byte| byte.is_ascii_alphanumeric())
|| !bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
{
return false;
}
}
label_count >= min_labels
}
fn is_footnote_label(label: &str) -> bool {
!label.is_empty()
&& reference_label_is_within_limit(label)
&& !label.chars().any(char::is_whitespace)
}
fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
let close = find_footnote_reference_label_end(input, 2)?;
if input.as_bytes().get(close + 1) == Some(&b':') {
Some(close)
} else {
None
}
}
fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
while cursor < input.len() {
let (next, char) = next_char(input, cursor)?;
if char == ']' && !is_escaped_at(input, cursor) {
return Some(cursor);
}
cursor = next;
}
None
}