use alloc::{borrow::Cow, collections::BTreeMap, string::String, vec, vec::Vec};
use crate::{
ast::*,
decode::{
decode_escapes_and_references, decode_selected_escapes_and_references,
parse_character_reference,
},
diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
memo::{
bracket_walk, path_walk, pattern_starts, BracketMemo, BracketStep, PathMemo, Positions,
Step,
},
span::Span,
validate::is_directive_name,
};
mod blank_lines;
mod blocks;
mod nul_replacement;
#[cfg(test)]
mod scan_tests;
mod source_map;
mod tags;
mod tokens;
pub(crate) use blocks::interrupts_paragraph;
use source_map::{DerivedText, Segment, SourceMap};
pub(crate) const MAX_ORDERED_NUMBER: u64 = 999_999_999;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseOutput {
pub document: Document,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ParsedLinkResource {
destination: String,
destination_kind: LinkDestinationKind,
title: Option<Title>,
}
#[derive(Clone, Copy)]
struct LinkTailRanges {
destination: (usize, usize),
title: Option<(usize, usize)>,
}
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,
segments: &'a [Segment],
text_offset: usize,
column: usize,
}
impl<'a> Line<'a> {
fn detached(text: &'a str) -> Self {
Line {
text,
eol: "",
start: 0,
end: text.len(),
end_with_eol: text.len(),
segments: &[],
text_offset: 0,
column: 0,
}
}
fn column_at(&self, offset: usize) -> usize {
advance_columns(self.column, &self.text[..offset])
}
fn source_start(&self, offset: usize) -> usize {
source_map::start_of(self.segments, self.text_offset + offset)
}
fn source_end(&self, offset: usize) -> usize {
source_map::end_of(self.segments, self.text_offset + offset)
}
fn eol_source(&self) -> (usize, usize) {
(self.end, self.end_with_eol)
}
fn copy_into(&self, map: &mut SourceMap, at: usize, from: usize, to: usize) {
map.copy(
at,
self.segments,
self.text_offset + from,
self.text_offset + to,
);
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum HtmlBlockKind {
RawTag,
BlockTag,
Until(&'static str),
UntilBlank,
}
pub fn parse(input: &str) -> ParseOutput {
let mut diagnostics = Vec::new();
let start = if input.starts_with('\u{feff}') {
'\u{feff}'.len_utf8()
} else {
0
};
let source = &input[start..];
let map = SourceMap::verbatim(source.len(), start);
let lines = collect_lines(source, &map);
let mut children = blocks::parse_document(&lines, &mut diagnostics);
blank_lines::add_blank_lines(input, &mut children);
let mut document = Document::parsed(
NodeMeta::new(Some(Span::new(0, input.len()))),
children,
input,
);
if source.contains('\0') {
nul_replacement::replace_in_document(&mut document);
}
ParseOutput {
document,
diagnostics,
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Definitions {
identifiers: Vec<String>,
}
impl Definitions {
pub fn of(document: &Document) -> Self {
fn walk(blocks: &[Block], identifiers: &mut Vec<String>) {
for block in blocks {
match block {
Block::Definition(node) => identifiers.push(node.identifier.clone()),
Block::BlockQuote(node) => walk(&node.children, identifiers),
Block::Alert(node) => walk(&node.children, identifiers),
Block::List(node) => {
for item in &node.children {
walk(&item.children, identifiers);
}
}
Block::FootnoteDefinition(node) => walk(&node.children, identifiers),
Block::HtmlContainer(node) => {
if let HtmlContainerContent::Blocks(children) = &node.content {
walk(children, identifiers);
}
}
Block::ContainerDirective(node) => walk(&node.children, identifiers),
_ => {}
}
}
}
let mut identifiers = Vec::new();
walk(&document.children, &mut identifiers);
Self::sorted(identifiers)
}
pub fn new<I, S>(labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::sorted(
labels
.into_iter()
.map(|label| identifier(label.as_ref()))
.collect(),
)
}
fn sorted(mut identifiers: Vec<String>) -> Self {
identifiers.sort_unstable();
identifiers.dedup();
Self { identifiers }
}
fn labels(&self) -> Labels<'_> {
Labels {
own: &self.identifiers,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct InlineOutput {
pub inlines: Vec<Inline>,
pub tokens: Vec<InlineToken>,
pub diagnostics: Vec<Diagnostic>,
}
impl InlineOutput {
pub fn nodes(&self) -> Vec<&Inline> {
let mut nodes = Vec::new();
let mut stack: Vec<&Inline> = self.inlines.iter().rev().collect();
while let Some(inline) = stack.pop() {
nodes.push(inline);
stack.extend(inline.children().iter().rev());
}
nodes
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InlineToken {
pub span: Span,
pub node: usize,
pub role: TokenRole,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum TokenRole {
Text,
Syntax,
Destination,
Title,
Label,
Target,
Name,
Attributes,
}
pub fn parse_inline(text: &str, definitions: &Definitions) -> InlineOutput {
read_inline(text, definitions, true)
}
pub(crate) fn parse_inline_tree(text: &str, definitions: &Definitions) -> InlineOutput {
read_inline(text, definitions, false)
}
fn read_inline(text: &str, definitions: &Definitions, with_tokens: bool) -> InlineOutput {
let mut diagnostics = Vec::new();
let (content, map) = if text.contains('\r') {
let mut content = String::with_capacity(text.len());
let mut map = SourceMap::default();
for line in collect_lines(text, &SourceMap::verbatim(text.len(), 0)) {
map.push(content.len(), line.text.len(), line.start, line.end);
content.push_str(line.text);
if !line.eol.is_empty() {
map.push(content.len(), 1, line.end, line.end_with_eol);
content.push('\n');
}
}
(Cow::Owned(content), map)
} else {
(Cow::Borrowed(text), SourceMap::verbatim(text.len(), 0))
};
let mut tokens = Vec::new();
let mut inlines = parse_inlines(
&content,
&map,
Some(definitions.labels()),
&mut diagnostics,
with_tokens.then_some(&mut tokens),
);
if text.contains('\0') {
nul_replacement::replace_in_inlines(&mut inlines);
}
diagnostics.sort_by_key(|diagnostic| diagnostic.span.map(|span| span.start));
InlineOutput {
inlines,
tokens,
diagnostics,
}
}
#[derive(Clone, Debug)]
pub struct InlineSource {
pub text: String,
map: SourceMap,
}
impl InlineSource {
pub fn to_source(&self, offset: usize) -> usize {
self.span_to_source(Span::new(offset, offset)).start
}
pub fn span_to_source(&self, span: Span) -> Span {
source_map::translate_span(&self.map, span)
}
}
#[doc(hidden)]
pub fn inline_spans_to_source(source: &InlineSource, inlines: &mut [Inline]) {
source_map::translate_inlines(&source.map, inlines, &mut []);
}
impl Document {
pub fn inline_source(&self, path: &NodePath) -> Option<InlineSource> {
let source = self.source()?;
let (&first, rest) = path.0.split_first()?;
let top = self.children.get(first)?;
let mut block = top;
let mut rest = rest.iter();
let leaf = loop {
let Some(&index) = rest.next() else {
break match block {
Block::Paragraph(node) => node.meta.span?,
Block::Heading(node) => node.meta.span?,
Block::LeafDirective(node) => node.meta.span?,
Block::ContainerDirective(node) => node.meta.span?,
_ => return None,
};
};
block = match block {
Block::BlockQuote(node) => node.children.get(index)?,
Block::Alert(node) => node.children.get(index)?,
Block::FootnoteDefinition(node) => node.children.get(index)?,
Block::ContainerDirective(node) => node.children.get(index)?,
Block::HtmlContainer(node) => match &node.content {
HtmlContainerContent::Blocks(children) => children.get(index)?,
_ => return None,
},
Block::List(node) => {
let item = node.children.get(index)?;
item.children.get(*rest.next()?)?
}
Block::Table(node) => {
let cell = node.rows.get(index)?.cells.get(*rest.next()?)?;
if rest.next().is_some() {
return None;
}
break cell.meta.span?;
}
_ => return None,
};
};
let end = top.span()?.end;
let opening = self.children[..first]
.iter()
.rev()
.take_while(|block| matches!(block, Block::Definition(_)))
.last()
.unwrap_or(top);
let start = opening.span()?.start;
if end > source.len() || start > end || !source.is_char_boundary(start) {
return None;
}
let bytes = source.as_bytes();
let mut start = start;
while start > 0 && !matches!(bytes[start - 1], b'\n' | b'\r') {
start -= 1;
}
if start == 0 && source.starts_with('\u{feff}') {
start = '\u{feff}'.len_utf8().min(end);
}
let slice = source.get(start..end)?;
let map = SourceMap::verbatim(slice.len(), start);
let lines = collect_lines(slice, &map);
let (_, text) = blocks::leaf_contents(&lines)
.into_iter()
.find(|(span, _)| *span == leaf)?;
let (text, map) = text.into_parts();
Some(InlineSource { text, map })
}
pub fn leaf_sources(&self) -> Vec<(NodePath, InlineSource)> {
let mut found = Vec::new();
self.each_leaf_source(|path, _, source| found.push((NodePath(path.to_vec()), source)));
found
}
pub(crate) fn each_leaf_source(&self, mut found: impl FnMut(&[usize], Span, InlineSource)) {
let Some(source) = self.source() else {
return;
};
let start = if source.starts_with('\u{feff}') {
'\u{feff}'.len_utf8()
} else {
0
};
let map = SourceMap::verbatim(source.len() - start, start);
let lines = collect_lines(&source[start..], &map);
let mut contents: Vec<(Span, Option<DerivedText>)> = blocks::leaf_contents(&lines)
.into_iter()
.map(|(span, text)| (span, Some(text)))
.collect();
let mut take = |path: &[usize], span: Option<Span>, _: Option<&str>| {
let Some(span) = span else { return };
let first = contents.partition_point(|(other, _)| other.start < span.start);
let entry = contents[first..]
.iter_mut()
.take_while(|(other, _)| other.start == span.start)
.find(|(other, text)| *other == span && text.is_some());
if let Some((_, text)) = entry {
let (text, map) = text.take().expect("found unused").into_parts();
found(path, span, InlineSource { text, map });
}
};
crate::leaves::visit(&self.children, &mut Vec::new(), &mut take);
}
}
fn source_char(char: char) -> char {
if char == '\0' {
'\u{FFFD}'
} else {
char
}
}
pub(crate) const MAX_BLOCK_NESTING: usize = 32;
fn advance_columns(column: usize, text: &str) -> usize {
text.chars().fold(column, |column, char| {
if char == '\t' {
column + 4 - column % 4
} else {
column + 1
}
})
}
fn collect_lines<'a>(input: &'a str, map: &'a SourceMap) -> Vec<Line<'a>> {
let bytes = input.as_bytes();
let mut lines = Vec::new();
let mut segments = LineSegments {
segments: map.segments(),
first: 0,
};
let mut start = 0;
let mut index = 0;
while index < bytes.len() {
let eol_end = match bytes[index] {
b'\n' => index + 1,
b'\r' if bytes.get(index + 1) == Some(&b'\n') => index + 2,
b'\r' => index + 1,
_ => {
index += 1;
continue;
}
};
lines.push(segments.line(input, start, index, eol_end));
index = eol_end;
start = index;
}
if start < bytes.len() || input.is_empty() {
lines.push(segments.line(input, start, bytes.len(), bytes.len()));
}
lines
}
struct LineSegments<'a> {
segments: &'a [Segment],
first: usize,
}
impl<'a> LineSegments<'a> {
fn line(&mut self, input: &'a str, start: usize, end: usize, eol_end: usize) -> Line<'a> {
let segments = self.segments;
while self.first + 1 < segments.len() && segments[self.first].text_end() <= start {
self.first += 1;
}
let mut last = self.first;
while last + 1 < segments.len() && segments[last + 1].text_start() < eol_end {
last += 1;
}
let covering = if segments.is_empty() {
segments
} else {
&segments[self.first..=last]
};
let source_start = source_map::start_of(covering, start);
let source_end = if end == start {
source_start
} else {
source_map::end_of(covering, end)
};
let source_end_with_eol = if eol_end == end {
source_end
} else {
source_map::end_of(covering, eol_end)
};
Line {
text: &input[start..end],
eol: &input[end..eol_end],
start: source_start,
end: source_end,
end_with_eol: source_end_with_eol,
segments: covering,
text_offset: start,
column: 0,
}
}
}
pub(crate) fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
match line.trim_end_matches([' ', '\t']) {
"---" => Some(FrontmatterKind::Yaml),
"+++" => Some(FrontmatterKind::Toml),
_ => None,
}
}
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 && is_blank(&input[fence_len..]) {
Some(fence_len)
} else {
None
}
}
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 && is_blank(&input[count..])
}
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,
};
Some((kind, alert_title(&line[close + 1..])))
}
pub(crate) fn alert_title(text: &str) -> Option<String> {
let title = text.trim_matches([' ', '\t']);
(!title.is_empty()).then(|| title.into())
}
fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
let text = trim_up_to_three_spaces(line.text)?;
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 definition_label(lines: &[Line<'_>], index: usize) -> Option<(String, usize, usize)> {
let text = trim_ascii_start(lines[index].text);
if !text.starts_with('[') {
return None;
}
let mut accumulated = String::from(text);
let mut label_end_line = index;
let mut cursor = 1;
loop {
let bytes = accumulated.as_bytes();
if cursor >= bytes.len() {
let next = label_end_line + 1;
if next >= lines.len() || accumulated.len() > 4 * REFERENCE_LABEL_MAX_CHARS + 2 {
return None;
}
accumulated.push('\n');
accumulated.push_str(lines[next].text);
label_end_line = next;
continue;
}
match reference_label_step(&accumulated, cursor) {
Step::Next(next) => cursor = next,
Step::Done(Some(close)) if reference_label_is_within_limit(&accumulated[1..close]) => {
if bytes.get(close + 1) != Some(&b':') {
return None;
}
return Some((accumulated, close, label_end_line));
}
Step::Done(_) => return None,
}
}
}
fn parse_definition(lines: &[Line<'_>], index: usize) -> Option<(Definition, usize)> {
let (accumulated, close, label_end_line) = definition_label(lines, index)?;
let label = String::from(&accumulated[1..close]);
if normalize_label(&label).is_empty() {
return None;
}
let mut source = String::from(&accumulated[close + 2..]);
let mut cursor = label_end_line;
let mut best_without_title = None;
let mut open_title: Option<char> = None;
loop {
let parses =
open_title.is_none_or(|closer| line_may_close_title(lines[cursor].text, closer));
if parses {
match parse_definition_destination_title(&source) {
Some(resource) => {
if resource.title.is_some() {
return Some((definition_node(&label, resource), cursor + 1));
}
best_without_title = Some((resource, cursor + 1));
let next = cursor + 1;
if next >= lines.len() || !line_can_start_definition_title(lines[next].text) {
break;
}
}
None if !is_blank(&source) => match open_definition_title(&source) {
Some(closer) if open_title.is_none() => open_title = Some(closer),
_ => break,
},
None => {}
}
}
let next = cursor + 1;
if next >= lines.len() {
break;
}
source.push('\n');
source.push_str(lines[next].text);
cursor = next;
}
let (resource, next) = best_without_title?;
Some((definition_node(&label, resource), next))
}
fn definition_node(label: &str, resource: ParsedLinkResource) -> Definition {
Definition {
meta: NodeMeta::default(),
label: label.into(),
identifier: normalize_label(label),
destination: resource.destination,
destination_kind: resource.destination_kind,
title: resource.title,
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum HtmlContainerTag {
Opening,
Closing,
}
#[derive(Clone, Copy)]
struct HtmlContainerFence {
marker: FenceMarker,
length: usize,
}
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) {
let after_fence = (cursor + 1..lines.len())
.find(|close| html_container_fence_closes(&lines[*close], 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)?;
let (end, name) = parse_html_tag(trimmed, 0)?;
if !name.eq_ignore_ascii_case(tag) || !is_blank(&trimmed[end..]) {
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;
}
Some(HtmlTag {
meta: NodeMeta::new(Some(Span::new(
line.source_start(indent_bytes),
line.source_end(indent_bytes + 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)?;
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;
}
Some(HtmlTag {
meta: NodeMeta::new(Some(Span::new(
line.source_start(indent_bytes),
line.source_end(indent_bytes + 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)
&& is_blank(&input[close_end..])
}
fn trim_html_container_line<'a>(line: &Line<'a>) -> Option<(&'a str, usize)> {
let (columns, bytes) = leading_indent_at(line.text, line.column);
(columns <= 3).then(|| (&line.text[bytes..], bytes))
}
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_matches([' ', '\t']).ends_with("/>")
}
fn html_container_fence_opens(line: &Line<'_>) -> Option<HtmlContainerFence> {
let (trimmed, _) = trim_html_container_line(line)?;
let (marker, length) = fence_start(trimmed)?;
Some(HtmlContainerFence { marker, length })
}
fn html_container_fence_closes(line: &Line<'_>, fence: HtmlContainerFence) -> bool {
trim_html_container_line(line)
.is_some_and(|(trimmed, _)| fence_close(trimmed, fence.marker, fence.length))
}
fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
let trimmed = input.trim_end_matches([' ', '\t']);
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;
};
if is_blank(&trimmed[end..]) {
Some(HtmlBlockKind::UntilBlank)
} else {
None
}
}
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 setext_underline_depth(input: &str) -> Option<u8> {
let underline = trim_up_to_three_spaces(input)?.trim_matches([' ', '\t']);
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,
}
}
#[derive(Clone, Copy)]
struct DelimMarker {
node_index: usize,
marker: u8,
length: usize,
run_length: usize,
can_open: bool,
can_close: bool,
recloses: usize,
enclosed_from: usize,
position: usize,
}
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
}
}
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,
definitions: Option<Labels<'_>>,
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, nodes, delimiters, brackets, &opener, depth);
nodes.truncate(opener.node_index - 1);
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);
}
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,
definitions,
);
if let Some((end, target)) = target {
let (children, marks) = take_label(input, 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);
}
opener.kind = BracketKind::Link;
opener.position += 1;
}
if link_active {
let target = match_link_target(
&mut pass.scan,
input,
opener.position + 1,
close,
definitions,
);
if let Some((end, target)) = target {
let (children, marks) = take_label(input, 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 !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);
}
fn push_delimiter(
nodes: &mut Vec<Inline>,
delimiters: &mut Vec<DelimMarker>,
index: usize,
base_offset: usize,
marker: u8,
length: usize,
) {
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,
run_length: length,
can_open: false,
can_close: false,
recloses: 0,
enclosed_from: NIL,
position: index,
});
}
fn emphasis_roles(
input: &str,
index: usize,
length: usize,
marker: u8,
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_punctuation),
flanking.right && (!flanking.left || flanking.next_punctuation),
)
} else {
(flanking.left, flanking.right)
};
if 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]) {
let mut opens_span = alloc::vec![false; delimiters.len()];
let mut closes_span = alloc::vec![false; delimiters.len()];
let mut open_marks: Vec<usize> = Vec::new();
let mut span_close = alloc::vec![NIL; delimiters.len()];
for (index, run) in delimiters.iter_mut().enumerate() {
if run.marker != b'=' {
continue;
}
assign_double_mark_roles(input, run);
let mut closed = 0;
if run.can_close {
while closed <= run.recloses {
let Some(opener) = open_marks.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_marks.push(index);
}
}
for index in 0..delimiters.len() {
let run = delimiters[index];
if run.marker == b'=' {
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,
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 assign_double_mark_roles(input: &str, run: &mut DelimMarker) {
let (index, length) = (run.position, run.length);
let after_escaped_mark =
index > 0 && input.as_bytes()[index - 1] == run.marker && is_escaped_at(input, index - 1);
run.can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
run.recloses = if run.can_close {
(1..length / 2)
.take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
.count()
} else {
0
};
run.can_open = can_open_delimited(input, index + length - 2, 2);
}
fn process_emphasis(
nodes: Vec<Inline>,
mut delimiters: Vec<DelimMarker>,
depth: usize,
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 closer_idx = 0;
while closer_idx < delimiters.len() {
let closer = delimiters[closer_idx];
let opener = if closer.can_close {
nested_opener(
&delimiters,
&links,
&openers_bottom,
&span_bottom,
closer_idx,
)
} else {
None
};
let Some(opener_idx) = opener else {
if closer.can_close {
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 !closer.can_open {
links.unlink(closer_idx);
}
closer_idx += 1;
continue;
};
let (used, wrap) = pair_shape(&delimiters[opener_idx], &delimiters[closer_idx]);
let inner = links.close_span(opener_idx, closer_idx);
let fits = 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 closer.marker == b'=' {
delimiters[opener_idx].can_open = false;
let run = &mut delimiters[closer_idx];
run.can_close = run.recloses > 0;
run.recloses = run.recloses.saturating_sub(1);
}
if delimiters[opener_idx].length == 0 {
links.unlink(opener_idx);
}
if delimiters[closer_idx].length == 0 {
links.unlink(closer_idx);
closer_idx += 1;
}
}
let mut nodes = nodes.into_vec();
merge_adjacent_text(&mut nodes);
(nodes, deepest)
}
const NESTED_MARKERS: usize = 4;
fn nested_opener(
delimiters: &[DelimMarker],
links: &DelimiterLinks,
openers_bottom: &[Option<usize>],
span_bottom: &[Vec<(usize, usize)>],
closer_idx: usize,
) -> Option<usize> {
let closer = &delimiters[closer_idx];
let key = openers_bottom_key(closer);
let mut bottom = openers_bottom[key];
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 pair_shape(opener: &DelimMarker, closer: &DelimMarker) -> (usize, EmphasisWrap) {
match closer.marker {
b'~' => (2, EmphasisWrap::Delete),
b'=' => (2, EmphasisWrap::Mark),
marker => {
let delimiter = if marker == b'_' {
EmphasisDelimiter::Underscore
} else {
EmphasisDelimiter::Asterisk
};
if opener.length >= 2 && closer.length >= 2 {
(2, EmphasisWrap::Strong(delimiter))
} else {
(1, EmphasisWrap::Emphasis(delimiter))
}
}
}
}
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::Mark(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,
_ => 0,
};
let both = usize::from(closer.can_open && closer.can_close);
let length = match closer.marker {
b'*' | b'_' => closer.run_length,
_ => closer.length,
};
let modulo = length % 3;
((marker * 2) + both) * 3 + modulo
}
fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
match opener.marker {
b'~' => true,
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 (opener_run, closer_run) = (opener.run_length, closer.run_length);
let sum = opener_run + closer_run;
if sum % 3 == 0 && !(opener_run % 3 == 0 && closer_run % 3 == 0) {
return false;
}
}
true
}
}
}
#[derive(Clone, Copy)]
enum EmphasisWrap {
Emphasis(EmphasisDelimiter),
Strong(EmphasisDelimiter),
Delete,
Mark,
}
impl EmphasisWrap {
fn is_mark(self) -> bool {
matches!(self, Self::Mark)
}
}
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(delimiter) => Inline::Strong(Strong {
meta,
delimiter,
children,
}),
EmphasisWrap::Emphasis(delimiter) => Inline::Emphasis(Emphasis {
meta,
delimiter,
children,
}),
EmphasisWrap::Delete => Inline::Delete(Delete { meta, children }),
EmphasisWrap::Mark => Inline::Mark(Mark { 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,
}
enum LinkTarget {
Resource(ParsedLinkResource),
Reference {
identifier: String,
kind: ReferenceKind,
},
}
fn match_link_target(
scan: &mut InlineScan,
input: &str,
label_start: usize,
close: usize,
definitions: Option<Labels<'_>>,
) -> 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 => {}
}
}
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,
children,
}),
(LinkTarget::Resource(resource), true) => Inline::Image(Image {
meta,
destination: resource.destination,
destination_kind: resource.destination_kind,
title: resource.title,
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 autolink_node(form: AutolinkForm, span: Span, text: &str) -> Inline {
Inline::Autolink(Autolink {
meta: NodeMeta::new(Some(span)),
form,
text: text.into(),
})
}
fn demote_links(nodes: &mut Vec<Inline>) {
for node in nodes.iter_mut() {
match node {
Inline::Autolink(autolink) => {
*node = Inline::Text(Text {
meta: autolink.meta.clone(),
value: core::mem::take(&mut autolink.text),
});
}
Inline::Emphasis(node) => demote_links(&mut node.children),
Inline::Strong(node) => demote_links(&mut node.children),
Inline::Delete(node) => demote_links(&mut node.children),
Inline::Mark(node) => demote_links(&mut node.children),
Inline::InlineFootnote(node) => demote_links(&mut node.children),
Inline::TextDirective(node) => demote_links(&mut node.label),
Inline::Image(node) => demote_links(&mut node.alt),
Inline::ImageReference(node) => demote_links(&mut node.alt),
_ => {}
}
}
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 open(&self) -> bool {
self.overflow > 0
|| self
.openers
.iter()
.any(|opener| !matches!(opener.kind, BracketKind::InlineFootnote))
}
fn close_links(&mut self) {
self.link_floor = self.openers.len();
}
}
fn take_label(
input: &str,
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);
process_emphasis(children, runs, 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>;
fn unescaped_dollar(&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))
}
fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
(from..self.input.len()).find(|index| is_unescaped_dollar(self.input, *index))
}
}
const CACHED_PATTERNS: [&str; 8] = [">", "-->", "?>", "]]>", "\"", "'", "`$", "$$"];
struct InlineLookups<'a> {
input: &'a str,
patterns: [Positions; CACHED_PATTERNS.len()],
backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
angle_destination_stops: Positions,
unescaped_dollars: Positions,
}
impl<'a> InlineLookups<'a> {
fn new(input: &'a str) -> Self {
Self {
input,
patterns: Default::default(),
backtick_runs: None,
angle_destination_stops: Positions::default(),
unescaped_dollars: 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 unescaped_dollar(&mut self, from: usize) -> Option<usize> {
let input = self.input;
self.unescaped_dollars.first_at_or_after(from, || {
(0..input.len())
.filter(|index| is_unescaped_dollar(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_unescaped_dollar(input: &str, index: usize) -> bool {
let bytes = input.as_bytes();
bytes[index] == b'$' && (index == 0 || bytes[index - 1] != b'\\')
}
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,
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(),
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 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,
map: &SourceMap,
definitions: Option<Labels<'_>>,
diagnostics: &mut Vec<Diagnostic>,
tokens: Option<&mut Vec<InlineToken>>,
) -> Vec<Inline> {
if definitions.is_none() {
return Vec::new();
}
let first_diagnostic = diagnostics.len();
let mut nodes = parse_inlines_with_context(
input,
0,
definitions,
diagnostics,
&mut InlineState::default(),
);
tags::split_tags(input, &mut nodes);
if let Some(tokens) = tokens {
*tokens = tokens::tokens(input, &nodes);
source_map::translate_tokens(map, tokens);
}
source_map::translate_inlines(map, &mut nodes, &mut diagnostics[first_diagnostic..]);
nodes
}
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,
definitions: Option<Labels<'_>>,
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, definitions, diagnostics, state);
state.depth -= 1;
nodes
}
fn parse_inline_content(
input: &str,
base_offset: usize,
definitions: Option<Labels<'_>>,
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 brackets = Brackets::new();
let mut pass = InlinePass {
state,
scan: InlineScan::new(input),
};
let literal_autolinks = bytes.contains(&b'@')
|| input.contains("://")
|| bytes
.windows(4)
.any(|window| window.eq_ignore_ascii_case(b"www."));
while index < bytes.len() {
if bytes[index] == b'\\' {
if let Some((next_index, char)) = next_char(input, index + 1) {
if char.is_ascii_punctuation() {
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 bytes[index] == b'&' {
if let Some((end, _)) = parse_character_reference(input, index) {
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(),
}));
index = end;
text_start = index;
continue;
}
}
if bytes[index] == b'\n' {
if index > 0
&& bytes[index - 1] == b'\\'
&& !is_escaped_at(input, index - 1)
&& 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;
}
let stripped_alone = trailing_spaces > 0 && text.len() == trailing_spaces;
if trailing_spaces > 0 {
text.truncate(text.len() - trailing_spaces);
}
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
let start = if stripped_alone {
index - trailing_spaces
} else {
index
};
nodes.push(Inline::SoftBreak(SoftBreak {
meta: NodeMeta::new(Some(Span::new(
base_offset + start,
base_offset + index + 1,
))),
}));
index += 1;
text_start = index;
continue;
}
if bytes[index] == b'`' {
if let Some((end, value)) = 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,
}));
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 bytes[index] == b'*' && starts_delimiter_run(input, index, b'*') {
let run_len = delimiter_byte_run_len(input, index, b'*');
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
push_delimiter(
&mut nodes,
&mut delimiters,
index,
base_offset,
b'*',
run_len,
);
index += run_len;
text_start = index;
continue;
}
if bytes[index] == b'_' && starts_delimiter_run(input, index, b'_') {
if literal_autolinks {
if let Some((end, _)) = parse_literal_autolink(
input,
index,
&mut pass.scan.literal_autolinks,
!brackets.open(),
) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(autolink_node(
AutolinkForm::Literal,
Span::new(base_offset + index, base_offset + end),
&input[index..end],
));
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);
push_delimiter(
&mut nodes,
&mut delimiters,
index,
base_offset,
b'_',
run_len,
);
index += run_len;
text_start = index;
continue;
}
if 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);
push_delimiter(
&mut nodes,
&mut delimiters,
index,
base_offset,
b'=',
run_len,
);
index += run_len;
text_start = index;
continue;
}
}
let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
if bracket_room && bytes[index] == b'^' && bytes.get(index + 1) == Some(&b'[') {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
push_text(&mut nodes, base_offset + index, "^");
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(),
});
index += 2;
text_start = index;
continue;
}
if bytes[index] == b'~' && starts_delimiter_run(input, 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);
push_delimiter(
&mut nodes,
&mut delimiters,
index,
base_offset,
b'~',
run_len,
);
index += run_len;
text_start = index;
continue;
}
}
if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') {
if let Some((end, wikilink)) =
parse_wikilink(input, index + 1, base_offset, &mut pass.scan)
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(embed(wikilink, base_offset + index));
brackets.close_links();
index = end;
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);
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(),
});
index += 2;
text_start = index;
continue;
}
if bytes[index] == b'[' {
if let Some((end, wikilink)) = parse_wikilink(input, index, base_offset, &mut pass.scan)
{
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(wikilink);
brackets.close_links();
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(),
});
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,
definitions,
&mut nodes,
&mut delimiters,
&mut brackets,
&mut pass,
) {
index = end;
text_start = index;
continue;
}
}
if bytes[index] == b'$' {
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 literal_autolinks {
if let Some((end, _)) = parse_literal_autolink(
input,
index,
&mut pass.scan.literal_autolinks,
!brackets.open(),
) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(autolink_node(
AutolinkForm::Literal,
Span::new(base_offset + index, base_offset + end),
&input[index..end],
));
index = end;
text_start = index;
continue;
}
}
if bytes[index] == b'<' {
if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
let uri = &input[index + 1..end - 1];
if is_angle_autolink(uri) {
flush_text(&mut nodes, &mut text, text_start, base_offset + index);
nodes.push(autolink_node(
AutolinkForm::Angle,
Span::new(base_offset + index, base_offset + end),
uri,
));
index = end;
text_start = index;
continue;
}
}
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':' {
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':' && pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING {
pass.state.depth += brackets.openers.len();
let directive = parse_text_directive(
input,
index,
base_offset,
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());
assign_emphasis_roles(input, &mut delimiters);
process_emphasis(nodes, delimiters, pass.state.depth - 1, &brackets.formed).0
}
fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
if input[index..].starts_with("::") {
return None;
}
if input[..index]
.chars()
.next_back()
.is_some_and(char::is_alphanumeric)
{
return None;
}
let mut cursor = index + 1;
while let Some((next, char)) = next_char(input, cursor) {
if char == ':' {
let name = &input[index + 1..cursor];
if crate::gemoji::glyph(name).is_none()
|| input[next..]
.chars()
.next()
.is_some_and(char::is_alphanumeric)
{
return None;
}
return Some((next, name.into()));
}
if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
return None;
}
cursor = next;
}
None
}
fn parse_wikilink(
input: &str,
index: usize,
base_offset: usize,
scan: &mut InlineScan,
) -> Option<(usize, Inline)> {
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) = match find_wikilink_separator(source) {
Some(separator) => (&source[..separator], &source[separator + 1..]),
None => (source, source),
};
if target_source.is_empty() {
return None;
}
let target = String::from(target_source);
let label = String::from(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,
embed: false,
}),
))
}
fn embed(mut wikilink: Inline, bang: usize) -> Inline {
if let Inline::WikiLink(node) = &mut wikilink {
node.embed = true;
node.meta.span = node.meta.span.map(|span| Span::new(bang, span.end));
}
wikilink
}
fn wikilink_close_step(input: &str, cursor: usize) -> Step {
let bytes = input.as_bytes();
match bytes.get(cursor) {
None | Some(b'\n' | b'\r' | b'[') => 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(b']') => Step::Done(None),
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' ')
}
#[cfg(test)]
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::Delete(_)
| Inline::Mark(_)
| 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(_) | Inline::WikiLink(_)
) || 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,
definitions: Option<Labels<'_>>,
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_offset = index + 1;
let opener = parse_directive_opener_with(
&input[opener_offset..],
|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)
},
|label, attributes, rest| text_directive_ends(label, attributes, rest.bytes().next()),
);
let opener = match opener {
Ok(opener) => opener,
Err(refused) => {
if refused == Refused::Unclosed {
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 = opener
.label
.map(|source| {
parse_inlines_with_context(
source,
base_offset + index + 1 + opener.name.len() + 1,
definitions,
diagnostics,
pass.state,
)
})
.unwrap_or_default();
let at = base_offset + opener_offset;
opener.report_dropped(diagnostics, |start, end| Span::new(at + start, at + end));
let DirectiveOpener {
name,
attributes,
consumed,
..
} = opener;
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 text_directive_ends(label: Option<&str>, attributes: Option<&str>, next: Option<u8>) -> bool {
match next {
None | Some(b' ' | b'\t' | b'\n' | b'\r') => true,
Some(byte) if byte.is_ascii_punctuation() => {
label.is_some_and(|label| !label.is_empty())
|| attributes
.is_some_and(|source| source.bytes().any(|byte| !byte.is_ascii_whitespace()))
}
Some(_) => false,
}
}
struct DirectiveOpener<'a> {
name: String,
label: Option<&'a str>,
attributes: Vec<DirectiveAttribute>,
dropped: Vec<(usize, usize)>,
consumed: usize,
}
impl DirectiveOpener<'_> {
fn report_dropped(
&self,
diagnostics: &mut Vec<Diagnostic>,
span: impl Fn(usize, usize) -> Span,
) {
for &(start, end) in &self.dropped {
diagnostics.push(Diagnostic::new(
DiagnosticSeverity::Warning,
DiagnosticCode::InvalidDirectiveAttribute,
span(start, end),
"directive attribute without a valid name is dropped",
));
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Refused {
Name,
Unclosed,
Follow,
}
fn parse_directive_opener<'a>(
input: &'a str,
accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
) -> Result<DirectiveOpener<'a>, Refused> {
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),
},
accept,
)
}
#[derive(Clone, Copy)]
enum DirectiveClose {
Label,
Attributes,
}
fn parse_directive_opener_with<'a>(
input: &'a str,
mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
) -> Result<DirectiveOpener<'a>, Refused> {
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 Err(Refused::Name);
}
let mut label = None;
let mut attribute_source = None;
let mut consumed = index;
if input.as_bytes().get(consumed) == Some(&b'[') {
let close = find_close(DirectiveClose::Label, consumed).ok_or(Refused::Unclosed)?;
label = Some(&input[consumed + 1..close]);
consumed = close + 1;
}
if input.as_bytes().get(consumed) == Some(&b'{') {
let close = find_close(DirectiveClose::Attributes, consumed).ok_or(Refused::Unclosed)?;
attribute_source = Some((consumed + 1, &input[consumed + 1..close]));
consumed = close + 1;
}
if !accept(
label,
attribute_source.map(|(_, source)| source),
&input[consumed..],
) {
return Err(Refused::Follow);
}
let (attributes, dropped) = match attribute_source {
Some((start, source)) => {
let (attributes, mut dropped) = parse_attributes(source);
for range in &mut dropped {
*range = (range.0 + start, range.1 + start);
}
(attributes, dropped)
}
None => (Vec::new(), Vec::new()),
};
Ok(DirectiveOpener {
name: name.into(),
label,
attributes,
dropped,
consumed,
})
}
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>, Vec<(usize, usize)>) {
let mut attributes = Vec::new();
let mut dropped = 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 token_start = cursor;
let (name, next) = parse_attribute_name(input, cursor);
if name.is_empty() {
dropped.push((token_start, input.trim_end().len().max(token_start)));
break;
}
let mut token_end = next;
cursor = skip_spaces(input, next);
let value = if input.as_bytes().get(cursor) == Some(&b'=') {
cursor = skip_spaces(input, cursor + 1);
token_end = cursor;
if let Some((value, next)) = parse_attribute_value(input, cursor) {
cursor = next;
token_end = next;
Some(value)
} else {
Some(String::new())
}
} else {
None
};
if crate::validate::is_attribute_name(name) {
attributes.push(DirectiveAttribute {
name: name.into(),
value,
});
} else {
dropped.push((token_start, token_end));
}
}
(attributes, dropped)
}
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((
decode_escapes_and_references(&input[index + 1..cursor]),
next,
));
}
cursor = next;
}
return None;
}
let (value, next) = parse_attribute_token(input, index);
Some((
decode_selected_escapes_and_references(value, |char| matches!(char, '\\' | '&')),
next,
))
}
fn parse_code_span_with(
lookups: &mut impl Lookups,
input: &str,
index: usize,
) -> Option<(usize, String)> {
let len = backtick_run_len(input, index);
let search_start = index + len;
let close = lookups.code_span_close(search_start, len)?;
Some((
close + len,
normalize_code_span(&input[search_start..close]),
))
}
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 (before, after) = code_padding(input);
let mut normalized = String::new();
let content = &input[before..input.len() - after];
let mut cursor = 0;
while cursor < content.len() {
let (next, char) = next_char(content, cursor).expect("valid UTF-8 byte index");
if char == '\r' {
if content.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;
}
normalized
}
pub(crate) fn code_padding(input: &str) -> (usize, usize) {
let space = |text: &str| -> Option<usize> {
match text.as_bytes().first()? {
b' ' | b'\n' => Some(1),
b'\r' if text.as_bytes().get(1) == Some(&b'\n') => Some(2),
b'\r' => Some(1),
_ => None,
}
};
let end_space = |text: &str| -> Option<usize> {
match text.as_bytes().last()? {
b' ' | b'\r' => Some(1),
b'\n' if text.len() >= 2 && text.as_bytes()[text.len() - 2] == b'\r' => Some(2),
b'\n' => Some(1),
_ => None,
}
};
match (space(input), end_space(input)) {
(Some(before), Some(after))
if input
.bytes()
.any(|byte| !matches!(byte, b' ' | b'\n' | b'\r')) =>
{
(before, after)
}
_ => (0, 0),
}
}
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 starts_delimiter_run(input: &str, index: usize, marker: u8) -> bool {
index == 0 || input.as_bytes()[index - 1] != marker || is_escaped_at(input, index - 1)
}
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
}
#[derive(Clone, Copy)]
struct DelimiterFlanking {
left: bool,
right: bool,
previous: Option<char>,
next: Option<char>,
previous_punctuation: bool,
next_punctuation: bool,
}
#[derive(Clone, Copy, Default)]
struct FlankingSide {
whitespace: bool,
punctuation: bool,
cjk: bool,
ideographic_variation_selector: bool,
}
impl FlankingSide {
fn of(value: Option<char>) -> Self {
let Some(value) = value else {
return Self {
whitespace: true,
..Self::default()
};
};
Self {
whitespace: value.is_whitespace(),
punctuation: is_flanking_punctuation(value),
cjk: crate::unicode_cjk::is_cjk(value),
ideographic_variation_selector: crate::unicode_cjk::is_ideographic_variation_selector(
value,
),
}
}
fn before(previous: Option<char>, before_previous: impl FnOnce() -> Option<char>) -> Self {
match previous {
Some(selector) if crate::unicode_cjk::is_general_use_variation_selector(selector) => {
match before_previous() {
Some(base) if !base.is_whitespace() => {
if selector == '\u{FE01}' && matches!(base, '‘' | '’' | '“' | '”') {
return Self {
punctuation: true,
cjk: true,
..Self::default()
};
}
Self {
ideographic_variation_selector: false,
..Self::of(Some(base))
}
}
_ => Self::default(),
}
}
_ => Self::of(previous),
}
}
}
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 before = FlankingSide::before(previous, || {
let mut chars = input[..index].chars();
chars.next_back();
chars.next_back().map(source_char)
});
let after = FlankingSide::of(next);
let left = !after.whitespace
&& (!after.punctuation
|| before.whitespace
|| before.punctuation
|| after.cjk
|| before.cjk
|| before.ideographic_variation_selector);
let right = !before.whitespace
&& (!before.punctuation
|| after.whitespace
|| after.punctuation
|| before.cjk
|| after.cjk);
DelimiterFlanking {
left,
right,
previous,
next,
previous_punctuation: before.punctuation,
next_punctuation: after.punctuation,
}
}
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(lookups, 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 dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
let kind = MathInlineKind::Dollar { dollars };
let value = if math_is_padded(kind) {
normalize_math_text(raw)
} else {
raw.into()
};
Some((close, value, kind))
}
pub(crate) fn math_is_padded(kind: MathInlineKind) -> bool {
matches!(kind, MathInlineKind::Dollar { dollars: 1 })
}
pub(crate) fn reads_as_inline_math(written: &str, value: &str, kind: MathInlineKind) -> bool {
parse_math_inline(&mut DirectLookups { input: written }, written, 0).is_some_and(
|(end, read, read_kind)| end == written.len() && read == value && read_kind == kind,
)
}
fn scan_to_closing_dollar(
lookups: &mut impl Lookups,
input: &str,
start: usize,
open_dollars: usize,
) -> Option<usize> {
let bytes = input.as_bytes();
if open_dollars == 1 {
if bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
return None;
}
let close = lookups.unescaped_dollar(start)?;
if is_math_space(bytes[close - 1]) || bytes.get(close + 1).is_some_and(u8::is_ascii_digit) {
return None;
}
return Some(close + 1);
}
lookups.find("$$", start).map(|close| close + 2)
}
fn is_math_space(byte: u8) -> bool {
matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
}
fn normalize_math_text(input: &str) -> String {
normalize_code_span(input)
}
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, LinkTailRanges)> {
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,
},
LinkTailRanges {
destination: (cursor, cursor),
title: None,
},
));
}
if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
let omitted = cursor;
let (title, next) = parse_link_title(input, cursor)?;
let title_range = (cursor + 1, next - 1);
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),
},
LinkTailRanges {
destination: (omitted, omitted),
title: Some(title_range),
},
));
}
return None;
}
let destination_start = cursor;
let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
let destination_range = match destination_kind {
LinkDestinationKind::Angle => (destination_start + 1, next - 1),
_ => (destination_start, next),
};
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,
},
LinkTailRanges {
destination: destination_range,
title: None,
},
));
}
if !had_space {
return None;
}
let (title, next) = parse_link_title(input, cursor)?;
let title_range = (cursor + 1, next - 1);
cursor = skip_link_resource_space(input, next)?;
if bytes.get(cursor) == Some(&b')') {
Some((
cursor + 1,
ParsedLinkResource {
destination,
destination_kind,
title: Some(title),
},
LinkTailRanges {
destination: destination_range,
title: Some(title_range),
},
))
} 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((
decode_escapes_and_references(&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() {
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((
decode_escapes_and_references(&input[index..cursor]),
LinkDestinationKind::Bare,
cursor,
))
}
}
fn parse_link_title(input: &str, index: usize) -> Option<(Title, 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;
}
let value = decode_escapes_and_references(&input[index + 1..cursor]);
return Some((Title::new(value, 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 map = SourceMap::verbatim(input.len(), 0);
let lines = collect_lines(input, &map);
let interior = lines.len().saturating_sub(1);
lines
.iter()
.take(interior)
.skip(1)
.any(|line| is_blank(line.text))
}
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 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,
});
}
if !had_space {
return None;
}
let (title, 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),
})
}
fn open_definition_title(source: &str) -> Option<char> {
let (cursor, _) = skip_link_resource_space_with_info(source, 0)?;
let (_, _, next) =
parse_link_destination(&mut DirectLookups { input: source }, source, cursor)?;
let (cursor, had_space) = skip_link_resource_space_with_info(source, next)?;
if !had_space {
return None;
}
let closer = match source.as_bytes().get(cursor)? {
b'"' => '"',
b'\'' => '\'',
b'(' => ')',
_ => return None,
};
(!line_may_close_title(&source[cursor + 1..], closer)).then_some(closer)
}
fn line_may_close_title(line: &str, closer: char) -> bool {
line.char_indices().any(|(index, char)| {
(char == closer || (closer == ')' && char == '(')) && !is_escaped_at(line, index)
})
}
fn line_can_start_definition_title(input: &str) -> bool {
let trimmed = trim_ascii_start(input);
matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
}
fn push_line(output: &mut String, line: &str) {
if !output.is_empty() {
output.push('\n');
}
output.push_str(line);
}
fn end_last_line(value: &mut String) {
if value.is_empty() || ends_with_line_ending(value) {
return;
}
value.push_str(value_line_ending(value));
}
fn value_line_ending(value: &str) -> &'static str {
match value.find(['\r', '\n']) {
Some(index) if value[index..].starts_with("\r\n") => "\r\n",
Some(index) if value[index..].starts_with('\r') => "\r",
_ => "\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([' ', '\t', '\n', '\r'])
.filter(|word| !word.is_empty())
.collect::<Vec<_>>()
.join(" ")
.to_uppercase()
.to_lowercase()
}
#[derive(Clone, Copy)]
pub(crate) struct Labels<'a> {
pub(crate) own: &'a [String],
}
fn definition_exists(definitions: Option<Labels<'_>>, label: &str) -> bool {
if label.is_empty() || !reference_label_is_within_limit(label) {
return false;
}
let Some(definitions) = definitions else {
return false;
};
definitions.own.binary_search(&identifier(label)).is_ok()
}
fn identifier(label: &str) -> String {
let identifier = normalize_label(label);
if identifier.contains('\0') {
identifier.replace('\0', "\u{FFFD}")
} else {
identifier
}
}
fn reference_label_is_within_limit(label: &str) -> bool {
label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
}
pub(crate) fn is_reference_label(label: &str) -> bool {
let written = alloc::format!("[{label}]");
path_walk(1, |cursor| reference_label_step(&written, cursor)) == Some(written.len() - 1)
&& reference_label_is_within_limit(label)
&& !normalize_label(label).is_empty()
&& !holds_blank_line(label)
}
pub(crate) fn is_written_footnote_label(label: &str) -> bool {
is_footnote_label(label) && !is_escaped_at(&alloc::format!("{label}]"), label.len())
}
fn holds_blank_line(text: &str) -> bool {
let text = text.replace("\r\n", "\n");
let lines: Vec<&str> = text.split(['\n', '\r']).collect();
lines.len() > 2 && lines[1..lines.len() - 1].iter().any(|line| is_blank(line))
}
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();
let opens = length >= 3 && (byte == b'~' || !input[length..].contains('`'));
opens.then_some((marker, length))
}
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 && is_blank(&input[count..])
}
fn trim_closing_hashes(input: &str) -> &str {
let input = input.trim_end_matches([' ', '\t']);
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_matches([' ', '\t'])
} else {
input
}
}
fn opens_list_item(input: &str) -> bool {
trim_up_to_three_spaces(input).is_some_and(|rest| blocks::list_marker_head(rest).is_some())
}
fn slice_offset_in(text: &str, slice: &str) -> Option<usize> {
let offset = (slice.as_ptr() as usize).checked_sub(text.as_ptr() as usize)?;
(offset + slice.len() <= text.len()).then_some(offset)
}
fn leading_indent(input: &str) -> (usize, usize) {
leading_indent_at(input, 0)
}
fn leading_indent_at(input: &str, start: usize) -> (usize, usize) {
let mut column = start;
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 - start, bytes)
}
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
}
}
pub(crate) fn is_blank(text: &str) -> bool {
text.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
}
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) -> Option<Vec<TableAlignment>> {
if !input.contains('-')
|| !input
.chars()
.all(|char| matches!(char, '|' | '-' | ':' | ' ' | '\t'))
{
return None;
}
let cells = table_row_cell_ranges(input);
if cells.is_empty() {
return None;
}
let mut alignments = Vec::new();
for (start, end) in cells {
alignments.push(table_delimiter_alignment(
input[start..end].trim_matches([' ', '\t']),
)?);
}
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_row_delimiters(row: &str) -> Vec<usize> {
let bytes = row.as_bytes();
let mut delimiters = Vec::new();
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;
cursor = if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
pipe + 1
} else {
pipe
};
}
b'|' => {
delimiters.push(cursor);
cursor += 1;
}
_ => cursor += 1,
}
}
delimiters
}
fn table_row_cell_ranges(input: &str) -> Vec<(usize, usize)> {
let trimmed = input.trim_matches([' ', '\t']);
let offset = leading_trim_bytes(input);
let delimiters = table_row_delimiters(trimmed);
let mut cells = Vec::new();
let mut start = 0;
for &pipe in &delimiters {
cells.push((offset + start, offset + pipe));
start = pipe + 1;
}
cells.push((offset + start, offset + trimmed.len()));
if delimiters.first() == Some(&0) {
cells.remove(0);
}
if delimiters
.last()
.is_some_and(|&pipe| is_blank(&trimmed[pipe + 1..]))
{
cells.pop();
}
cells
}
struct TableCellSource {
text: DerivedText,
span: Span,
}
fn table_row_cells(line: &Line<'_>, row: &str) -> Vec<TableCellSource> {
let row_offset = slice_offset_in(line.text, row).unwrap_or(0);
table_row_cell_ranges(row)
.into_iter()
.map(|(start, end)| {
let raw = &row[start..end];
let leading = leading_trim_bytes(raw);
let content = raw.trim_matches([' ', '\t']);
let offset = row_offset + start + leading;
let span = Span::new(
line.source_start(offset),
line.source_end(offset + content.len()),
);
let mut text = DerivedText::default();
let bytes = content.as_bytes();
let mut copied = 0;
let mut cursor = 0;
while cursor < bytes.len() {
if bytes[cursor] == b'\\' {
let pipe = cursor + delimiter_byte_run_len(content, cursor, b'\\');
if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
text.append(line, &content[copied..pipe - 1]);
text.append_replacing(
"|",
line.source_start(offset + pipe - 1),
line.source_end(offset + pipe + 1),
);
copied = pipe + 1;
}
cursor = pipe;
} else {
cursor += 1;
}
}
text.append(line, &content[copied..]);
TableCellSource { text, span }
})
.collect()
}
fn table_has_separator(header: &str, delimiter: &str) -> bool {
let Some(alignments) = parse_table_delimiter(delimiter) else {
return true;
};
if alignments.len() == 1 {
return contains_unescaped_pipe(header)
|| contains_unescaped_pipe(delimiter)
|| delimiter.contains(':');
}
true
}
fn contains_unescaped_pipe(input: &str) -> bool {
!table_row_delimiters(input).is_empty()
}
fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
let end = lookups.find(">", index)? + 1;
is_angle_autolink(&input[index + 1..end - 1]).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)? + 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
}
pub(crate) fn angle_autolink_destination(uri: &str) -> Option<String> {
if is_uri_autolink(uri) {
Some(String::from(uri))
} else if is_email_autolink(uri) {
Some(alloc::format!("mailto:{uri}"))
} else {
None
}
}
fn is_angle_autolink(uri: &str) -> bool {
is_uri_autolink(uri) || is_email_autolink(uri)
}
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_ascii_control())
}
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)
}
pub(crate) fn literal_autolink_destination(text: &str) -> Option<String> {
let mut input = String::from(text);
input.push_str(".. ");
let (end, prefix) =
parse_literal_autolink(&input, 0, &mut LiteralAutolinkScan::default(), true)?;
(end == text.len()).then(|| {
let mut destination = String::from(prefix);
destination.push_str(text);
destination
})
}
fn parse_literal_autolink(
input: &str,
index: usize,
scan: &mut LiteralAutolinkScan,
urls: bool,
) -> Option<(usize, &'static str)> {
let rest = &input[index..];
let www = rest
.as_bytes()
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."));
if !urls {
if www {
return None;
}
return parse_literal_email(input, index, scan);
}
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;
}
if !http_literal_host_ok(&input[index + scheme_len..]) {
return None;
}
let end = autolink_url_end(input, index + scheme_len);
if end <= index + scheme_len {
return None;
}
return Some((end, ""));
}
if www {
if !literal_www_prefix_ok(input, index) {
return None;
}
if !check_domain(rest, false) {
return None;
}
let end = autolink_url_end(input, index);
if end <= index || (end <= index + 3 && !literal_starts_line(input, index)) {
return None;
}
return Some((end, "http://"));
}
parse_literal_email(input, index, scan)
}
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 http_literal_host_ok(host: &str) -> bool {
match host.chars().next() {
Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => check_domain(host, true),
Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
check_domain(host, true)
}
_ => false,
}
}
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) -> bool {
let mut np = 0usize;
let mut uscore1 = 0usize;
let mut uscore2 = 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 {
'\\' => {
chars.next();
}
'_' if account => uscore2 += 1,
'.' if account => {
uscore1 = uscore2;
uscore2 = 0;
np += 1;
if np > 10 {
return true;
}
}
'_' | '.' | '-' => {}
_ => {
if !is_valid_hostchar(source_char(char)) {
break;
}
}
}
}
if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
return false;
}
allow_short || np > 0
}
fn autolink_url_end(input: &str, start: usize) -> usize {
let bytes = input.as_bytes();
let mut end = start;
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)
|| (!char.is_ascii()
&& char != '\u{FFFD}'
&& crate::unicode_punctuation::is_unicode_punctuation(char))
|| input[start + offset..].starts_with("[[")
{
break;
}
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) {
if next.is_ascii_punctuation() && next != b'.' {
break;
}
}
}
end = start + offset + char.len_utf8();
}
autolink_delim(input, start, 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';' => {
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
}
#[derive(Default)]
struct LiteralAutolinkScan {
email_local: ByteRun,
email_at: EmailAt,
email_prefix: EmailPrefix,
}
#[derive(Default)]
struct EmailAt {
at: Option<usize>,
from: usize,
last_colon: Option<usize>,
domain_end: [Option<Option<usize>>; 2],
}
impl EmailAt {
fn update(&mut self, input: &str, index: usize, at: usize) {
if self.at != Some(at) {
*self = Self {
at: Some(at),
from: usize::MAX,
..Self::default()
};
}
if index < self.from {
self.last_colon = input[index..at].rfind(':').map(|offset| index + offset);
self.from = index;
}
}
fn domain_end(&mut self, input: &str, is_xmpp: bool) -> Option<usize> {
let at = self.at.expect("updated before use");
*self.domain_end[usize::from(is_xmpp)].get_or_insert_with(|| {
let start = at + 1;
let end = literal_email_domain_end(input, start, is_xmpp)?;
let trimmed = autolink_delim(input, start, end);
(trimmed > start && is_gfm_email_domain(&input[start..trimmed], is_xmpp))
.then_some(trimmed)
})
}
}
#[derive(Default)]
struct EmailPrefix {
cursor: usize,
segment: usize,
non_local: Option<usize>,
at: Option<usize>,
domain_bad: Option<usize>,
domain_dot: Option<usize>,
domain_empty_label: Option<usize>,
}
impl EmailPrefix {
fn advance(&mut self, input: &str, end: usize) {
if end < self.cursor {
*self = Self::default();
}
let bytes = input.as_bytes();
let from = self.cursor;
for (offset, char) in input[from..end].char_indices() {
let position = from + offset;
if char.is_whitespace() {
*self = Self {
segment: position + char.len_utf8(),
..Self::default()
};
continue;
}
let byte = char.is_ascii().then_some(char as u8);
if self.non_local.is_none() && !byte.is_some_and(is_gfm_email_local_byte) {
self.non_local = Some(position);
}
if char == '@' {
self.at = Some(position);
self.domain_bad = None;
self.domain_dot = None;
self.domain_empty_label = None;
continue;
}
let Some(at) = self.at else {
continue;
};
match byte {
Some(b'.') => {
self.domain_dot.get_or_insert(position);
if position == at + 1 || bytes[position - 1] == b'.' {
self.domain_empty_label.get_or_insert(position);
}
}
Some(byte) if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') => {}
_ => {
self.domain_bad.get_or_insert(position);
}
}
}
self.cursor = end;
}
fn ends_with_email(&mut self, input: &str, end: usize) -> bool {
self.advance(input, end);
let Some(at) = self.at else {
return false;
};
let before = |position: Option<usize>| position.is_some_and(|position| position < end);
at > self.segment
&& !self.non_local.is_some_and(|position| position < at)
&& end > at + 1
&& !before(self.domain_bad)
&& before(self.domain_dot)
&& !before(self.domain_empty_label)
&& !matches!(input.as_bytes()[end - 1], b'.' | b'-' | b'_')
}
}
#[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 parse_literal_email(
input: &str,
index: usize,
scan: &mut LiteralAutolinkScan,
) -> Option<(usize, &'static str)> {
let at_index = scan
.email_local
.end(input, index, is_email_local_or_scheme_byte);
if input.as_bytes().get(at_index) != Some(&b'@') || at_index == index {
return None;
}
let (auto_mailto, is_xmpp) = classify_email_local(&input[index..at_index]);
if !email_left_boundary_ok(input, index, auto_mailto, |end| {
scan.email_prefix.ends_with_email(input, end)
}) {
return None;
}
let body_start = if auto_mailto {
index
} else if is_xmpp {
index + "xmpp:".len()
} else {
index + "mailto:".len()
};
scan.email_at.update(input, index, at_index);
if body_start >= at_index
|| scan
.email_at
.last_colon
.is_some_and(|colon| colon >= body_start)
{
return None;
}
let end = scan.email_at.domain_end(input, is_xmpp)?;
Some((end, if auto_mailto { "mailto:" } else { "" }))
}
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,
prefix_ends_with_email: impl FnOnce(usize) -> 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_email(index) {
return true;
}
return false;
}
if auto_mailto && previous == '/' {
return false;
}
true
}
#[cfg(test)]
fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
let start = input[..end]
.char_indices()
.rev()
.find(|(_, char)| char.is_whitespace())
.map_or(0, |(offset, char)| offset + char.len_utf8());
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)
}
#[cfg(test)]
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
}
pub(crate) fn is_footnote_label(label: &str) -> bool {
!label.is_empty()
&& reference_label_is_within_limit(label)
&& !label.contains([' ', '\t', '\n', '\r'])
&& !label
.match_indices(['[', ']'])
.any(|(index, _)| !is_escaped_at(label, index))
}
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
}