use alloc::{string::String, vec::Vec};
use core::{cmp::max, ops::Range};
use satteri_arena::line_ending_iter;
use unicase::UniCase;
#[cfg(feature = "mdx")]
use crate::mdx::*;
use crate::{
HeadingLevel, LinkType, MetadataBlockKind, Options,
linklabel::{LinkLabel, scan_link_label_rest},
parse::{
Allocations, AutolinkCandidate, DirectiveAttrData, FootnoteDef, HeadingAttributes, Item,
ItemBody, LINK_MAX_NESTED_PARENS, LinkDef, scan_containers,
},
post_passes::{scan_autolink_literal, scan_email_autolink},
scanners::*,
strings::CowStr,
tree::{Tree, TreeIndex},
};
pub(crate) fn run_first_pass(
text: &str,
options: Options,
) -> (Tree<Item>, Allocations<'_>, Vec<(usize, String)>) {
let start_capacity = max(128, text.len() / 10);
let lookup_table = &create_lut(&options);
let first_pass = FirstPass {
text,
tree: Tree::with_capacity(start_capacity),
begin_list_item: None,
last_line_blank: false,
list_interrupted_paragraph: false,
refdef_interrupted_paragraph: false,
allocs: Allocations::new(),
options,
lookup_table,
brace_context_next: 0,
brace_context_stack: Vec::new(),
mdx_errors: Vec::new(),
#[cfg(feature = "mdx")]
mdx_expr_allocator: oxc_allocator::Allocator::default(),
pending_lazy_blockquote_close: false,
};
first_pass.run()
}
const MATH_BRACE_CONTEXT_MAX_NESTING: usize = 25;
pub(crate) struct FirstPass<'a, 'b> {
pub(crate) text: &'a str,
pub(crate) tree: Tree<Item>,
begin_list_item: Option<usize>,
last_line_blank: bool,
list_interrupted_paragraph: bool,
refdef_interrupted_paragraph: bool,
pub(crate) allocs: Allocations<'a>,
pub(crate) options: Options,
lookup_table: &'b LookupTable,
brace_context_stack: Vec<u8>,
brace_context_next: usize,
pub(crate) mdx_errors: Vec<(usize, String)>,
pending_lazy_blockquote_close: bool,
#[cfg(feature = "mdx")]
pub(crate) mdx_expr_allocator: oxc_allocator::Allocator,
}
impl<'a, 'b> FirstPass<'a, 'b> {
fn run(mut self) -> (Tree<Item>, Allocations<'a>, Vec<(usize, String)>) {
let mut ix = 0;
while ix < self.text.len() {
ix = self.parse_block(ix);
}
while self.tree.spine_len() > 0 {
self.pop(ix);
}
(self.tree, self.allocs, self.mdx_errors)
}
fn collect_term_segments(&mut self, child: Option<TreeIndex>) -> Vec<(TreeIndex, TreeIndex)> {
let mut segments = Vec::new();
let mut seg: Option<(TreeIndex, TreeIndex)> = None;
let mut cur = child;
while let Some(node) = cur {
let next = self.tree[node].next;
if matches!(self.tree[node].item.body, ItemBody::SoftBreak) {
if let Some(s) = seg.take() {
segments.push(s);
}
} else {
self.tree[node].next = None;
seg = Some(match seg {
None => (node, node),
Some((head, tail)) => {
self.tree[tail].next = Some(node);
(head, node)
}
});
}
cur = next;
}
if let Some(s) = seg.take() {
segments.push(s);
}
segments
}
fn parse_block(&mut self, mut start_ix: usize) -> usize {
let bytes = self.text.as_bytes();
let mut line_start = LineStart::new(&bytes[start_ix..]);
self.brace_context_stack.clear();
self.brace_context_next = 0;
let i = scan_containers(&self.tree, &mut line_start, self.options);
if i < self.tree.spine_len() {
self.list_interrupted_paragraph = false;
self.refdef_interrupted_paragraph = false;
let probe_ix = start_ix + line_start.bytes_scanned();
let line_is_blank = scan_blank_line(&bytes[probe_ix..]).is_some();
if !line_is_blank
&& !self.pending_lazy_blockquote_close
&& self.tree.walk_spine().skip(i).any(|&ix| {
matches!(
self.tree[ix].item.body,
ItemBody::BlockQuote(..) | ItemBody::ListItem(..)
)
})
{
self.pending_lazy_blockquote_close = true;
}
}
for _ in i..self.tree.spine_len() {
self.pop(start_ix);
}
if self.last_line_blank {
let content_probe = start_ix + line_start.bytes_scanned();
let has_content =
content_probe < bytes.len() && scan_blank_line(&bytes[content_probe..]).is_none();
if has_content
&& let Some(up) = self.tree.peek_up()
&& self.tree[up].child.is_some()
{
match self.tree[up].item.body {
ItemBody::ListItem(_, _) => self.mark_enclosing_listitem_spread(),
ItemBody::DefinitionListDefinition(indent, _) => {
self.tree[up].item.body = ItemBody::DefinitionListDefinition(indent, true);
}
_ => {}
}
}
}
loop {
let save = line_start.clone();
let mut outer_indent = line_start.scan_space_upto(4);
if outer_indent >= 4 {
if self.options.contains(Options::ENABLE_MDX) {
let extra = line_start.scan_space_upto(usize::MAX);
let mdx_ix = start_ix + line_start.bytes_scanned();
let has_directive = self.options.contains(Options::ENABLE_DIRECTIVE)
&& scan_ch_repeat(&bytes[mdx_ix..], b':') > 1;
let has_container = scan_listitem(&bytes[mdx_ix..]).is_some()
|| (mdx_ix < bytes.len() && bytes[mdx_ix] == b'>')
|| has_directive;
if !has_container {
if self.options.contains(Options::ENABLE_TABLES)
&& mdx_ix < bytes.len()
&& bytes[mdx_ix] == b'|'
{
break;
}
line_start = save;
break;
}
outer_indent += extra;
} else {
line_start = save;
break;
}
}
if self.options.contains(Options::ENABLE_FOOTNOTES) {
let container_start = start_ix + line_start.bytes_scanned();
if let Some(bytecount) = self.parse_footnote(container_start) {
start_ix = container_start + bytecount;
line_start = LineStart::new(&bytes[start_ix..]);
continue;
}
}
let container_start = start_ix + line_start.bytes_scanned();
if let Some((ch, index, indent)) = line_start.scan_list_marker_with_indent_and_clamp(
outer_indent,
!self.options.contains(Options::ENABLE_MDX),
) {
let after_marker_index = start_ix + line_start.bytes_scanned();
let already_in_list = self
.tree
.peek_up()
.is_some_and(|ix| matches!(self.tree[ix].item.body, ItemBody::List(_, _, _)));
let after_marker_blank = {
let rest = &bytes[after_marker_index..];
rest.is_empty() || scan_blank_line(rest).is_some()
};
if (self.list_interrupted_paragraph || self.refdef_interrupted_paragraph)
&& !already_in_list
&& after_marker_blank
{
self.list_interrupted_paragraph = false;
self.refdef_interrupted_paragraph = false;
line_start = save;
break;
}
let is_textual_one = (ch == b'.' || ch == b')')
&& bytes.get(container_start) == Some(&b'1')
&& bytes
.get(container_start + 1)
.is_some_and(|&b| b == b'.' || b == b')');
if (self.list_interrupted_paragraph || self.refdef_interrupted_paragraph)
&& (ch == b'.' || ch == b')')
&& !is_textual_one
{
self.list_interrupted_paragraph = false;
self.refdef_interrupted_paragraph = false;
line_start = save;
break;
}
self.continue_list(container_start, ch, index);
self.tree.append(Item {
start: container_start,
end: after_marker_index, body: ItemBody::ListItem(indent, false),
});
self.tree.push();
if let Some(n) = scan_blank_line(&bytes[after_marker_index..]) {
self.begin_list_item = Some(after_marker_index + n);
return after_marker_index + n;
}
if self.options.contains(Options::ENABLE_TASKLISTS) {
let saved_line_start = line_start.clone();
let task_list_marker =
line_start.scan_task_list_marker().map(|is_checked| Item {
start: after_marker_index,
end: start_ix + line_start.bytes_scanned(),
body: ItemBody::TaskListMarker(is_checked),
});
if let Some(task_list_marker) = task_list_marker {
let rest = &bytes[task_list_marker.end..];
let marker_ate_newline = matches!(
bytes.get(task_list_marker.end.wrapping_sub(1)),
Some(b'\n' | b'\r')
);
let trailing_ws = rest
.iter()
.position(|&b| b != b' ' && b != b'\t')
.unwrap_or(rest.len());
let after_ws = &rest[trailing_ws..];
let rest_of_line_blank =
after_ws.is_empty() || matches!(after_ws.first(), Some(b'\n' | b'\r'));
let lazy_continuation_start = if marker_ate_newline {
let start = task_list_marker.end + trailing_ws;
(start < bytes.len()
&& scan_blank_line(&bytes[start..]).is_none()
&& !scan_paragraph_interrupt_no_table(
&bytes[start..],
true,
self.options.contains(Options::ENABLE_FOOTNOTES),
self.options.contains(Options::ENABLE_DEFINITION_LIST),
self.options.contains(Options::ENABLE_MDX),
self.options.contains(Options::ENABLE_MATH_MULTI_DOLLAR),
self.options.contains(Options::ENABLE_DIRECTIVE),
&self.tree,
self.tree.spine_len(),
))
.then_some(start)
} else if rest_of_line_blank {
let newline_len = scan_eol(after_ws).unwrap_or(0);
let start = task_list_marker.end + trailing_ws + newline_len;
(newline_len > 0
&& start < bytes.len()
&& scan_blank_line(&bytes[start..]).is_none()
&& !scan_paragraph_interrupt_no_table(
&bytes[start..],
true,
self.options.contains(Options::ENABLE_FOOTNOTES),
self.options.contains(Options::ENABLE_DEFINITION_LIST),
self.options.contains(Options::ENABLE_MDX),
self.options.contains(Options::ENABLE_MATH_MULTI_DOLLAR),
self.options.contains(Options::ENABLE_DIRECTIVE),
&self.tree,
self.tree.spine_len(),
))
.then_some(start)
} else {
None
};
if let Some(new_start) = lazy_continuation_start {
return self.parse_paragraph(new_start, Some(task_list_marker));
} else if rest_of_line_blank || marker_ate_newline {
line_start = saved_line_start;
} else {
return self
.parse_paragraph(task_list_marker.end, Some(task_list_marker));
}
}
}
} else if let Some((indent, child, item)) = self
.options
.contains(Options::ENABLE_DEFINITION_LIST)
.then(|| {
self.tree
.cur()
.map(|cur| (self.tree[cur].child, &mut self.tree[cur].item))
})
.flatten()
.filter(|(_, item)| {
match item.body {
ItemBody::Paragraph | ItemBody::TightParagraph => {
let gap_start = item.end.min(container_start);
line_ending_iter(&bytes[gap_start..container_start])
.take(2)
.count()
<= 1
}
ItemBody::MaybeDefinitionListTitle
| ItemBody::DefinitionListDefinition(..) => true,
_ => false,
}
})
.and_then(|item| {
Some((
line_start
.scan_definition_list_definition_marker_with_indent(outer_indent)?,
item.0,
item.1,
))
})
{
match item.body {
ItemBody::Paragraph | ItemBody::TightParagraph => {
item.body = ItemBody::DefinitionList(true);
let Item { start, end, .. } = *item;
let segments = self.collect_term_segments(child);
let list_idx = self.tree.cur().unwrap();
self.tree[list_idx].child = None;
self.tree.push();
if segments.is_empty() {
self.tree.append(Item {
start,
end,
body: ItemBody::DefinitionListTitle,
});
} else {
for (head, tail) in segments {
let t_start = self.tree[head].item.start;
let t_end = self.tree[tail].item.end;
let title = self.tree.append(Item {
start: t_start,
end: t_end,
body: ItemBody::DefinitionListTitle,
});
self.tree[title].child = Some(head);
}
}
}
ItemBody::MaybeDefinitionListTitle => {
item.body = ItemBody::DefinitionListTitle;
let node_idx = self.tree.cur().unwrap();
let segments = self.collect_term_segments(child);
if let Some((head, tail)) = segments.first().copied() {
let t_start = self.tree[head].item.start;
let t_end = self.tree[tail].item.end;
self.tree[node_idx].child = Some(head);
self.tree[node_idx].item.start = t_start;
self.tree[node_idx].item.end = t_end;
}
for (head, tail) in segments.into_iter().skip(1) {
let t_start = self.tree[head].item.start;
let t_end = self.tree[tail].item.end;
let title = self.tree.append(Item {
start: t_start,
end: t_end,
body: ItemBody::DefinitionListTitle,
});
self.tree[title].child = Some(head);
}
}
ItemBody::DefinitionListDefinition(..) => {}
_ => unreachable!(),
}
let after_marker_index = start_ix + line_start.bytes_scanned();
let loose = self.last_line_blank;
self.tree.append(Item {
start: container_start - outer_indent,
end: after_marker_index, body: ItemBody::DefinitionListDefinition(indent, loose),
});
if let Some(ItemBody::DefinitionList(is_tight)) =
self.tree.peek_up().map(|cur| &mut self.tree[cur].item.body)
&& self.last_line_blank
{
*is_tight = false;
self.last_line_blank = false;
}
self.tree.push();
if let Some(n) = scan_blank_line(&bytes[after_marker_index..]) {
self.begin_list_item = Some(after_marker_index + n);
return after_marker_index + n;
}
} else if line_start.scan_blockquote_marker() {
let kind = if self.options.contains(Options::ENABLE_GITHUB_ALERTS) {
line_start.scan_blockquote_tag()
} else {
None
};
self.finish_list(start_ix);
self.tree.append(Item {
start: container_start,
end: 0, body: ItemBody::BlockQuote(kind),
});
self.tree.push();
if kind.is_some() {
let ix = start_ix + line_start.bytes_scanned();
let mut lazy_line_start = LineStart::new(&bytes[ix..]);
let tree_position =
scan_containers(&self.tree, &mut lazy_line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
if !lazy_line_start.scan_space(4)
&& self.scan_paragraph_interrupt(
&bytes[ix + lazy_line_start.bytes_scanned()..],
current_container,
tree_position,
)
{
return ix;
} else {
line_start = lazy_line_start;
line_start.scan_all_space();
start_ix = ix;
break;
}
}
} else if self.options.contains(Options::ENABLE_DIRECTIVE)
&& scan_ch_repeat(&bytes[(start_ix + line_start.bytes_scanned())..], b':') > 1
{
let colon_start = start_ix + line_start.bytes_scanned();
let colon_count = scan_ch_repeat(&bytes[colon_start..], b':');
if colon_count >= 3 && self.tree.spine_len() <= u8::MAX as usize {
let fence_length = core::cmp::min(colon_count, u8::MAX as usize);
let after_colons = colon_start + colon_count;
if let Some((mut dir_data, content_end)) =
parse_directive_after_colons(self.text, bytes, after_colons)
{
dir_data.initial_size = outer_indent.min(u8::MAX as usize) as u8;
self.finish_list(start_ix);
let after = &bytes[content_end..];
let ws = scan_space_or_tab(after);
let line_end = content_end + ws + scan_nextline(&after[ws..]);
let label_start = dir_data.label_start;
let label_end = dir_data.label_end;
let dir_ix = self.allocs.allocate_directive(dir_data);
self.tree.append(Item {
start: container_start,
end: 0,
body: ItemBody::ContainerDirective(fence_length as u8, dir_ix),
});
self.tree.push();
if label_start != 0 || label_end != 0 {
self.append_container_directive_label(label_start, label_end);
}
return line_end;
} else {
break;
}
} else if colon_count == 2 {
let after_colons = colon_start + 2;
if let Some((dir_data, line_end)) =
parse_directive_after_colons(self.text, bytes, after_colons)
{
let remaining = &bytes[line_end..];
let ws = scan_space_or_tab(remaining);
let at_eol = line_end + ws >= bytes.len()
|| bytes[line_end + ws] == b'\n'
|| bytes[line_end + ws] == b'\r';
if at_eol || line_end >= bytes.len() {
self.finish_list(start_ix);
let label_start = dir_data.label_start;
let label_end = dir_data.label_end;
let dir_ix = self.allocs.allocate_directive(dir_data);
self.tree.append(Item {
start: container_start,
end: line_end,
body: ItemBody::LeafDirective(dir_ix),
});
if label_start < label_end {
self.tree.push();
self.parse_line(
label_start,
Some(label_end),
TableParseMode::Disabled,
label_start,
);
self.tree.pop();
}
let next_line = line_end + scan_nextline(&bytes[line_end..]);
return next_line;
}
}
break;
} else {
break;
}
} else {
line_start = save;
break;
}
}
if self.options.contains(Options::ENABLE_DIRECTIVE) {
let mut pop_count = None;
let mut fence_line_end = start_ix;
let fence_save = line_start.clone();
let _ = line_start.scan_space_upto(3);
let mut matched_length: Option<u8> = None;
for (i, &node_ix) in self.tree.walk_spine().rev().enumerate() {
match self.tree[node_ix].item.body {
ItemBody::ContainerDirective(length, ..) => {
let probe = line_start.clone();
if line_start.scan_closing_container_extensions_fence(length) {
let after_fence = start_ix + line_start.bytes_scanned();
fence_line_end = after_fence + scan_nextline(&bytes[after_fence..]);
pop_count = Some(i + 1);
matched_length = Some(length);
break;
}
line_start = probe;
}
ItemBody::List(..) | ItemBody::ListItem(..) => {}
_ => break,
}
}
if pop_count.is_none() {
line_start = fence_save;
}
if let Some(mut c) = pop_count {
if let Some(length) = matched_length {
let spine: Vec<TreeIndex> = self.tree.walk_spine().copied().collect();
if c < spine.len() {
for &ancestor_ix in spine.iter().rev().skip(c) {
match self.tree[ancestor_ix].item.body {
ItemBody::ContainerDirective(other_length, ..)
if other_length == length =>
{
c += 1;
}
ItemBody::List(..) | ItemBody::ListItem(..) => {}
_ => break,
}
}
}
}
for _ in 0..c {
self.pop(fence_line_end);
}
return fence_line_end;
}
}
let ix = start_ix + line_start.bytes_scanned();
if let Some(n) = scan_blank_line(&bytes[ix..]) {
self.pending_lazy_blockquote_close = false;
if let Some(node_ix) = self.tree.peek_up() {
match &mut self.tree[node_ix].item.body {
ItemBody::ContainerDirective(..) => {
self.mark_enclosing_listitem_spread();
}
ItemBody::BlockQuote(..) => {
self.list_interrupted_paragraph = false;
}
ItemBody::ListItem(indent, _)
| ItemBody::DefinitionListDefinition(indent, _)
if self.begin_list_item.is_some() =>
{
self.last_line_blank = true;
if !self.options.contains(Options::ENABLE_MDX) {
*indent = 0;
}
}
_ => {
self.last_line_blank = true;
}
}
} else {
self.last_line_blank = true;
}
return ix + n;
}
let remaining_space = line_start.remaining_space();
let content_start_ix = start_ix + line_start.bytes_scanned();
let mut indent = line_start.scan_space_upto(4);
if indent == 4 {
if self.options.contains(Options::ENABLE_MDX) {
indent += line_start.scan_space_upto(usize::MAX);
} else {
let empty_listitem_will_close =
self.begin_list_item.is_some_and(|bli| start_ix > bli)
&& self.tree.peek_up().is_some_and(|ix| {
matches!(self.tree[ix].item.body, ItemBody::ListItem(..))
&& self.tree[ix].child.is_none()
});
self.finish_list(start_ix);
if empty_listitem_will_close {
self.pending_lazy_blockquote_close = true;
}
let ix = start_ix + line_start.bytes_scanned();
let remaining_space = line_start.remaining_space();
return self.parse_indented_code_block(content_start_ix, ix, remaining_space);
}
}
let ix = start_ix + line_start.bytes_scanned();
if indent == 0
&& ix == 0
&& self.tree.spine_len() == 0
&& let Some((_n, metadata_block_ch)) = scan_metadata_block(
&bytes[ix..],
self.options
.contains(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS),
self.options
.contains(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS),
)
{
self.finish_list(start_ix);
return self.parse_metadata_block(ix, metadata_block_ch);
}
#[cfg(feature = "mdx")]
if self.options.contains(Options::ENABLE_MDX) {
let at_root_for_esm = indent == 0
&& self
.tree
.walk_spine()
.all(|&ix| matches!(self.tree[ix].item.body, ItemBody::List(..)));
if at_root_for_esm && let Some(end_ix) = scan_mdx_esm(&bytes[ix..]) {
let mut final_end = end_ix;
let candidate = self.text[ix..ix + final_end].trim_end();
if !candidate.is_empty() {
use crate::mdx::EsmParseResult;
let mut allocator = oxc_allocator::Allocator::default();
match crate::mdx::try_parse_esm(candidate, &mut allocator) {
EsmParseResult::Complete => {}
EsmParseResult::Incomplete => {
let mut pos = ix + final_end;
loop {
let blank_start = pos;
while pos < bytes.len()
&& (bytes[pos] == b'\n'
|| bytes[pos] == b'\r'
|| bytes[pos] == b' '
|| bytes[pos] == b'\t')
{
pos += 1;
}
if pos == blank_start || pos >= bytes.len() {
break;
}
let chunk_start = pos;
while pos < bytes.len() {
pos += scan_nextline(&bytes[pos..]);
if pos < bytes.len()
&& (bytes[pos] == b'\n' || bytes[pos] == b'\r')
{
break;
}
}
if pos == chunk_start {
break;
}
final_end = pos - ix;
let candidate = self.text[ix..ix + final_end].trim_end();
match crate::mdx::try_parse_esm(candidate, &mut allocator) {
EsmParseResult::Complete => break,
EsmParseResult::Incomplete => continue,
EsmParseResult::Error => break,
}
}
}
EsmParseResult::Error => {}
}
}
self.finish_list(start_ix);
return self.parse_mdx_esm(ix, ix + final_end);
}
if bytes[ix] == b'<'
&& let Some(end_ix) =
self.scan_mdx_flow_in_container(ix, |b, c| scan_mdx_jsx_block(b, c))
{
self.finish_list(start_ix);
let result = self.parse_mdx_jsx_flow(ix, ix + end_ix);
if contains_blank_line(&bytes[ix..ix + end_ix]) {
self.last_line_blank = true;
self.mark_enclosing_listitem_spread();
}
return result;
}
if bytes[ix] == b'{' {
if let Some(end_ix) =
self.scan_mdx_flow_in_container(ix, |b, c| scan_mdx_expression_block(b, c))
{
self.finish_list(start_ix);
let result = self.parse_mdx_jsx_flow(ix, ix + end_ix);
if contains_blank_line(&bytes[ix..ix + end_ix]) {
self.last_line_blank = true;
self.mark_enclosing_listitem_spread();
}
return result;
}
if scan_mdx_inline_expression(&bytes[ix..]).is_none() {
self.mdx_errors.push((
ix,
"Unexpected end of file in expression, expected a corresponding \
closing brace for `{`"
.to_string(),
));
}
}
}
if bytes[ix] == b'<' && !self.options.contains(Options::ENABLE_MDX) {
let synth = remaining_space.min(indent);
if let Some(html_end_tag) = get_html_end_tag(&bytes[(ix + 1)..]) {
self.finish_list(start_ix);
return self.parse_html_block_type_1_to_5(content_start_ix, html_end_tag, synth, 0);
}
if starts_html_block_type_6(&bytes[(ix + 1)..]) {
self.finish_list(start_ix);
return self.parse_html_block_type_6_or_7(content_start_ix, synth, 0);
}
if let Some(_html_bytes) = scan_html_type_7(&bytes[ix..]) {
self.finish_list(start_ix);
return self.parse_html_block_type_6_or_7(content_start_ix, synth, 0);
}
}
if let Ok(n) = scan_hrule(&bytes[ix..]) {
self.finish_list(start_ix);
return self.parse_hrule(n, ix);
}
if let Some(atx_size) = scan_atx_heading(&bytes[ix..]) {
self.finish_list(start_ix);
return self.parse_atx_heading(ix, atx_size);
}
if let Some((n, fence_ch)) = scan_code_fence(&bytes[ix..]) {
self.finish_list(start_ix);
return self.parse_fenced_code_block(ix, indent, fence_ch, n);
}
if self.options.contains(Options::ENABLE_MATH_MULTI_DOLLAR)
&& let Some(n) = scan_math_fence(&bytes[ix..])
{
self.finish_list(start_ix);
return self.parse_math_block(ix, indent, n);
}
while let Some((bytecount, label, link_def)) =
self.parse_refdef_total(start_ix + line_start.bytes_scanned())
{
self.allocs
.refdefs_all
.push((label.clone(), link_def.clone()));
self.allocs.refdefs.0.entry(label).or_insert(link_def);
let container_start = start_ix + line_start.bytes_scanned();
let mut ix = container_start + bytecount;
let refdef_terminated_by_blank;
if let Some(nl) = scan_blank_line(&bytes[ix..]) {
let after_terminator = ix + nl;
refdef_terminated_by_blank = scan_blank_line(&bytes[after_terminator..]).is_some();
ix = after_terminator;
} else {
self.finish_list(start_ix);
self.refdef_interrupted_paragraph = true;
return ix;
}
if let Some(lazy_line_start) = self.scan_next_line_or_lazy_continuation(&bytes[ix..]) {
line_start = lazy_line_start;
start_ix = ix;
} else {
self.finish_list(start_ix);
if !refdef_terminated_by_blank {
self.refdef_interrupted_paragraph = true;
}
return ix;
}
}
let ix = start_ix + line_start.bytes_scanned();
self.parse_paragraph(ix, None)
}
fn scan_next_line_or_lazy_continuation<'input>(
&mut self,
bytes: &'input [u8],
) -> Option<LineStart<'input>> {
let mut line_start = LineStart::new(bytes);
let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
if self.options.contains(Options::ENABLE_MDX) {
line_start.scan_all_space();
if self.scan_paragraph_interrupt(
&bytes[line_start.bytes_scanned()..],
current_container,
tree_position,
) || scan_blank_line(&bytes[line_start.bytes_scanned()..]).is_some()
{
None
} else {
Some(line_start)
}
} else if (!line_start.scan_space(4)
&& self.scan_paragraph_interrupt(
&bytes[line_start.bytes_scanned()..],
current_container,
tree_position,
))
|| scan_blank_line(&bytes[line_start.bytes_scanned()..]).is_some()
{
None
} else {
line_start.scan_all_space();
Some(line_start)
}
}
fn parse_table(
&mut self,
table_cols: usize,
head_start: usize,
body_start: usize,
) -> Option<usize> {
let mut missing_empty_cells = 0;
let (_sep_start, thead_ix) =
self.parse_table_row_inner(head_start, table_cols, &mut missing_empty_cells)?;
self.tree[thead_ix].item.body = ItemBody::TableHead;
let mut ix = body_start;
while let Some((next_ix, _row_ix)) =
self.parse_table_row(ix, table_cols, &mut missing_empty_cells)
{
ix = next_ix;
}
self.pop(ix);
Some(ix)
}
fn parse_table_row_inner(
&mut self,
mut ix: usize,
row_cells: usize,
missing_empty_cells: &mut usize,
) -> Option<(usize, TreeIndex)> {
let bytes = self.text.as_bytes();
let mut cells = 0;
let mut final_cell_ix = None;
let old_cur = self.tree.cur();
let row_ix = self.tree.append(Item {
start: ix,
end: 0, body: ItemBody::TableRow,
});
self.tree.push();
let mut first_iter = true;
let mut saw_opening_pipe = false;
loop {
let cell_start = ix;
let pipe_consumed = scan_ch(&bytes[ix..], b'|');
ix += pipe_consumed;
if first_iter && pipe_consumed > 0 {
saw_opening_pipe = true;
}
first_iter = false;
let _start_ix = ix;
ix += scan_space_or_tab(&bytes[ix..]);
if let Some(eol_bytes) = scan_eol(&bytes[ix..]) {
if saw_opening_pipe && cells == 0 {
let empty_cell_ix = self.tree.append(Item {
start: cell_start,
end: ix,
body: ItemBody::TableCell,
});
final_cell_ix = Some(empty_cell_ix);
cells = 1;
}
ix += eol_bytes;
break;
}
let cell_ix = self.tree.append(Item {
start: cell_start,
end: ix,
body: ItemBody::TableCell,
});
self.tree.push();
let (next_ix, _brk) = self.parse_line(ix, None, TableParseMode::Active, ix);
self.tree[cell_ix].item.end = next_ix;
self.tree.pop();
ix = next_ix;
cells += 1;
if cells == row_cells {
final_cell_ix = Some(cell_ix);
}
}
if let (Some(cur), 0) = (old_cur, cells) {
self.pop(ix);
self.tree[cur].next = None;
return None;
}
let _ = row_cells;
let _ = missing_empty_cells;
let last_cell_ix = {
let mut walker = self.tree[row_ix].child;
let mut last = None;
while let Some(c) = walker {
last = Some(c);
walker = self.tree[c].next;
}
last
};
if let Some(cell_ix) = last_cell_ix {
let row_end = ix;
let mut cell_end = self.tree[cell_ix].item.end;
let bytes = self.text.as_bytes();
while cell_end < row_end
&& cell_end < bytes.len()
&& bytes[cell_end] != b'\n'
&& bytes[cell_end] != b'\r'
{
cell_end += 1;
}
self.tree[cell_ix].item.end = cell_end;
}
let _ = final_cell_ix;
self.pop(ix);
Some((ix, row_ix))
}
fn parse_table_row(
&mut self,
mut ix: usize,
row_cells: usize,
missing_empty_cells: &mut usize,
) -> Option<(usize, TreeIndex)> {
let bytes = self.text.as_bytes();
let mut line_start = LineStart::new(&bytes[ix..]);
let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
if !current_container {
return None;
}
if self.options.contains(Options::ENABLE_MDX) {
line_start.scan_all_space();
} else {
let _ = line_start.scan_space_upto(3);
if !line_start.is_at_eol() && line_start.scan_space(1) {
return None;
}
}
ix += line_start.bytes_scanned();
if scan_paragraph_interrupt_no_table(
&bytes[ix..],
current_container,
self.options.contains(Options::ENABLE_FOOTNOTES),
self.options.contains(Options::ENABLE_DEFINITION_LIST),
self.options.contains(Options::ENABLE_MDX),
self.options.contains(Options::ENABLE_MATH_MULTI_DOLLAR),
self.options.contains(Options::ENABLE_DIRECTIVE),
&self.tree,
tree_position,
) {
return None;
}
let (ix, row_ix) = self.parse_table_row_inner(ix, row_cells, missing_empty_cells)?;
Some((ix, row_ix))
}
fn append_container_directive_label(&mut self, label_start: usize, label_end: usize) {
let bracket_offset = label_start.saturating_sub(1);
let bracket_end = label_end + 1;
self.tree.append(Item {
start: bracket_offset,
end: bracket_end,
body: ItemBody::DirectiveLabel,
});
self.tree.push();
if label_start < label_end {
self.parse_line(
label_start,
Some(label_end),
TableParseMode::Disabled,
label_start,
);
}
self.tree.pop();
}
fn parse_paragraph(&mut self, start_ix: usize, tasklist_marker: Option<Item>) -> usize {
self.list_interrupted_paragraph = false;
let body = if let Some(ItemBody::DefinitionList(_)) =
self.tree.peek_up().map(|idx| self.tree[idx].item.body)
{
if self.tree.cur().is_none_or(|idx| {
matches!(
&self.tree[idx].item.body,
ItemBody::DefinitionListDefinition(..)
)
}) {
self.last_line_blank = false;
ItemBody::MaybeDefinitionListTitle
} else {
self.finish_list(start_ix);
ItemBody::Paragraph
}
} else {
self.finish_list(start_ix);
ItemBody::Paragraph
};
let node_ix = self.tree.append(Item {
start: start_ix,
end: 0, body,
});
self.tree.push();
if let Some(item) = tasklist_marker {
self.tree.append(item);
}
let bytes = self.text.as_bytes();
let mut ix = start_ix;
loop {
let scan_mode = if self.options.contains(Options::ENABLE_TABLES) && ix == start_ix {
TableParseMode::Scan
} else {
TableParseMode::Disabled
};
let (next_ix, brk) = self.parse_line(ix, None, scan_mode, start_ix);
if let Some(Item {
body: ItemBody::Table(alignment_ix),
..
}) = brk
{
let table_cols = self.allocs[alignment_ix].len();
self.tree[node_ix].item.body = ItemBody::Table(alignment_ix);
self.tree[node_ix].child = None;
self.tree.pop();
if body == ItemBody::MaybeDefinitionListTitle {
self.finish_list(ix);
}
self.tree.push();
if let Some(ix) = self.parse_table(table_cols, ix, next_ix) {
return ix;
}
}
ix = next_ix;
let mut line_start = LineStart::new(&bytes[ix..]);
let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
let trailing_backslash_pos = match brk {
Some(Item {
start,
body: ItemBody::HardBreak(true),
..
}) if bytes[start] == b'\\' => Some(start),
_ => None,
};
let is_indented = if self.options.contains(Options::ENABLE_MDX) {
line_start.scan_all_space();
false
} else {
line_start.scan_space(4)
};
if !is_indented {
let ix_new = ix + line_start.bytes_scanned();
if current_container
&& let Some(ix_setext) =
self.parse_setext_heading(ix_new, node_ix, trailing_backslash_pos.is_some())
{
if let Some(pos) = trailing_backslash_pos {
self.tree.append_text(pos, pos + 1, false);
}
self.pop(ix_setext);
if body == ItemBody::MaybeDefinitionListTitle {
self.finish_list(ix);
}
return ix_setext;
}
let suffix = &bytes[ix_new..];
if self.scan_paragraph_interrupt(suffix, current_container, tree_position) {
if !(self.options.contains(Options::ENABLE_MDX)
&& prev_line_has_open_inline_jsx(bytes, ix, self.options.has_math()))
{
if let Some(pos) = trailing_backslash_pos {
self.tree.append_text(pos, pos + 1, false);
}
self.list_interrupted_paragraph = scan_listitem(suffix).is_some()
|| scan_blockquote_start(suffix).is_some();
if !self.options.contains(Options::ENABLE_MDX)
&& !current_container
&& suffix.starts_with(b"<")
&& scan_html_type_7(suffix).is_some()
&& !starts_html_block_type_6(&suffix[1..])
&& get_html_end_tag(&suffix[1..]).is_none()
{
let line_terminated = suffix.iter().any(|&b| b == b'\n' || b == b'\r');
let parent_is_container =
self.tree.walk_spine().nth(tree_position).is_some_and(|ix| {
matches!(
self.tree[*ix].item.body,
ItemBody::BlockQuote(..) | ItemBody::ListItem(..)
)
});
if parent_is_container && line_terminated {
self.pop(ix);
return self.parse_html_block_type_6_or_7(
ix_new,
line_start.remaining_space(),
0,
);
}
}
break;
}
}
if self.options.contains(Options::ENABLE_DIRECTIVE)
&& !current_container
&& line_start.scan_closing_container_extensions_fence(3)
{
break;
}
}
line_start.scan_all_space();
if line_start.is_at_eol() {
if let Some(pos) = trailing_backslash_pos {
self.tree.append_text(pos, pos + 1, false);
}
break;
}
if self.options.contains(Options::ENABLE_DIRECTIVE) {
let mut closes = false;
for &node_ix in self.tree.walk_spine().rev().skip(1) {
match self.tree[node_ix].item.body {
ItemBody::ContainerDirective(length, ..) => {
let probe = line_start.clone();
if line_start.scan_closing_container_extensions_fence(length) {
closes = true;
break;
}
line_start = probe;
}
ItemBody::List(..) | ItemBody::ListItem(..) => {}
_ => break,
}
}
if closes {
break;
}
}
ix = next_ix + line_start.bytes_scanned();
if let Some(item) = brk {
self.tree.append(item);
}
}
self.pop(ix);
ix
}
fn parse_setext_heading(
&mut self,
ix: usize,
node_ix: TreeIndex,
has_trailing_content: bool,
) -> Option<usize> {
let bytes = self.text.as_bytes();
let (n, level) = scan_setext_heading(&bytes[ix..])?;
let mut attrs = None;
if let Some(cur_ix) = self.tree.cur() {
let parent_ix = self.tree.peek_up().unwrap();
let header_start = self.tree[parent_ix].item.start;
let header_end = self.tree[cur_ix].item.end;
let (content_end, attrs_, tail_start) =
match self.extract_and_parse_heading_attribute_block(header_start, header_end) {
Some(block) => (block.block_open, Some(block.attrs), block.tail_start),
None => (header_end, None, None),
};
attrs = attrs_;
#[cfg(feature = "mdx")]
if attrs.is_some() && self.options.contains(Options::ENABLE_MDX) {
self.mdx_errors
.retain(|(offset, _)| *offset < content_end || *offset >= header_end);
}
let new_end = if has_trailing_content {
content_end
} else {
let mut last_line_start = header_start;
if attrs.is_some() {
loop {
let next_line_start =
last_line_start + scan_nextline(&bytes[last_line_start..content_end]);
if next_line_start >= content_end {
break;
}
let mut line_start = LineStart::new(&bytes[next_line_start..content_end]);
if scan_containers(&self.tree, &mut line_start, self.options)
!= self.tree.spine_len()
{
break;
}
last_line_start = next_line_start + line_start.bytes_scanned();
}
}
let trailing_ws =
scan_rev_while(&bytes[last_line_start..content_end], is_space_or_tab);
content_end - trailing_ws
};
if attrs.is_some() {
self.tree.truncate_siblings(new_end);
}
if let Some(cur_ix) = self.tree.cur() {
self.tree[cur_ix].item.end = new_end;
}
if let Some(tail_start) = tail_start {
self.parse_line(
tail_start,
Some(header_end),
TableParseMode::Disabled,
tail_start,
);
}
}
self.tree[node_ix].item.body = ItemBody::Heading(
level,
attrs.map(|attrs| self.allocs.allocate_heading(attrs)),
);
let bytes = self.text.as_bytes();
let is_adjacent = |prev_end: usize, next_start: usize| -> bool {
if next_start <= prev_end {
return false;
}
let gap = &bytes[prev_end..next_start];
let mut line_endings = 0;
let mut i = 0;
while i < gap.len() {
let b = gap[i];
if b == b'\n' || b == b'\r' {
if line_endings > 0 {
return false;
}
line_endings += 1;
i += if b == b'\r' && gap.get(i + 1) == Some(&b'\n') {
2
} else {
1
};
} else if b == b' ' || b == b'\t' {
if line_endings == 0 {
return false;
}
i += 1;
} else {
return false;
}
}
line_endings == 1
};
let original_start = self.tree[node_ix].item.start;
let first_line_end = memchr::memchr2(b'\n', b'\r', &bytes[original_start..ix])
.map(|p| original_start + p)
.unwrap_or(ix);
let first_line = &bytes[original_start..first_line_end];
let is_setext_underline_shape = {
let mut p = 0;
while p < first_line.len() && first_line[p] == b' ' && p < 3 {
p += 1;
}
if p < first_line.len() && (first_line[p] == b'-' || first_line[p] == b'=') {
let c = first_line[p];
let mut q = p + 1;
while q < first_line.len() && first_line[q] == c {
q += 1;
}
while q < first_line.len() && (first_line[q] == b' ' || first_line[q] == b'\t') {
q += 1;
}
q == first_line.len()
} else {
false
}
};
if !is_setext_underline_shape {
let mut cur_start = original_start;
for def in self.allocs.refdefs_all.iter().rev() {
let def_end = def.1.span.end;
let def_start = def.1.span.start;
if is_adjacent(def_end, cur_start) {
cur_start = def_start;
} else {
break;
}
}
if cur_start < original_start {
self.tree[node_ix].item.start = cur_start;
}
}
Some(ix + n)
}
fn append_autolink_link(&mut self, d: AutolinkDetection, begin_text: usize, escaped: bool) {
let link_ix = self
.allocs
.allocate_link(d.link_type, d.url.into(), "".into(), "".into());
self.tree.append_text(begin_text, d.start, escaped);
let link_node_ix = self.tree.append(Item {
start: d.start,
end: d.end,
body: ItemBody::Link(link_ix),
});
let text_child = self.tree.create_node(Item {
start: d.start,
end: d.end,
body: ItemBody::Text {
backslash_escaped: false,
},
});
self.tree[link_node_ix].child = Some(text_child);
}
fn append_autolink_marker(&mut self, d: AutolinkDetection, begin_text: usize, escaped: bool) {
let link = self
.allocs
.allocate_link(d.link_type, d.url.into(), "".into(), "".into());
let cand = self.allocs.allocate_autolink_candidate(AutolinkCandidate {
start: d.start,
end: d.end,
link,
});
self.tree.append_text(begin_text, d.start, escaped);
self.tree.append(Item {
start: d.start,
end: d.start,
body: ItemBody::MaybeAutolink(cand),
});
}
fn parse_line(
&mut self,
start: usize,
end: Option<usize>,
mode: TableParseMode,
scope_start: usize,
) -> (usize, Option<Item>) {
let bytes = self.text.as_bytes();
let bytes = match end {
Some(end) => &bytes[..end],
None => bytes,
};
let bytes_len = bytes.len();
let mut pipes = 0;
let mut last_pipe_ix = start;
let mut begin_text = start;
let mut backslash_escaped = false;
let mut last_inline_emission_end: usize = start;
let mut candidate_floor: usize = start;
let mut deferred_ends: Vec<usize> = Vec::new();
let (final_ix, brk) = iterate_special_bytes(self.lookup_table, bytes, start, |ix, byte| {
match byte {
b'\n' | b'\r' => {
if let TableParseMode::Active = mode {
return LoopInstruction::BreakAtWith(ix, None);
}
let mut i = ix;
let eol_bytes = scan_eol(&bytes[ix..]).unwrap();
let end_ix = ix + eol_bytes;
let trailing_backslashes = {
let mut p = ix;
while p > last_inline_emission_end && bytes[p - 1] == b'\\' {
p -= 1;
}
ix - p
};
if mode == TableParseMode::Scan {
let next_line_ix = ix + eol_bytes;
let mut line_start = LineStart::new(&bytes[next_line_ix..]);
if scan_containers(&self.tree, &mut line_start, self.options)
== self.tree.spine_len()
{
if self.options.contains(Options::ENABLE_MDX) {
line_start.scan_all_space();
}
let table_head_ix = next_line_ix + line_start.bytes_scanned();
let delim = &bytes[table_head_ix..];
let leading_spaces =
delim.iter().take(3).take_while(|&&b| b == b' ').count();
let delim_is_list_item =
scan_listitem(&delim[leading_spaces..]).is_some();
let (table_head_bytes, alignment) = if delim_is_list_item {
(0, vec![])
} else {
scan_table_head(delim)
};
if table_head_bytes > 0 {
let header_count =
count_header_cols(bytes, pipes, start, last_pipe_ix);
if alignment.len() == header_count {
let alignment_ix = self.allocs.allocate_alignment(alignment);
let end_ix = table_head_ix + table_head_bytes;
return LoopInstruction::BreakAtWith(
end_ix,
Some(Item {
start: i,
end: end_ix, body: ItemBody::Table(alignment_ix),
}),
);
}
}
}
}
if trailing_backslashes % 2 == 1 && end_ix < bytes_len {
i -= 1;
self.tree.append_text(begin_text, i, backslash_escaped);
backslash_escaped = false;
return LoopInstruction::BreakAtWith(
end_ix,
Some(Item {
start: i,
end: end_ix,
body: ItemBody::HardBreak(true),
}),
);
}
let trailing_spaces = scan_rev_while(&bytes[..ix], |c| c == b' ');
let has_tab_before_spaces = trailing_spaces > 0
&& ix > trailing_spaces
&& bytes[ix - trailing_spaces - 1] == b'\t';
if trailing_spaces >= 2 && !has_tab_before_spaces {
i -= trailing_spaces;
self.tree.append_text(begin_text, i, backslash_escaped);
backslash_escaped = false;
return LoopInstruction::BreakAtWith(
end_ix,
Some(Item {
start: i,
end: end_ix,
body: ItemBody::HardBreak(false),
}),
);
}
let trailing_whitespace = scan_rev_while(&bytes[..ix], is_space_or_tab);
self.tree
.append_text(begin_text, ix - trailing_whitespace, backslash_escaped);
backslash_escaped = false;
LoopInstruction::BreakAtWith(
end_ix,
Some(Item {
start: i,
end: end_ix,
body: ItemBody::SoftBreak,
}),
)
}
b'\\' if bytes.get(ix + 1).copied().is_some_and(is_ascii_punctuation) => {
self.tree.append_text(begin_text, ix, backslash_escaped);
if bytes[ix + 1] == b'`' {
let count = 1 + scan_ch_repeat(&bytes[(ix + 2)..], b'`');
self.tree.append(Item {
start: ix + 1,
end: ix + count + 1,
body: ItemBody::MaybeCode(count, true),
});
begin_text = ix + 1 + count;
backslash_escaped = false;
LoopInstruction::ContinueAndSkip(count)
} else if bytes[ix + 1] == b'|' && TableParseMode::Active == mode {
begin_text = ix + 1;
backslash_escaped = true;
LoopInstruction::ContinueAndSkip(1)
} else if bytes[ix + 1] == b'<' {
self.tree.append(Item {
start: ix + 1,
end: ix + 2,
body: ItemBody::MaybeHtml(true),
});
begin_text = ix + 2;
backslash_escaped = false;
LoopInstruction::ContinueAndSkip(1)
} else if bytes[ix + 1] == b'$' && self.options.has_math() {
begin_text = ix + 1;
backslash_escaped = true;
LoopInstruction::ContinueAndSkip(0)
} else if let Some((count, fired)) = (deferred_ends.contains(&(ix + 1)))
.then(|| escaped_delim_run(self.text, start, ix, mode, self.options))
.flatten()
{
let blocked = delim_run_flags(
self.text,
start,
ix + 2,
count - 1,
mode,
self.options,
);
self.tree.append(Item {
start: ix + 1,
end: ix + 2,
body: ItemBody::MaybeEmphasisEscaped(count, fired.0, fired.1),
});
for i in 1..count {
self.tree.append(Item {
start: ix + 1 + i,
end: ix + 2 + i,
body: ItemBody::MaybeEmphasis(count - i, blocked.0, blocked.1),
});
}
begin_text = ix + 1 + count;
backslash_escaped = false;
LoopInstruction::ContinueAndSkip(count)
} else {
begin_text = ix + 1;
backslash_escaped = true;
LoopInstruction::ContinueAndSkip(1)
}
}
c @ b'*' | c @ b'_' | c @ b'~' | c @ b'^' => {
if c == b'_' && self.options.contains(Options::ENABLE_GFM) {
let paragraph_floor = self
.tree
.peek_up()
.map(|nix| self.tree[nix].item.start)
.unwrap_or(start);
if let Some((email_start, email_end, full_url)) =
scan_email_forward_from_atext(bytes, ix, begin_text, paragraph_floor)
&& email_start >= candidate_floor
{
let d = AutolinkDetection {
start: email_start,
end: email_end,
link_type: LinkType::Email,
url: full_url
.strip_prefix("mailto:")
.map(str::to_owned)
.unwrap_or(full_url),
};
if defer_autolink_decision(bytes, paragraph_floor, ix, self.options) {
candidate_floor = email_start + 1;
self.append_autolink_marker(d, begin_text, backslash_escaped);
if email_start > begin_text {
backslash_escaped = false;
}
begin_text = email_start;
} else {
candidate_floor = email_end;
self.append_autolink_link(d, begin_text, backslash_escaped);
backslash_escaped = false;
begin_text = email_end;
last_inline_emission_end = email_end;
let skip = email_end.saturating_sub(ix + 1);
return LoopInstruction::ContinueAndSkip(skip);
}
}
}
let string_suffix = &self.text[ix..];
let count = 1 + scan_ch_repeat(&string_suffix.as_bytes()[1..], c);
let can_open = delim_run_can_open(
&self.text[start..],
string_suffix,
count,
ix - start,
mode,
self.options,
);
let can_close = delim_run_can_close(
&self.text[start..],
string_suffix,
count,
ix - start,
mode,
self.options,
);
let is_valid_seq = delim_run_is_valid(c, count, self.options);
if (can_open || can_close) && is_valid_seq {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
for i in 0..count {
self.tree.append(Item {
start: ix + i,
end: ix + i + 1,
body: ItemBody::MaybeEmphasis(count - i, can_open, can_close),
});
}
begin_text = ix + count;
}
LoopInstruction::ContinueAndSkip(count - 1)
}
b'$' => {
let brace_context =
if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
self.brace_context_next as u8
} else {
self.brace_context_stack.last().copied().unwrap_or_else(|| {
self.brace_context_stack.push(!0);
!0
})
};
let dollar_escaped = backslash_escaped && begin_text == ix;
self.tree.append_text(begin_text, ix, backslash_escaped);
self.tree.append(Item {
start: ix,
end: ix + 1,
body: ItemBody::MaybeMath(dollar_escaped, brace_context),
});
begin_text = ix + 1;
backslash_escaped = false;
LoopInstruction::ContinueAndSkip(0)
}
#[cfg(feature = "mdx")]
b'{' if self.options.contains(Options::ENABLE_MDX) => {
if is_inside_code_span(bytes, ix)
|| is_inside_link_url_parens(bytes, ix, scope_start)
|| is_inside_open_inline_jsx_tag(bytes, ix)
|| (self.options.has_math() && is_inside_math_span(bytes, ix))
{
LoopInstruction::ContinueAndSkip(0)
} else {
let scan_result = if self.tree.spine_len() > 0 {
let check = self.make_container_line_check();
let allow_lazy_body = !is_at_paragraph_line_start(bytes, ix);
scan_mdx_inline_expression_in_container(
&bytes[ix..],
&check,
allow_lazy_body,
)
} else {
scan_mdx_inline_expression(&bytes[ix..])
};
if let Some((content_start, content_end, total_len)) = scan_result {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
let (normalized, offset_map) =
self.inline_expression_value(ix + content_start, ix + content_end);
if let Some((err_offset, detail)) =
crate::mdx::try_parse_expression_body(
&normalized,
&mut self.mdx_expr_allocator,
)
{
let source_offset =
satteri_arena::mdx_types::Location::relative_to_absolute(
&offset_map,
err_offset,
)
.unwrap_or(ix + content_start + err_offset);
self.mdx_errors.push((
source_offset,
format!("Could not parse expression with oxc: {detail}"),
));
}
let cow_ix = self.allocs.allocate_cow(normalized.into());
self.tree.append(Item {
start: ix,
end: ix + total_len,
body: ItemBody::MdxTextExpression(cow_ix),
});
begin_text = ix + total_len;
LoopInstruction::ContinueAndSkip(total_len - 1)
} else {
self.mdx_errors.push((
ix,
"Unexpected end of file in expression, expected a corresponding \
closing brace for `{`"
.to_string(),
));
LoopInstruction::ContinueAndSkip(0)
}
}
}
b'{' => {
if self.brace_context_stack.len() == MATH_BRACE_CONTEXT_MAX_NESTING {
self.brace_context_stack.push(self.brace_context_next as u8);
self.brace_context_next = MATH_BRACE_CONTEXT_MAX_NESTING;
} else if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
self.brace_context_next += 1;
} else if !self.brace_context_stack.is_empty() {
self.brace_context_stack.push(self.brace_context_next as u8);
self.brace_context_next += 1;
}
LoopInstruction::ContinueAndSkip(0)
}
b'}' => {
if let &mut [ref mut top_level_context] = &mut self.brace_context_stack[..] {
*top_level_context = top_level_context.wrapping_sub(1);
} else if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
if self.brace_context_next <= MATH_BRACE_CONTEXT_MAX_NESTING {
self.brace_context_stack.pop();
} else {
self.brace_context_next -= 1;
}
} else {
self.brace_context_stack.pop();
}
LoopInstruction::ContinueAndSkip(0)
}
b'`' => {
let count = 1 + scan_ch_repeat(&bytes[(ix + 1)..], b'`');
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + count,
body: ItemBody::MaybeCode(count, false),
});
begin_text = ix + count;
LoopInstruction::ContinueAndSkip(count - 1)
}
b'<' if self.options.contains(Options::ENABLE_MDX)
|| bytes.get(ix + 1) != Some(&b'\\') =>
{
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + 1,
body: ItemBody::MaybeHtml(false),
});
begin_text = ix + 1;
LoopInstruction::ContinueAndSkip(0)
}
b'!' if bytes.get(ix + 1) == Some(&b'[') => {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + 2,
body: ItemBody::MaybeImage,
});
begin_text = ix + 2;
LoopInstruction::ContinueAndSkip(1)
}
b'[' => {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + 1,
body: ItemBody::MaybeLinkOpen,
});
begin_text = ix + 1;
LoopInstruction::ContinueAndSkip(0)
}
b']' => {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + 1,
body: ItemBody::MaybeLinkClose(true),
});
begin_text = ix + 1;
LoopInstruction::ContinueAndSkip(0)
}
b'&' => match scan_entity(&bytes[ix..]) {
(n, Some(value)) => {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + n,
body: ItemBody::SynthesizeText(self.allocs.allocate_cow(value)),
});
begin_text = ix + n;
LoopInstruction::ContinueAndSkip(n - 1)
}
_ => LoopInstruction::ContinueAndSkip(0),
},
b':' if self.options.contains(Options::ENABLE_DIRECTIVE) => {
if ix > 0 && bytes[ix - 1] == b':' {
LoopInstruction::ContinueAndSkip(0)
} else if is_inside_code_span(bytes, ix) {
LoopInstruction::ContinueAndSkip(0)
} else if let Some((dir_data, end_pos)) =
parse_directive_after_colons(self.text, bytes, ix + 1)
{
if end_pos < bytes.len() && bytes[end_pos] == b':' {
let name_end = ix + 1 + dir_data.name.len();
if name_end == end_pos {
return LoopInstruction::ContinueAndSkip(0);
}
}
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
let label_start = dir_data.label_start;
let label_end = dir_data.label_end;
let dir_ix = self.allocs.allocate_directive(dir_data);
let consumed = end_pos - ix;
self.tree.append(Item {
start: ix,
end: end_pos,
body: ItemBody::TextDirective(dir_ix),
});
if label_start < label_end {
self.tree.push();
self.parse_line(
label_start,
Some(label_end),
TableParseMode::Disabled,
label_start,
);
self.tree.pop();
}
begin_text = end_pos;
LoopInstruction::ContinueAndSkip(consumed - 1)
} else {
LoopInstruction::ContinueAndSkip(0)
}
}
b'|' => {
let preceding_backslashes = scan_rev_while(&bytes[..ix], |b| b == b'\\');
if preceding_backslashes % 2 == 1 {
LoopInstruction::ContinueAndSkip(0)
} else if let TableParseMode::Active = mode {
LoopInstruction::BreakAtWith(ix, None)
} else {
last_pipe_ix = ix;
pipes += 1;
LoopInstruction::ContinueAndSkip(0)
}
}
b'.' if matches!(bytes.get(ix + 1..), Some(&[b'.', b'.', ..])) => {
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + 3,
body: ItemBody::SynthesizeChar('…'),
});
begin_text = ix + 3;
LoopInstruction::ContinueAndSkip(2)
}
b'-' => {
let count = 1 + scan_ch_repeat(&bytes[(ix + 1)..], b'-');
if count == 1 {
LoopInstruction::ContinueAndSkip(0)
} else {
let itembody = if count == 2 {
ItemBody::SynthesizeChar('–')
} else if count == 3 {
ItemBody::SynthesizeChar('—')
} else {
let buf = crate::post_passes::smart_dash_run(count);
ItemBody::SynthesizeText(self.allocs.allocate_cow(buf.into()))
};
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + count,
body: itembody,
});
begin_text = ix + count;
LoopInstruction::ContinueAndSkip(count - 1)
}
}
c @ b'\'' | c @ b'"' => {
let string_suffix = &self.text[ix..];
let can_open = delim_run_can_open(
&self.text[start..],
string_suffix,
1,
ix - start,
mode,
self.options,
);
let can_close = delim_run_can_close(
&self.text[start..],
string_suffix,
1,
ix - start,
mode,
self.options,
);
self.tree.append_text(begin_text, ix, backslash_escaped);
backslash_escaped = false;
self.tree.append(Item {
start: ix,
end: ix + 1,
body: ItemBody::MaybeSmartQuote(c, can_open, can_close),
});
begin_text = ix + 1;
LoopInstruction::ContinueAndSkip(0)
}
b'h' | b'H' | b'w' | b'W' | b'@' if self.options.contains(Options::ENABLE_GFM) => {
let paragraph_floor = self
.tree
.peek_up()
.map(|ix| self.tree[ix].item.start)
.unwrap_or(start);
let detection =
detect_gfm_autolink(bytes, ix, byte, paragraph_floor, begin_text)
.filter(|d| d.start >= candidate_floor);
if let Some(d) = detection {
let (cand_start, cand_end) = (d.start, d.end);
if defer_autolink_decision(bytes, paragraph_floor, ix, self.options) {
candidate_floor = cand_start + 1;
deferred_ends.retain(|&e| e > ix);
deferred_ends.push(cand_end);
self.append_autolink_marker(d, begin_text, backslash_escaped);
if cand_start > begin_text {
backslash_escaped = false;
}
begin_text = cand_start;
LoopInstruction::ContinueAndSkip(0)
} else {
candidate_floor = cand_end;
self.append_autolink_link(d, begin_text, backslash_escaped);
begin_text = cand_end;
last_inline_emission_end = cand_end;
backslash_escaped = false;
LoopInstruction::ContinueAndSkip(cand_end.saturating_sub(ix + 1))
}
} else {
LoopInstruction::ContinueAndSkip(0)
}
}
_ => LoopInstruction::ContinueAndSkip(0),
}
});
if brk.is_none() {
let trailing_whitespace = scan_rev_while(&bytes[begin_text..final_ix], is_space_or_tab);
self.tree.append_text(
begin_text,
final_ix - trailing_whitespace,
backslash_escaped,
);
}
(final_ix, brk)
}
fn at_closing_directive_fence(&self, line_start: &LineStart<'_>) -> bool {
if !self.options.contains(Options::ENABLE_DIRECTIVE) {
return false;
}
for &node_ix in self.tree.walk_spine().rev() {
match self.tree[node_ix].item.body {
ItemBody::ContainerDirective(length, ..) => {
let mut probe = line_start.clone();
let _ = probe.scan_space_upto(3);
if probe.scan_closing_container_extensions_fence(length) {
probe.scan_all_space();
if probe.is_at_eol() {
return true;
}
}
}
ItemBody::HtmlBlock(..) | ItemBody::List(..) | ItemBody::ListItem(..) => {}
_ => break,
}
}
false
}
fn parse_html_block_type_1_to_5(
&mut self,
start_ix: usize,
html_end_tag: &str,
mut remaining_space: usize,
mut indent: usize,
) -> usize {
self.list_interrupted_paragraph = false;
let block_node = self.tree.append(Item {
start: start_ix,
end: 0, body: ItemBody::HtmlBlock(false),
});
self.tree.push();
let bytes = self.text.as_bytes();
let mut ix = start_ix;
let end_ix;
let mut closer_pattern_found = false;
let parent_spine_len = self.tree.spine_len() - 1;
let ancestor_has_blockquote = self
.tree
.walk_spine()
.any(|&ix| matches!(self.tree[ix].item.body, ItemBody::BlockQuote(..)));
let mut closed_via_blank = false;
let mut closed_via_lazy = false;
loop {
if parent_spine_len > 0 && scan_blank_line(&bytes[ix..]).is_some() {
let mut peek_ix = ix;
while peek_ix < bytes.len() {
if let Some(adv) = scan_blank_line(&bytes[peek_ix..]) {
peek_ix += adv;
} else {
break;
}
}
if peek_ix == bytes.len() {
end_ix = ix;
closed_via_blank = true;
break;
}
let mut peek_line_start = LineStart::new(&bytes[peek_ix..]);
let n_peek = scan_containers(&self.tree, &mut peek_line_start, self.options);
if n_peek <= parent_spine_len {
end_ix = ix;
closed_via_blank = true;
break;
}
}
let line_start_ix = ix;
ix += scan_nextline(&bytes[ix..]);
self.append_html_line(remaining_space.max(indent), line_start_ix, ix);
let mut line_start = LineStart::new(&bytes[ix..]);
let n_containers = scan_containers(&self.tree, &mut line_start, self.options);
if n_containers < self.tree.spine_len() {
end_ix = ix;
if scan_blank_line(&bytes[ix..]).is_some() {
closed_via_blank = true;
} else {
let next_line_ix = ix + line_start.bytes_scanned();
let rest = &bytes[next_line_ix..];
let opens_container =
scan_blockquote_start(rest).is_some() || scan_listitem(rest).is_some();
if !opens_container {
closed_via_lazy = true;
}
}
break;
}
if self.text[line_start_ix..ix].contains(html_end_tag) {
end_ix = ix;
closer_pattern_found = true;
break;
}
let next_line_ix = ix + line_start.bytes_scanned();
if next_line_ix == self.text.len() {
end_ix = next_line_ix;
break;
}
if self.at_closing_directive_fence(&line_start) {
end_ix = ix;
break;
}
ix = next_line_ix;
remaining_space = line_start.remaining_space();
indent = 0;
}
let trim_trailing = closer_pattern_found
|| closed_via_lazy
|| (ancestor_has_blockquote && closed_via_blank);
if trim_trailing {
self.tree[block_node].item.body = ItemBody::HtmlBlock(true);
}
self.pop(end_ix);
ix
}
fn parse_html_block_type_6_or_7(
&mut self,
start_ix: usize,
mut remaining_space: usize,
mut indent: usize,
) -> usize {
self.list_interrupted_paragraph = false;
self.tree.append(Item {
start: start_ix,
end: 0, body: ItemBody::HtmlBlock(true),
});
self.tree.push();
let bytes = self.text.as_bytes();
let mut ix = start_ix;
let end_ix;
loop {
let line_start_ix = ix;
ix += scan_nextline(&bytes[ix..]);
self.append_html_line(remaining_space.max(indent), line_start_ix, ix);
let mut line_start = LineStart::new(&bytes[ix..]);
let n_containers = scan_containers(&self.tree, &mut line_start, self.options);
if n_containers < self.tree.spine_len() || line_start.is_at_eol() {
end_ix = ix;
break;
}
let next_line_ix = ix + line_start.bytes_scanned();
if next_line_ix == self.text.len() || scan_blank_line(&bytes[next_line_ix..]).is_some()
{
end_ix = next_line_ix;
break;
}
if self.at_closing_directive_fence(&line_start) {
end_ix = ix;
break;
}
ix = next_line_ix;
remaining_space = line_start.remaining_space();
indent = 0;
}
self.pop(end_ix);
ix
}
fn parse_indented_code_block(
&mut self,
line_start_ix: usize,
start_ix: usize,
mut remaining_space: usize,
) -> usize {
let lazy_one_line = self.pending_lazy_blockquote_close;
self.pending_lazy_blockquote_close = false;
self.tree.append(Item {
start: line_start_ix,
end: 0, body: ItemBody::IndentCodeBlock(lazy_one_line),
});
self.tree.push();
let bytes = self.text.as_bytes();
let mut last_nonblank_child = None;
let mut last_nonblank_ix = 0;
let mut end_ix = 0;
self.last_line_blank = false;
let mut ix = start_ix;
loop {
let line_start_ix = ix;
ix += scan_nextline(&bytes[ix..]);
self.append_code_text(remaining_space, line_start_ix, ix);
if !self.last_line_blank {
last_nonblank_child = self.tree.cur();
last_nonblank_ix = ix;
end_ix = ix;
}
if lazy_one_line {
break;
}
let mut line_start = LineStart::new(&bytes[ix..]);
let n_containers = scan_containers(&self.tree, &mut line_start, self.options);
if n_containers < self.tree.spine_len()
|| !(line_start.scan_space(4) || line_start.is_at_eol())
{
break;
}
let next_line_ix = ix + line_start.bytes_scanned();
if next_line_ix == self.text.len() {
break;
}
ix = next_line_ix;
remaining_space = line_start.remaining_space();
self.last_line_blank = scan_eol(&bytes[ix..]).is_some();
}
if let Some(child) = last_nonblank_child {
self.tree[child].next = None;
self.tree[child].item.end = last_nonblank_ix;
}
self.pop(end_ix);
if !lazy_one_line {
self.list_interrupted_paragraph = true;
}
ix
}
fn parse_fenced_code_block(
&mut self,
start_ix: usize,
indent: usize,
fence_ch: u8,
n_fence_char: usize,
) -> usize {
self.list_interrupted_paragraph = false;
let bytes = self.text.as_bytes();
let mut info_start = start_ix + n_fence_char;
info_start += scan_space_or_tab(&bytes[info_start..]);
let mut ix = info_start + scan_nextline(&bytes[info_start..]);
let info_end = ix - scan_rev_while(&bytes[info_start..ix], |b| b == b'\n' || b == b'\r');
let raw_info = &self.text[info_start..info_end];
let raw_lang_end = raw_info.find([' ', '\t']).map_or(raw_info.len(), |ix| ix);
let in_table = self.tree.is_in_table();
let lang_len = unescape(&raw_info[..raw_lang_end], in_table).len() as u32;
let info_string = unescape(raw_info, in_table);
self.tree.append(Item {
start: start_ix,
end: 0, body: ItemBody::FencedCodeBlock(self.allocs.allocate_cow(info_string), lang_len),
});
self.tree.push();
loop {
if ix >= bytes.len() {
self.pop(ix);
return ix;
}
let mut line_start = LineStart::new(&bytes[ix..]);
let n_containers = scan_containers(&self.tree, &mut line_start, self.options);
if n_containers < self.tree.spine_len() {
let next_line_ix = ix + line_start.bytes_scanned();
let rest = &bytes[next_line_ix..];
let line_starts_container =
scan_blockquote_start(rest).is_some() || scan_listitem(rest).is_some();
let extra = if line_starts_container { 0 } else { 1 };
if extra > 0 {
trim_trailing_newlines_from_code_block(&mut self.tree, bytes, extra);
}
self.pop(ix);
return ix;
}
if self.options.contains(Options::ENABLE_MDX) {
let mut close_line_start = line_start.clone();
close_line_start.scan_all_space();
let close_ix = ix + close_line_start.bytes_scanned();
if let Some(n) = scan_closing_code_fence(&bytes[close_ix..], fence_ch, n_fence_char)
{
ix = close_ix + n;
self.pop(ix);
return ix + scan_blank_line(&bytes[ix..]).unwrap_or(0);
}
line_start.scan_space(indent);
} else {
line_start.scan_space(indent);
let mut close_line_start = line_start.clone();
if !close_line_start.scan_space(4 - indent) {
let close_ix = ix + close_line_start.bytes_scanned();
if let Some(n) =
scan_closing_code_fence(&bytes[close_ix..], fence_ch, n_fence_char)
{
ix = close_ix + n;
self.pop(ix);
return ix + scan_blank_line(&bytes[ix..]).unwrap_or(0);
}
}
}
let remaining_space = line_start.remaining_space();
ix += line_start.bytes_scanned();
let next_ix = ix + scan_nextline(&bytes[ix..]);
self.append_code_text(remaining_space, ix, next_ix);
ix = next_ix;
}
}
fn parse_math_block(&mut self, start_ix: usize, indent: usize, n_fence_char: usize) -> usize {
self.list_interrupted_paragraph = false;
let bytes = self.text.as_bytes();
let mut meta_start = start_ix + n_fence_char;
meta_start += scan_space_or_tab(&bytes[meta_start..]);
let mut ix = meta_start + scan_nextline(&bytes[meta_start..]);
let meta_end = ix - scan_rev_while(&bytes[meta_start..ix], |c| c == b'\n' || c == b'\r');
let meta_string = if meta_start < meta_end {
unescape(&self.text[meta_start..meta_end], self.tree.is_in_table())
} else {
"".into()
};
self.tree.append(Item {
start: start_ix,
end: 0,
body: ItemBody::MathBlock(self.allocs.allocate_cow(meta_string)),
});
self.tree.push();
loop {
if ix >= bytes.len() {
self.pop(ix);
return ix;
}
let mut line_start = LineStart::new(&bytes[ix..]);
let n_containers = scan_containers(&self.tree, &mut line_start, self.options);
if n_containers < self.tree.spine_len() {
self.pop(ix);
return ix;
}
line_start.scan_space(indent);
let mut close_line_start = line_start.clone();
if !close_line_start.scan_space(4 - indent) {
let close_ix = ix + close_line_start.bytes_scanned();
if let Some(n) = scan_closing_math_fence(&bytes[close_ix..], n_fence_char) {
ix = close_ix + n;
self.pop(ix);
return ix + scan_blank_line(&bytes[ix..]).unwrap_or(0);
}
}
let remaining_space = line_start.remaining_space();
let content_start = ix + line_start.bytes_scanned();
let next_ix = content_start + scan_nextline(&bytes[content_start..]);
let line_is_blank = content_start + 1 == next_ix
&& matches!(bytes[content_start], b'\n' | b'\r')
|| content_start == next_ix;
if line_is_blank && next_ix < bytes.len() {
let mut peek_ls = LineStart::new(&bytes[next_ix..]);
let peek_n = scan_containers(&self.tree, &mut peek_ls, self.options);
if peek_n < self.tree.spine_len() {
self.pop(ix);
return ix;
}
}
self.append_code_text(remaining_space, content_start, next_ix);
ix = next_ix;
}
}
fn parse_metadata_block(&mut self, start_ix: usize, metadata_block_ch: u8) -> usize {
let bytes = self.text.as_bytes();
let metadata_block_kind = match metadata_block_ch {
b'-' => MetadataBlockKind::YamlStyle,
b'+' => MetadataBlockKind::PlusesStyle,
_ => panic!("Erroneous metadata block character when parsing metadata block"),
};
let mut ix = start_ix + 3 + scan_nextline(&bytes[start_ix + 3..]);
self.tree.append(Item {
start: start_ix,
end: 0, body: ItemBody::MetadataBlock(metadata_block_kind),
});
self.tree.push();
loop {
let mut line_start = LineStart::new(&bytes[ix..]);
let n_containers = scan_containers(&self.tree, &mut line_start, self.options);
if n_containers < self.tree.spine_len() {
break;
}
if let (_, 0) = calc_indent(&bytes[ix..], 4)
&& let Some(n) = scan_closing_metadata_block(&bytes[ix..], metadata_block_ch)
{
ix += n;
break;
}
let remaining_space = line_start.remaining_space();
ix += line_start.bytes_scanned();
let next_ix = ix + scan_nextline(&bytes[ix..]);
if remaining_space > 0 {
let cow_ix = self.allocs.allocate_cow(" "[..remaining_space].into());
self.tree.append(Item {
start: ix,
end: ix,
body: ItemBody::SynthesizeText(cow_ix),
});
}
self.tree.append_text(ix, next_ix, false);
ix = next_ix;
}
self.pop(ix);
ix + scan_blank_line(&bytes[ix..]).unwrap_or(0)
}
fn append_code_text(&mut self, remaining_space: usize, start: usize, end: usize) {
if remaining_space > 0 {
let cow_ix = self.allocs.allocate_cow(" "[..remaining_space].into());
self.tree.append(Item {
start,
end: start,
body: ItemBody::SynthesizeText(cow_ix),
});
}
self.tree.append_text(start, end, false);
}
fn append_html_line(&mut self, remaining_space: usize, start: usize, end: usize) {
if remaining_space > 0 {
let cow_ix = self.allocs.allocate_cow(" "[..remaining_space].into());
self.tree.append(Item {
start,
end: start,
body: ItemBody::SynthesizeText(cow_ix),
});
}
self.tree.append(Item {
start,
end,
body: ItemBody::Html,
});
}
fn pop(&mut self, ix: usize) {
let cur_ix = self.tree.pop().unwrap();
self.tree[cur_ix].item.end = ix;
if let ItemBody::DefinitionList(_) = self.tree[cur_ix].item.body {
fixup_end_of_definition_list(&mut self.tree, cur_ix);
self.begin_list_item = None;
}
if let ItemBody::List(true, _, _) | ItemBody::DefinitionList(true) =
self.tree[cur_ix].item.body
{
surgerize_tight_list(&mut self.tree, cur_ix);
self.begin_list_item = None;
}
}
fn finish_list(&mut self, ix: usize) {
self.finish_empty_list_item();
if let Some(node_ix) = self.tree.peek_up()
&& let ItemBody::List(_, _, _) | ItemBody::DefinitionList(_) =
self.tree[node_ix].item.body
{
self.pop(ix);
}
if self.last_line_blank {
if let Some(node_ix) = self.tree.peek_grandparent()
&& let ItemBody::List(ref mut is_tight, _, _)
| ItemBody::DefinitionList(ref mut is_tight) = self.tree[node_ix].item.body
{
*is_tight = false;
}
self.last_line_blank = false;
}
}
fn mark_enclosing_listitem_spread(&mut self) {
let spine: Vec<TreeIndex> = self.tree.walk_spine().copied().collect();
for node_ix in spine.into_iter().rev() {
if let ItemBody::ListItem(indent, _) = self.tree[node_ix].item.body {
self.tree[node_ix].item.body = ItemBody::ListItem(indent, true);
return;
}
}
}
fn finish_empty_list_item(&mut self) {
if let Some(begin_list_item) = self.begin_list_item
&& self.last_line_blank
{
if let Some(node_ix) = self.tree.peek_up()
&& let ItemBody::ListItem(_, _) | ItemBody::DefinitionListDefinition(..) =
self.tree[node_ix].item.body
{
self.pop(begin_list_item);
}
}
self.begin_list_item = None;
}
fn continue_list(&mut self, start: usize, ch: u8, index: u64) {
self.finish_empty_list_item();
if let Some(node_ix) = self.tree.peek_up() {
if let ItemBody::List(ref mut is_tight, existing_ch, _) = self.tree[node_ix].item.body
&& existing_ch == ch
{
if self.last_line_blank {
*is_tight = false;
self.last_line_blank = false;
}
return;
}
self.finish_list(start);
}
self.tree.append(Item {
start,
end: 0, body: ItemBody::List(true, ch, index),
});
self.tree.push();
self.last_line_blank = false;
}
fn parse_hrule(&mut self, hrule_size: usize, ix: usize) -> usize {
self.tree.append(Item {
start: ix,
end: ix + hrule_size,
body: ItemBody::Rule,
});
self.list_interrupted_paragraph = false;
ix + hrule_size
}
fn parse_atx_heading(&mut self, start: usize, atx_level: HeadingLevel) -> usize {
self.list_interrupted_paragraph = false;
let mut ix = start;
let heading_ix = self.tree.append(Item {
start,
end: 0, body: ItemBody::default(), });
ix += atx_level as usize;
let bytes = self.text.as_bytes();
if let Some(eol_bytes) = scan_eol(&bytes[ix..]) {
self.tree[heading_ix].item.end = ix + eol_bytes;
self.tree[heading_ix].item.body = ItemBody::Heading(atx_level, None);
return ix + eol_bytes;
}
let skip_spaces = scan_space_or_tab(&bytes[ix..]);
ix += skip_spaces;
let header_start = ix;
let header_node_idx = self.tree.push();
let attr_block = if self.options.contains(Options::ENABLE_HEADING_ATTRIBUTES) {
let header_end = header_start + scan_nextline(&bytes[header_start..]);
self.extract_and_parse_heading_attribute_block(header_start, header_end)
.map(|block| (block, header_end))
} else {
None
};
let (end, content_end, attrs) = if let Some((block, header_end)) = attr_block {
self.parse_line(ix, Some(block.block_open), TableParseMode::Disabled, ix);
let content_end = match block.tail_start {
Some(tail_start) => {
self.parse_line(
tail_start,
Some(header_end),
TableParseMode::Disabled,
tail_start,
);
header_end
}
None => block.block_open,
};
(header_end, content_end, Some(block.attrs))
} else {
let line_end = if self.options.contains(Options::ENABLE_MDX) {
Some(header_start + scan_nextline(&bytes[header_start..]))
} else {
None
};
let (line_ix, line_brk) = self.parse_line(ix, line_end, TableParseMode::Disabled, ix);
ix = line_ix;
if let Some(Item {
start,
end,
body: ItemBody::HardBreak(true),
}) = line_brk
{
self.tree.append_text(start, end, false);
}
(ix, ix, None)
};
self.tree[header_node_idx].item.end = end;
let mut empty_text_node = false;
if let Some(cur_ix) = self.tree.cur() {
let header_text = &bytes[header_start..content_end];
let mut limit = header_text
.iter()
.rposition(|&b| !(b == b'\n' || b == b'\r' || b == b' ' || b == b'\t'))
.map_or(0, |i| i + 1);
let closer = header_text[..limit]
.iter()
.rposition(|&b| b != b'#')
.map_or(0, |i| i + 1);
if closer == 0 {
limit = closer;
} else {
let spaces = scan_rev_while(&header_text[..closer], |b| b == b' ' || b == b'\t');
if spaces > 0 {
limit = closer - spaces;
}
}
self.tree[cur_ix].item.end = limit + header_start;
if limit == 0 {
empty_text_node = true;
}
}
if empty_text_node {
self.tree.remove_node();
} else {
self.tree.pop();
}
self.tree[heading_ix].item.body = ItemBody::Heading(
atx_level,
attrs.map(|attrs| self.allocs.allocate_heading(attrs)),
);
end
}
fn parse_footnote(&mut self, start: usize) -> Option<usize> {
let bytes = &self.text.as_bytes()[start..];
if !bytes.starts_with(b"[^") {
return None;
}
let (mut i, label) =
scan_link_label_rest(&self.text[start + 2..], &|_| None, self.tree.is_in_table())?;
let raw_label = &self.text.as_bytes()[start + 2..start + 2 + i.saturating_sub(1)];
if raw_label.is_empty()
|| raw_label
.iter()
.any(|&b| b == b' ' || b == b'\t' || b == b'\r' || b == b'\n')
{
return None;
}
i += 2;
if bytes.get(i) != Some(&b':') {
return None;
}
i += 1;
self.finish_list(start);
if let Some(node_ix) = self.tree.peek_up()
&& let ItemBody::FootnoteDefinition(..) = self.tree[node_ix].item.body
{
self.pop(start);
}
i += scan_space_or_tab(&bytes[i..]);
self.allocs
.footdefs
.0
.insert(UniCase::new(label.clone()), FootnoteDef { use_count: 0 });
self.tree.append(Item {
start,
end: 0, body: ItemBody::FootnoteDefinition(self.allocs.allocate_cow(label)),
});
self.tree.push();
Some(i)
}
fn parse_refdef_label(&self, start: usize) -> Option<(usize, CowStr<'a>)> {
scan_link_label_rest(
&self.text[start..],
&|bytes| {
let mut line_start = LineStart::new(bytes);
let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
if line_start.scan_space(4) {
return Some(line_start.bytes_scanned());
}
let bytes_scanned = line_start.bytes_scanned();
let suffix = &bytes[bytes_scanned..];
if self.scan_paragraph_interrupt(suffix, current_container, tree_position)
|| (current_container && scan_setext_heading(suffix).is_some())
{
None
} else {
Some(bytes_scanned)
}
},
self.tree.is_in_table(),
)
}
fn parse_refdef_total(&mut self, start: usize) -> Option<(usize, LinkLabel<'a>, LinkDef<'a>)> {
let bytes = &self.text.as_bytes()[start..];
if bytes.first() != Some(&b'[') {
return None;
}
let (mut i, label) = self.parse_refdef_label(start + 1)?;
i += 1;
if bytes.get(i) != Some(&b':') {
return None;
}
i += 1;
let (bytecount, link_def) = self.scan_refdef(start, start + i)?;
Some((bytecount + i, UniCase::new(label), link_def))
}
fn scan_refdef_space(&self, bytes: &[u8], mut i: usize) -> Option<(usize, usize)> {
let mut newlines = 0;
loop {
let whitespaces = scan_space_or_tab(&bytes[i..]);
i += whitespaces;
if let Some(eol_bytes) = scan_eol(&bytes[i..]) {
i += eol_bytes;
newlines += 1;
if newlines > 1 {
return None;
}
} else {
break;
}
let mut line_start = LineStart::new(&bytes[i..]);
let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
if !line_start.scan_space(4) {
let suffix = &bytes[i + line_start.bytes_scanned()..];
if self.scan_paragraph_interrupt(suffix, current_container, tree_position)
|| scan_setext_heading(suffix).is_some()
{
return None;
}
}
i += line_start.bytes_scanned();
}
Some((i, newlines))
}
fn scan_refdef_title<'t>(&self, text: &'t str) -> Option<(usize, CowStr<'t>)> {
let bytes = text.as_bytes();
let closing_delim = match bytes.first()? {
b'\'' => b'\'',
b'"' => b'"',
b'(' => b')',
_ => return None,
};
let mut bytecount = 1;
let mut linestart = 1;
let mut linebuf = None;
while let Some(&c) = bytes.get(bytecount) {
match c {
b'\n' | b'\r' => {
let linebuf = if let Some(linebuf) = &mut linebuf {
linebuf
} else {
linebuf = Some(String::new());
linebuf.as_mut().unwrap()
};
linebuf.push_str(&text[linestart..bytecount]);
linebuf.push(c as char);
bytecount += 1;
if c == b'\r' && bytes.get(bytecount) == Some(&b'\n') {
linebuf.push('\n');
bytecount += 1;
}
let mut line_start = LineStart::new(&bytes[bytecount..]);
let tree_position = scan_containers(&self.tree, &mut line_start, self.options);
let current_container = tree_position == self.tree.spine_len();
if !line_start.scan_space(4) {
let suffix = &bytes[bytecount + line_start.bytes_scanned()..];
if self.scan_paragraph_interrupt(suffix, current_container, tree_position)
|| scan_setext_heading(suffix).is_some()
{
return None;
}
}
line_start.scan_all_space();
bytecount += line_start.bytes_scanned();
linestart = bytecount;
if scan_blank_line(&bytes[bytecount..]).is_some() {
return None;
}
}
b'\\' => {
bytecount += 1;
if let Some(c) = bytes.get(bytecount)
&& c != &b'\r'
&& c != &b'\n'
{
bytecount += 1;
}
}
c if c == closing_delim => {
let cow = if let Some(mut linebuf) = linebuf {
linebuf.push_str(&text[linestart..bytecount]);
CowStr::from(linebuf)
} else {
CowStr::from(&text[linestart..bytecount])
};
return Some((bytecount + 1, cow));
}
_ => {
bytecount += 1;
}
}
}
None
}
fn scan_refdef(&self, span_start: usize, start: usize) -> Option<(usize, LinkDef<'a>)> {
let bytes = self.text.as_bytes();
let (mut i, _newlines) = self.scan_refdef_space(bytes, start)?;
let (dest_length, dest) = scan_link_dest(self.text, i, LINK_MAX_NESTED_PARENS)?;
if dest_length == 0 {
return None;
}
let dest = unescape(dest, self.tree.is_in_table());
i += dest_length;
let span_end = i + scan_space_or_tab(&bytes[i..]);
let mut backup = (
span_end - start,
LinkDef {
dest,
title: None,
span: span_start..span_end,
},
);
let (mut i, newlines) =
if let Some((new_i, mut newlines)) = self.scan_refdef_space(bytes, i) {
if i == self.text.len() {
newlines += 1;
}
if new_i == i && newlines == 0 {
return None;
}
if newlines > 1 {
return Some(backup);
};
(new_i, newlines)
} else {
return Some(backup);
};
if let Some((title_length, title)) = self.scan_refdef_title(&self.text[i..]) {
i += title_length;
if scan_blank_line(&bytes[i..]).is_some() {
let span_end = i + scan_space_or_tab(&bytes[i..]);
backup.0 = i - start;
backup.1.span = span_start..span_end;
backup.1.title = Some(unescape(title, self.tree.is_in_table()));
return Some(backup);
}
}
if newlines > 0 { Some(backup) } else { None }
}
fn scan_paragraph_interrupt(
&self,
bytes: &[u8],
current_container: bool,
tree_position: usize,
) -> bool {
#[cfg(feature = "mdx")]
if self.options.contains(Options::ENABLE_MDX)
&& bytes.starts_with(b"<")
&& self
.scan_mdx_flow_in_container_bytes(bytes, |b, c| scan_mdx_jsx_block(b, c))
.is_some()
{
return true;
}
if scan_paragraph_interrupt_no_table(
bytes,
current_container,
self.options.contains(Options::ENABLE_FOOTNOTES),
self.options.contains(Options::ENABLE_DEFINITION_LIST),
self.options.contains(Options::ENABLE_MDX),
self.options.contains(Options::ENABLE_MATH_MULTI_DOLLAR),
self.options.contains(Options::ENABLE_DIRECTIVE),
&self.tree,
tree_position,
) {
return true;
}
if !self.options.contains(Options::ENABLE_TABLES) {
return false;
}
if !bytes.starts_with(b"|") && !current_container {
return false;
}
let Some(eol_off) = memchr::memchr2(b'\n', b'\r', bytes) else {
return false;
};
let next_line_ix = eol_off + scan_eol(&bytes[eol_off..]).unwrap();
if memchr::memchr3(b'-', b'\n', b'\r', &bytes[next_line_ix..])
.is_none_or(|p| bytes[next_line_ix + p] != b'-')
{
return false;
}
let mut line_start = LineStart::new(&bytes[next_line_ix..]);
if scan_containers(&self.tree, &mut line_start, self.options) != self.tree.spine_len() {
return false;
}
if self.options.contains(Options::ENABLE_MDX) {
line_start.scan_all_space();
}
let table_head_ix = next_line_ix + line_start.bytes_scanned();
let (table_head_bytes, alignment) = scan_table_head(&bytes[table_head_ix..]);
if table_head_bytes == 0 {
return false;
}
let mut pipes = 0;
let mut bsesc = false;
let mut last_pipe_ix = 0;
for (i, &byte) in bytes[..eol_off].iter().enumerate() {
match byte {
b'\\' => {
bsesc = !bsesc;
continue;
}
b'|' if !bsesc => {
pipes += 1;
last_pipe_ix = i;
}
_ => {}
}
bsesc = false;
}
let header_count = count_header_cols(bytes, pipes, 0, last_pipe_ix);
alignment.len() == header_count
}
fn extract_and_parse_heading_attribute_block(
&mut self,
header_start: usize,
header_end: usize,
) -> Option<HeadingAttrBlock<'a>> {
if !self.options.contains(Options::ENABLE_HEADING_ATTRIBUTES) {
return None;
}
let bytes = self.text.as_bytes();
let search_end = if self.options.contains(Options::ENABLE_DIRECTIVE) {
self.heading_attr_block_search_end(header_start, header_end)
} else {
header_end
};
let header_bytes = &bytes[header_start..search_end];
let (content_len, attr_block_range_rel) =
extract_attribute_block_content_from_header_text(header_bytes);
let range = attr_block_range_rel?;
#[cfg(feature = "mdx")]
if self.options.contains(Options::ENABLE_MDX) {
let inner_start = header_start + range.start;
let inner_end = header_start + range.end;
crate::mdx::try_parse_expression_body(
&self.text[inner_start..inner_end],
&mut self.mdx_expr_allocator,
)?;
}
let attrs = parse_inside_attribute_block(
&self.text[(header_start + range.start)..(header_start + range.end)],
)?;
let block_open = header_start + content_len;
let block_close = header_start + range.end + 1;
let tail_has_content = bytes[block_close..header_end]
.iter()
.any(|&b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r'));
Some(HeadingAttrBlock {
attrs,
block_open,
tail_start: if tail_has_content {
Some(block_close)
} else {
None
},
})
}
fn heading_attr_block_search_end(&self, header_start: usize, header_end: usize) -> usize {
let bytes = self.text.as_bytes();
let mut spans: Vec<(usize, usize)> = Vec::new();
let mut i = header_start;
while i < header_end {
if bytes[i] == b':'
&& let Some(end) = scan_heading_text_directive(self.text, bytes, i, header_end)
{
spans.push((i, end));
i = end;
continue;
}
i += 1;
}
let mut pos = header_end;
pos -= scan_rev_while(&bytes[header_start..pos], |b| {
matches!(b, b' ' | b'\t' | b'\n' | b'\r')
});
for &(start, end) in spans.iter().rev() {
if end != pos {
break;
}
pos = start;
pos -= scan_rev_while(&bytes[header_start..pos], |b| {
matches!(b, b' ' | b'\t' | b'\n' | b'\r')
});
}
pos
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
enum TableParseMode {
Scan,
Active,
Disabled,
}
fn count_header_cols(
bytes: &[u8],
mut pipes: usize,
mut start: usize,
last_pipe_ix: usize,
) -> usize {
if pipes == 0 {
return 1;
}
start += scan_space_or_tab(&bytes[start..]);
if bytes[start] == b'|' {
pipes -= 1;
}
if scan_blank_line(&bytes[(last_pipe_ix + 1)..]).is_some() {
pipes
} else {
pipes + 1
}
}
#[cfg(feature = "mdx")]
fn mdx_block_interrupts(bytes: &[u8], mdx: bool) -> bool {
(mdx && bytes.starts_with(b"<") && scan_mdx_jsx_block(bytes, None).is_some())
|| (mdx && bytes.starts_with(b"{") && scan_mdx_expression_block(bytes, None).is_some())
}
#[cfg(not(feature = "mdx"))]
fn mdx_block_interrupts(_bytes: &[u8], _mdx: bool) -> bool {
false
}
#[allow(clippy::too_many_arguments)]
fn scan_paragraph_interrupt_no_table(
bytes: &[u8],
current_container: bool,
has_footnote: bool,
definition_list: bool,
mdx: bool,
math: bool,
directive: bool,
tree: &Tree<Item>,
tree_position: usize,
) -> bool {
scan_eol(bytes).is_some()
|| scan_hrule(bytes).is_ok()
|| scan_atx_heading(bytes).is_some()
|| scan_code_fence(bytes).is_some()
|| (math && scan_math_fence(bytes).is_some())
|| (directive && scan_interrupting_container_extensions_fence(bytes))
|| scan_blockquote_start(bytes).is_some()
|| scan_listitem(bytes).is_some_and(|(ix, delim, _index, _)| {
! current_container ||
tree.is_in_table() ||
(delim == b'*' || delim == b'-' || delim == b'+'
|| (bytes.first() == Some(&b'1')
&& matches!(bytes.get(1), Some(b'.') | Some(b')'))))
&& (scan_blank_line(&bytes[ix..]).is_none())
})
|| (!mdx
&& bytes.starts_with(b"<")
&& (get_html_end_tag(&bytes[1..]).is_some() || starts_html_block_type_6(&bytes[1..])))
|| (!mdx
&& !current_container
&& bytes.starts_with(b"<")
&& scan_html_type_7(bytes).is_some())
|| mdx_block_interrupts(bytes, mdx)
|| definition_list
&& ((current_container
&& tree.peek_up().is_some_and(|cur| {
matches!(
tree[cur].item.body,
ItemBody::Paragraph
| ItemBody::TightParagraph
| ItemBody::MaybeDefinitionListTitle
)
}))
|| tree.walk_spine().nth(tree_position).is_some_and(|cur| {
matches!(tree[*cur].item.body, ItemBody::DefinitionListDefinition(..))
}))
&& is_definition_list_marker(bytes)
|| (has_footnote
&& bytes.starts_with(b"[^")
&& scan_link_label_rest(
core::str::from_utf8(&bytes[2..]).unwrap(),
&|_| None,
tree.is_in_table(),
)
.is_some_and(|(len, _)| bytes.get(2 + len) == Some(&b':')))
}
fn clamp_scope_to_math_fences(
bytes: &[u8],
pos: usize,
para_start: usize,
para_end: usize,
) -> (usize, usize) {
let mut lo = para_start;
let mut hi = para_end;
let mut line_start = para_start;
while line_start < para_end {
let mut line_end = line_start;
while line_end < para_end && !matches!(bytes[line_end], b'\n' | b'\r') {
line_end += 1;
}
let mut content = line_start;
while content < line_end && bytes[content] == b' ' && content - line_start < 3 {
content += 1;
}
let mut next_line = line_end;
if bytes.get(next_line) == Some(&b'\r') {
next_line += 1;
}
if bytes.get(next_line) == Some(&b'\n') {
next_line += 1;
}
if scan_math_fence(&bytes[content..line_end]).is_some() {
if line_start > pos {
hi = line_start;
break;
}
if line_end <= pos {
lo = next_line;
}
}
line_start = next_line;
}
(lo, hi)
}
fn is_inside_math_span(bytes: &[u8], pos: usize) -> bool {
let (para_start, para_end) = scope_for_inline(bytes, pos);
if pos <= para_start
|| pos >= para_end
|| memchr::memchr(b'$', &bytes[para_start..pos]).is_none()
|| memchr::memchr(b'$', &bytes[pos..para_end]).is_none()
{
return false;
}
let (para_start, para_end) = clamp_scope_to_math_fences(bytes, pos, para_start, para_end);
let mut runs: Vec<(usize, usize, bool)> = Vec::with_capacity(4);
let mut i = para_start;
while i < para_end {
if bytes[i] == b'$' {
let start = i;
while i < para_end && bytes[i] == b'$' {
i += 1;
}
let mut backslashes = 0usize;
let mut k = start;
while k > para_start && bytes[k - 1] == b'\\' {
backslashes += 1;
k -= 1;
}
runs.push((start, i - start, backslashes % 2 == 1));
} else {
i += 1;
}
}
if runs.len() < 2 {
return false;
}
let mut paired = vec![false; runs.len()];
for a in 0..runs.len() {
if paired[a] || runs[a].2 {
continue;
}
for b in (a + 1)..runs.len() {
if paired[b] {
continue;
}
if runs[b].1 == runs[a].1 {
paired[a] = true;
paired[b] = true;
let span_start = runs[a].0 + runs[a].1;
let span_end = runs[b].0;
if pos >= span_start && pos < span_end {
return true;
}
break;
}
}
}
false
}
fn is_inside_code_span(bytes: &[u8], pos: usize) -> bool {
let (para_start, para_end) = scope_for_inline(bytes, pos);
if pos <= para_start
|| pos >= para_end
|| memchr::memchr(b'`', &bytes[para_start..pos]).is_none()
|| memchr::memchr(b'`', &bytes[pos..para_end]).is_none()
{
return false;
}
let mut bracket_matched = vec![false; para_end - para_start];
{
let mut stack: Vec<usize> = Vec::new();
let mut k = para_start;
while k < para_end {
if bytes[k] == b'\\' && k + 1 < para_end {
k += 2;
continue;
}
match bytes[k] {
b'[' => stack.push(k),
b']' => {
if let Some(open) = stack.pop() {
bracket_matched[open - para_start] = true;
bracket_matched[k - para_start] = true;
}
}
_ => {}
}
k += 1;
}
}
let mut runs: Vec<(usize, usize, i32)> = Vec::with_capacity(8);
let mut bracket_depth: i32 = 0;
let mut i = para_start;
while i < para_end {
if bytes[i] == b'\\' && i + 1 < para_end {
i += 2;
continue;
}
match bytes[i] {
b'[' if bracket_matched[i - para_start] => {
bracket_depth += 1;
i += 1;
}
b']' if bracket_depth > 0 && bracket_matched[i - para_start] => {
bracket_depth -= 1;
i += 1;
}
b'`' => {
let start = i;
while i < para_end && bytes[i] == b'`' {
i += 1;
}
runs.push((start, i - start, bracket_depth));
}
_ => i += 1,
}
}
if runs.len() < 2 {
return false;
}
let mut paired = vec![false; runs.len()];
for a in 0..runs.len() {
if paired[a] {
continue;
}
for b in (a + 1)..runs.len() {
if paired[b] {
continue;
}
if runs[b].1 == runs[a].1 && runs[b].2 == runs[a].2 {
paired[a] = true;
paired[b] = true;
let span_start = runs[a].0 + runs[a].1;
let span_end = runs[b].0;
if pos >= span_start && pos < span_end {
return true;
}
break;
}
}
}
false
}
fn scope_for_inline(bytes: &[u8], pos: usize) -> (usize, usize) {
let cur_line_start = {
let mut j = pos;
while j > 0 && !matches!(bytes[j - 1], b'\n' | b'\r') {
j -= 1;
}
j
};
let cur_prefix = leading_container_prefix(&bytes[cur_line_start..]);
let start = scope_start_with_prefix(bytes, cur_line_start, cur_prefix);
let end = scope_end_with_prefix(bytes, cur_line_start, cur_prefix);
(start, end)
}
fn leading_container_prefix(line: &[u8]) -> usize {
let mut depth = 0;
let mut i = 0;
loop {
let mut spaces = 0;
while i < line.len() && line[i] == b' ' && spaces < 3 {
i += 1;
spaces += 1;
}
if i < line.len() && line[i] == b'>' {
depth += 1;
i += 1;
if i < line.len() && (line[i] == b' ' || line[i] == b'\t') {
i += 1;
}
} else {
break;
}
}
depth
}
fn line_opens_list_item(bytes: &[u8], line_start: usize) -> bool {
let mut i = line_start;
while i < bytes.len() && bytes[i] == b' ' && i - line_start < 3 {
i += 1;
}
scan_listitem(&bytes[i..]).is_some()
}
fn scope_start_with_prefix(bytes: &[u8], cur_line_start: usize, prefix: usize) -> usize {
if line_opens_list_item(bytes, cur_line_start) {
return cur_line_start;
}
let mut line_start = cur_line_start;
while line_start > 0 {
let mut prev_end = line_start - 1;
if prev_end > 0 && bytes[prev_end] == b'\n' && bytes[prev_end - 1] == b'\r' {
prev_end -= 1;
}
let mut prev_start = prev_end;
while prev_start > 0 && !matches!(bytes[prev_start - 1], b'\n' | b'\r') {
prev_start -= 1;
}
let prev_line = &bytes[prev_start..prev_end];
if prev_line.iter().all(|&b| b == b' ' || b == b'\t') {
return line_start;
}
if leading_container_prefix(prev_line) != prefix {
return line_start;
}
line_start = prev_start;
if line_opens_list_item(bytes, line_start) {
return line_start;
}
}
line_start
}
fn scope_end_with_prefix(bytes: &[u8], cur_line_start: usize, prefix: usize) -> usize {
let mut i = cur_line_start;
while i < bytes.len() && !matches!(bytes[i], b'\n' | b'\r') {
i += 1;
}
while i < bytes.len() {
let after_eol = if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
i + 2
} else {
i + 1
};
let mut next_eol = after_eol;
while next_eol < bytes.len() && !matches!(bytes[next_eol], b'\n' | b'\r') {
next_eol += 1;
}
let next_line = &bytes[after_eol..next_eol];
if next_line.iter().all(|&b| b == b' ' || b == b'\t') {
return i;
}
if leading_container_prefix(next_line) != prefix {
return i;
}
if line_opens_list_item(bytes, after_eol) {
return i;
}
i = next_eol;
}
i
}
#[cfg(feature = "mdx")]
const LINK_LABEL_MAX_TRACKED_DEPTH: u32 = 64;
#[cfg(feature = "mdx")]
fn is_inside_link_url_parens(bytes: &[u8], pos: usize, scope_start: usize) -> bool {
let line_start = memchr::memrchr2(b'\n', b'\r', &bytes[..pos])
.map(|i| i + 1)
.unwrap_or(0);
let opens_tail = |from: usize, to: usize| {
memchr::memchr_iter(b']', &bytes[from..to])
.any(|rel| bytes.get(from + rel + 1) == Some(&b'('))
};
let on_line = opens_tail(line_start, pos);
let earlier = scope_start < line_start && opens_tail(scope_start, line_start);
if !on_line && !earlier {
return false;
}
let line_end = line_end_at(bytes, pos);
let prefix = leading_container_prefix(&bytes[line_start..]);
let widen = if on_line {
match replay_link_tails(
bytes,
pos,
line_start,
line_end,
prefix,
scope_start >= line_start,
) {
TailReplay::Inside => return true,
TailReplay::NeedsScope => true,
TailReplay::Outside => {
earlier && tail_reaches_from_line_above(bytes, line_start, pos, prefix)
}
}
} else {
tail_reaches_from_line_above(bytes, line_start, pos, prefix)
};
widen
&& scope_start < line_start
&& !earlier_lines_are_ambiguous(bytes, scope_start, line_start)
&& matches!(
replay_link_tails(bytes, pos, scope_start, line_end, prefix, true),
TailReplay::Inside
)
}
#[cfg(feature = "mdx")]
fn tail_reaches_from_line_above(
bytes: &[u8],
line_start: usize,
pos: usize,
prefix: usize,
) -> bool {
if line_start == 0 {
return false;
}
let mut eol = line_start - 1;
if eol > 0 && bytes[eol] == b'\n' && bytes[eol - 1] == b'\r' {
eol -= 1;
}
let above = memchr::memrchr2(b'\n', b'\r', &bytes[..eol])
.map(|i| i + 1)
.unwrap_or(0);
let mut closes = TitleCloses::default();
memchr::memchr_iter(b']', &bytes[above..eol]).any(|rel| {
let rbracket = above + rel;
bytes.get(rbracket + 1) == Some(&b'(')
&& link_tail_close(bytes, rbracket + 1, eol, prefix, &mut closes)
.is_some_and(|rparen| rparen > pos)
})
}
#[cfg(feature = "mdx")]
enum TailReplay {
Inside,
Outside,
NeedsScope,
}
#[cfg(feature = "mdx")]
fn replay_link_tails(
bytes: &[u8],
pos: usize,
from: usize,
scope_end: usize,
prefix: usize,
at_block_start: bool,
) -> TailReplay {
let mut depth: u32 = 0;
let mut image_kinds: u64 = 0;
let mut inactive_below: u32 = 0;
let mut closes = TitleCloses::default();
let mut needs_scope = false;
let mut i = from;
let mut tail_line_end = line_end_at(bytes, from);
while i < pos {
match bytes[i] {
b'\\' if i + 1 < scope_end && is_ascii_punctuation(bytes[i + 1]) => i += 2,
b'`' => {
needs_scope = true;
let run = 1 + scan_ch_repeat(&bytes[i + 1..scope_end], b'`');
match code_span_end(bytes, i + run, scope_end, run) {
Some(end) if end > pos => {
return if at_block_start {
TailReplay::Inside
} else {
TailReplay::NeedsScope
};
}
Some(end) => i = end,
None => i += run,
}
}
b'!' if bytes.get(i + 1) == Some(&b'[') => {
depth += 1;
if depth <= LINK_LABEL_MAX_TRACKED_DEPTH {
image_kinds |= 1 << (depth - 1);
}
i += 2;
}
b'[' => {
depth += 1;
if depth <= LINK_LABEL_MAX_TRACKED_DEPTH {
image_kinds &= !(1 << (depth - 1));
}
i += 1;
}
b']' => {
if depth == 0 {
needs_scope |= bytes.get(i + 1) == Some(&b'(');
i += 1;
continue;
}
if i >= tail_line_end {
tail_line_end = line_end_at(bytes, i);
}
let is_image =
depth <= LINK_LABEL_MAX_TRACKED_DEPTH && image_kinds & (1 << (depth - 1)) != 0;
let active = is_image || depth > inactive_below;
depth -= 1;
inactive_below = inactive_below.min(depth);
if active
&& bytes.get(i + 1) == Some(&b'(')
&& let Some(rparen) =
link_tail_close(bytes, i + 1, tail_line_end, prefix, &mut closes)
{
if rparen > pos {
return TailReplay::Inside;
}
if !is_image {
inactive_below = depth;
}
i = rparen + 1;
continue;
}
i += 1;
}
_ => i += 1,
}
}
if needs_scope {
TailReplay::NeedsScope
} else {
TailReplay::Outside
}
}
#[cfg(feature = "mdx")]
fn earlier_lines_are_ambiguous(bytes: &[u8], scope_start: usize, line_start: usize) -> bool {
if matches!(bytes.get(line_start), Some(b'{') | Some(b'<')) {
return true;
}
memchr::memchr_iter(b'<', &bytes[scope_start..line_start]).any(|rel| {
matches!(
bytes.get(scope_start + rel + 1),
Some(c) if c.is_ascii_alphabetic() || matches!(c, b'/' | b'>' | b'!' | b'?' | b'_' | b'$')
)
})
}
#[cfg(feature = "mdx")]
fn line_end_at(bytes: &[u8], i: usize) -> usize {
memchr::memchr2(b'\n', b'\r', &bytes[i..]).map_or(bytes.len(), |k| i + k)
}
#[cfg(feature = "mdx")]
fn code_span_end(bytes: &[u8], from: usize, line_end: usize, run: usize) -> Option<usize> {
let mut i = from;
while let Some(rel) = memchr::memchr(b'`', &bytes[i..line_end]) {
let start = i + rel;
let len = 1 + scan_ch_repeat(&bytes[start + 1..line_end], b'`');
if len == run {
return Some(start + len);
}
i = start + len;
}
None
}
#[cfg(feature = "mdx")]
fn link_tail_close(
bytes: &[u8],
lparen: usize,
line_end: usize,
prefix: usize,
closes: &mut TitleCloses,
) -> Option<usize> {
let (i, line_end) = skip_resource_space(bytes, lparen + 1, line_end, prefix)?;
let dest_end = scan_link_tail_dest(bytes, i, line_end)?;
let (mut i, mut line_end) = skip_resource_space(bytes, dest_end, line_end, prefix)?;
if i > dest_end
&& let Some((title_end, title_line_end)) =
scan_link_tail_title(bytes, i, line_end, prefix, closes)
{
(i, line_end) = skip_resource_space(bytes, title_end, title_line_end, prefix)?;
}
(i < line_end && bytes[i] == b')').then_some(i)
}
#[cfg(feature = "mdx")]
fn skip_resource_space(
bytes: &[u8],
mut at: usize,
mut line_end: usize,
prefix: usize,
) -> Option<(usize, usize)> {
loop {
at += scan_while(&bytes[at..line_end], is_space_or_tab);
if at < line_end {
return Some((at, line_end));
}
at = resource_continuation_line(bytes, line_end, prefix)?;
line_end = line_end_at(bytes, at);
}
}
#[cfg(feature = "mdx")]
fn resource_continuation_line(bytes: &[u8], eol: usize, prefix: usize) -> Option<usize> {
if eol >= bytes.len() {
return None;
}
let mut next = eol + 1;
if bytes[eol] == b'\r' && bytes.get(next) == Some(&b'\n') {
next += 1;
}
let mut i = next;
for _ in 0..prefix {
i += scan_while_max(&bytes[i..], |b| b == b' ', 3);
if bytes.get(i) != Some(&b'>') {
return None;
}
i += 1;
if bytes.get(i).is_some_and(|&b| is_space_or_tab(b)) {
i += 1;
}
}
let content = i + scan_while(&bytes[i..], is_space_or_tab);
match bytes.get(content) {
None | Some(b'\n' | b'\r') => None,
Some(_) if opens_block(&bytes[content..]) => None,
Some(_) => Some(i),
}
}
#[cfg(feature = "mdx")]
fn opens_block(data: &[u8]) -> bool {
let Some(&first) = data.first() else {
return true;
};
if !matches!(
first,
b'<' | b'|'
| b'{'
| b':'
| b'['
| b'#'
| b'>'
| b'*'
| b'-'
| b'_'
| b'+'
| b'='
| b'`'
| b'~'
| b'$'
| b'0'..=b'9'
) {
return false;
}
matches!(first, b'<' | b'|' | b'{' | b':')
|| data.starts_with(b"[^")
|| scan_hrule(data).is_ok()
|| scan_atx_heading(data).is_some()
|| scan_code_fence(data).is_some()
|| scan_setext_heading(data).is_some()
|| scan_blockquote_start(data).is_some()
|| scan_listitem(data).is_some()
|| scan_math_fence(data).is_some()
|| scan_interrupting_container_extensions_fence(data)
}
#[cfg(feature = "mdx")]
fn scan_link_tail_dest(bytes: &[u8], start: usize, line_end: usize) -> Option<usize> {
let mut i = start;
if bytes.get(i) == Some(&b'<') {
i += 1;
while i < line_end {
match bytes[i] {
b'<' => return None,
b'>' => return Some(i + 1),
b'\\' if i + 1 < line_end && is_ascii_punctuation(bytes[i + 1]) => i += 1,
_ => {}
}
i += 1;
}
return None;
}
let mut nest = 0;
while i < line_end {
match bytes[i] {
0x0..=0x20 => break,
b'(' => {
if nest > LINK_MAX_NESTED_PARENS {
return None;
}
nest += 1;
}
b')' => {
if nest == 0 {
break;
}
nest -= 1;
}
b'\\' if i + 1 < line_end && is_ascii_punctuation(bytes[i + 1]) => i += 1,
_ => {}
}
i += 1;
}
(nest == 0).then_some(i)
}
#[cfg(feature = "mdx")]
#[derive(Default)]
struct TitleCloses {
line_end: usize,
asked_from: [usize; 3],
found_at: [usize; 3],
}
#[cfg(feature = "mdx")]
impl TitleCloses {
fn find(
&mut self,
bytes: &[u8],
kind: usize,
close: u8,
start: usize,
line_end: usize,
) -> Option<usize> {
if self.line_end != line_end {
*self = Self {
line_end,
asked_from: [usize::MAX; 3],
found_at: [0; 3],
};
}
let cached = self.asked_from[kind];
if cached != usize::MAX && start >= cached && start <= self.found_at[kind] {
return (self.found_at[kind] < line_end).then_some(self.found_at[kind]);
}
let mut i = if memchr::memchr(close, &bytes[start..line_end]).is_some() {
let mut j = start;
while j < line_end {
if bytes[j] == close {
break;
}
if bytes[j] == b'\\' && j + 1 < line_end && is_ascii_punctuation(bytes[j + 1]) {
j += 1;
}
j += 1;
}
j
} else {
line_end
};
i = i.min(line_end);
self.asked_from[kind] = start;
self.found_at[kind] = i;
(i < line_end).then_some(i)
}
}
#[cfg(feature = "mdx")]
fn scan_link_tail_title(
bytes: &[u8],
start: usize,
line_end: usize,
prefix: usize,
closes: &mut TitleCloses,
) -> Option<(usize, usize)> {
if start >= line_end {
return None;
}
let (kind, close) = match bytes[start] {
b'\'' => (0, b'\''),
b'"' => (1, b'"'),
b'(' => (2, b')'),
_ => return None,
};
let mut from = start + 1;
let mut line_end = line_end;
loop {
if let Some(i) = closes.find(bytes, kind, close, from, line_end) {
return Some((i + 1, line_end));
}
from = resource_continuation_line(bytes, line_end, prefix)?;
line_end = line_end_at(bytes, from);
}
}
#[cfg(feature = "mdx")]
fn is_at_paragraph_line_start(bytes: &[u8], pos: usize) -> bool {
let mut j = pos;
while j > 0 && bytes[j - 1] != b'\n' && bytes[j - 1] != b'\r' {
j -= 1;
}
let line = &bytes[j..pos];
let mut k = 0;
while k < line.len() && (line[k] == b' ' || line[k] == b'\t') {
k += 1;
}
while k < line.len() && line[k] == b'>' {
k += 1;
if k < line.len() && (line[k] == b' ' || line[k] == b'\t') {
k += 1;
}
}
if k < line.len() {
let b = line[k];
let consumed = if matches!(b, b'-' | b'+' | b'*')
&& line.get(k + 1).is_some_and(|c| *c == b' ' || *c == b'\t')
{
Some(k + 2)
} else if b.is_ascii_digit() {
let mut m = k + 1;
while m < line.len() && line[m].is_ascii_digit() {
m += 1;
}
if m < line.len()
&& (line[m] == b'.' || line[m] == b')')
&& line.get(m + 1).is_some_and(|c| *c == b' ' || *c == b'\t')
{
Some(m + 2)
} else {
None
}
} else {
None
};
if let Some(after_marker) = consumed {
k = after_marker;
while k < line.len() && (line[k] == b' ' || line[k] == b'\t') {
k += 1;
}
}
}
k == line.len()
}
#[cfg(not(feature = "mdx"))]
fn prev_line_has_open_inline_jsx(_bytes: &[u8], _ix: usize, _has_math: bool) -> bool {
false
}
#[cfg(feature = "mdx")]
fn prev_line_has_open_inline_jsx(bytes: &[u8], ix: usize, has_math: bool) -> bool {
if ix == 0 || ix > bytes.len() {
return false;
}
let mut prev_line_end = ix - 1;
if !matches!(bytes[prev_line_end], b'\n' | b'\r') {
return false;
}
if prev_line_end > 0 && bytes[prev_line_end] == b'\n' && bytes[prev_line_end - 1] == b'\r' {
prev_line_end -= 1;
}
let prev_line_start =
memchr::memrchr2(b'\n', b'\r', &bytes[..prev_line_end]).map_or(0, |nl| nl + 1);
let line = &bytes[prev_line_start..prev_line_end];
if memchr::memchr2(b'<', b'\\', line).is_none() {
return false;
}
let mut offset = 0;
while offset < line.len() {
let Some(rel) = memchr::memchr2(b'<', b'\\', &line[offset..]) else {
return false;
};
let i = offset + rel;
if line[i] == b'\\' {
offset = i + 2;
continue;
}
let pos = prev_line_start + i;
if is_inside_code_span(bytes, pos) || (has_math && is_inside_math_span(bytes, pos)) {
offset = i + 1;
continue;
}
if let Some(len) = crate::mdx::scan_mdx_inline_jsx(&bytes[pos..])
&& pos + len > ix
{
return true;
}
offset = i + 1;
}
false
}
#[cfg(not(feature = "mdx"))]
fn is_inside_open_inline_jsx_tag(_bytes: &[u8], _pos: usize) -> bool {
false
}
#[cfg(feature = "mdx")]
fn is_inside_open_inline_jsx_tag(bytes: &[u8], pos: usize) -> bool {
if pos == 0 || pos > bytes.len() {
return false;
}
let mut line_start = pos;
let mut seen_newline = false;
while line_start > 0 {
let b = bytes[line_start - 1];
if matches!(b, b'\n' | b'\r') {
if seen_newline {
break;
}
seen_newline = true;
} else if !matches!(b, b' ' | b'\t') {
seen_newline = false;
}
line_start -= 1;
}
let mut i = line_start;
while i < pos {
let Some(rel) = memchr::memchr2(b'<', b'\\', &bytes[i..pos]) else {
return false;
};
let j = i + rel;
if bytes[j] == b'\\' {
i = j + 2;
continue;
}
if let Some(len) = crate::mdx::scan_mdx_inline_jsx(&bytes[j..])
&& j + len > pos
{
return true;
}
i = j + 1;
}
false
}
fn scan_email_forward_from_atext(
bytes: &[u8],
underscore_ix: usize,
begin_text: usize,
paragraph_start: usize,
) -> Option<(usize, usize, String)> {
let mut at_ix = underscore_ix;
while at_ix < bytes.len() && is_email_local_char(bytes[at_ix]) {
at_ix += 1;
}
if at_ix >= bytes.len() || bytes[at_ix] != b'@' {
return None;
}
let (sc_start, sc_end, full_url, retry_needed) = scan_email_autolink(bytes, at_ix, true)?;
if retry_needed
|| sc_start > underscore_ix
|| sc_start < begin_text
|| sc_start < paragraph_start
{
return None;
}
Some((sc_start, sc_end, full_url))
}
fn is_email_local_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.' | b'_')
}
fn defer_autolink_decision(bytes: &[u8], block_start: usize, pos: usize, options: Options) -> bool {
let before = &bytes[block_start..pos];
memchr::memchr3(b'[', b'<', b'`', before).is_some()
|| (options.has_math() && memchr::memchr(b'$', before).is_some())
}
struct AutolinkDetection {
start: usize,
end: usize,
link_type: LinkType,
url: String,
}
fn delim_run_flags(
text: &str,
start: usize,
at: usize,
run_len: usize,
mode: TableParseMode,
options: Options,
) -> (bool, bool) {
if run_len == 0 || at >= text.len() {
return (false, false);
}
let s = &text[start..];
let suffix = &text[at..];
(
delim_run_can_open(s, suffix, run_len, at - start, mode, options),
delim_run_can_close(s, suffix, run_len, at - start, mode, options),
)
}
fn is_attention_marker(c: char, options: Options) -> bool {
c == '*' || c == '_' || (c == '~' && options.contains(Options::ENABLE_STRIKETHROUGH))
}
fn tilde_is_delimiter(options: Options) -> bool {
options.contains(Options::ENABLE_STRIKETHROUGH) || options.contains(Options::ENABLE_SUBSCRIPT)
}
pub(crate) fn delim_run_is_valid(c: u8, count: usize, options: Options) -> bool {
c != b'~' || count == 2 || (count == 1 && tilde_is_delimiter(options))
}
fn escaped_delim_run(
text: &str,
start: usize,
ix: usize,
mode: TableParseMode,
options: Options,
) -> Option<(usize, (bool, bool))> {
let bytes = text.as_bytes();
let c = *bytes.get(ix + 1)?;
if !matches!(c, b'*' | b'_' | b'~' | b'^') {
return None;
}
let count = 1 + scan_ch_repeat(&bytes[(ix + 2)..], c);
let fired = delim_run_flags(text, start, ix + 1, count, mode, options);
if (!fired.0 && !fired.1) || !delim_run_is_valid(c, count, options) {
return None;
}
Some((count, fired))
}
fn detect_email_inside_www(
bytes: &[u8],
ix: usize,
www_end: usize,
paragraph_start: usize,
begin_text: usize,
) -> Option<AutolinkDetection> {
if ix > 0 && bytes[ix - 1] == b'_' {
return None;
}
let at_ix = ix + memchr::memchr(b'@', &bytes[ix..www_end])?;
let (email_start, email_end, full_url, retry_needed) =
crate::post_passes::scan_email_autolink(bytes, at_ix, true)?;
if email_start != ix {
return None;
}
debug_assert!(
!retry_needed,
"a `/` predecessor would have failed the www literal first"
);
debug_assert!(
email_start >= begin_text && email_start >= paragraph_start,
"the trigger is inside the current text run"
);
Some(AutolinkDetection {
start: email_start,
end: email_end,
link_type: LinkType::Email,
url: email_addr(full_url),
})
}
fn email_addr(full_url: String) -> String {
full_url
.strip_prefix("mailto:")
.map(str::to_owned)
.unwrap_or(full_url)
}
fn detect_gfm_autolink(
bytes: &[u8],
ix: usize,
byte: u8,
paragraph_start: usize,
begin_text: usize,
) -> Option<AutolinkDetection> {
let is_www = match byte {
b'h' | b'H' | b'w' | b'W' => crate::post_passes::match_autolink_scheme(bytes, ix)?.1,
b'@' => {
if ix == 0 || !is_email_local_char(bytes[ix - 1]) {
return None;
}
false
}
_ => return None,
};
match byte {
b'h' | b'H' | b'w' | b'W' => {
let (start, _raw_end, end, full_url, fnr_only) =
scan_autolink_literal(bytes, ix, ix == paragraph_start)?;
if fnr_only {
return None;
}
if is_www
&& let Some(email) =
detect_email_inside_www(bytes, ix, end, paragraph_start, begin_text)
{
return Some(email);
}
Some(AutolinkDetection {
start,
end,
link_type: LinkType::Autolink,
url: full_url,
})
}
b'@' => {
let (email_start, email_end, full_url, retry_needed) =
scan_email_autolink(bytes, ix, true)?;
if retry_needed {
return None;
}
if email_start < begin_text {
return None;
}
if email_start < paragraph_start {
return None;
}
if email_start > 0
&& bytes[email_start - 1] == b'\\'
&& is_ascii_punctuation(bytes[email_start])
{
return None;
}
Some(AutolinkDetection {
start: email_start,
end: email_end,
link_type: LinkType::Email,
url: email_addr(full_url),
})
}
_ => None,
}
}
#[cfg(feature = "mdx")]
fn contains_blank_line(bytes: &[u8]) -> bool {
let mut i = 0;
let mut at_line_start = true;
let mut line_has_non_ws = false;
while i < bytes.len() {
let b = bytes[i];
if b == b'\n' || b == b'\r' {
if !line_has_non_ws && !at_line_start {
return true;
}
if b == b'\r' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
i += 2;
} else {
i += 1;
}
if i < bytes.len() && (bytes[i] == b'\n' || bytes[i] == b'\r') {
return true;
}
at_line_start = true;
line_has_non_ws = false;
continue;
}
at_line_start = false;
if b != b' ' && b != b'\t' {
line_has_non_ws = true;
}
i += 1;
}
false
}
fn is_directive_name_start_ascii(c: u8) -> bool {
c.is_ascii_alphanumeric()
}
fn is_directive_name_char_ascii(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'-' || c == b'_'
}
struct HeadingAttrBlock<'a> {
attrs: HeadingAttributes<'a>,
block_open: usize,
tail_start: Option<usize>,
}
fn scan_heading_text_directive(
text: &str,
bytes: &[u8],
colon: usize,
limit: usize,
) -> Option<usize> {
if colon > 0 && bytes[colon - 1] == b':' {
return None;
}
let (dir, end_pos) = parse_directive_after_colons(text, bytes, colon + 1)?;
if end_pos > limit {
return None;
}
if end_pos < limit && bytes[end_pos] == b':' && colon + 1 + dir.name.len() == end_pos {
return None;
}
Some(end_pos)
}
fn scan_directive_name_char(bytes: &[u8], ix: usize) -> Option<usize> {
if ix >= bytes.len() {
return None;
}
let b = bytes[ix];
if b < 0x80 {
return if is_directive_name_char_ascii(b) {
Some(1)
} else {
None
};
}
let rest = core::str::from_utf8(&bytes[ix..]).ok()?;
let ch = rest.chars().next()?;
if unicode_id_start::is_id_continue(ch) {
Some(ch.len_utf8())
} else {
None
}
}
fn scan_directive_name_start(bytes: &[u8]) -> Option<usize> {
if bytes.is_empty() {
return None;
}
let b = bytes[0];
if b < 0x80 {
return if is_directive_name_start_ascii(b) {
Some(1)
} else {
None
};
}
let rest = core::str::from_utf8(bytes).ok()?;
let ch = rest.chars().next()?;
if unicode_id_start::is_id_start(ch) {
Some(ch.len_utf8())
} else {
None
}
}
fn scan_directive_name(bytes: &[u8]) -> Option<(usize, usize)> {
let first = scan_directive_name_start(bytes)?;
let mut len = first;
while let Some(n) = scan_directive_name_char(bytes, len) {
len += n;
}
if len == 0 {
return None;
}
let last = bytes[len - 1];
if last == b'-' || last == b'_' {
return None;
}
Some((0, len))
}
fn scan_directive_label(bytes: &[u8]) -> Option<(usize, usize, usize)> {
if bytes.is_empty() || bytes[0] != b'[' {
return None;
}
let mut depth = 1i32;
let mut i = 1;
let label_start = 1;
while i < bytes.len() {
match bytes[i] {
b'\\' if i + 1 < bytes.len() => {
i += 2;
continue;
}
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
return Some((label_start, i, i + 1));
}
}
b'\n' | b'\r' => return None,
_ => {}
}
i += 1;
}
None
}
fn scan_directive_attributes(bytes: &[u8]) -> Option<(Vec<(CowStr<'_>, CowStr<'_>)>, usize)> {
if bytes.is_empty() || bytes[0] != b'{' {
return None;
}
let mut i = 1;
let end = loop {
if i >= bytes.len() {
return None;
}
match bytes[i] {
b'}' => break i,
b'\n' | b'\r' => return None,
b'\\' if i + 1 < bytes.len() => i += 2,
_ => i += 1,
}
};
let inner = &bytes[1..end];
let attrs = parse_directive_attrs_inner(inner);
Some((attrs, end + 1))
}
fn parse_directive_attrs_inner(bytes: &[u8]) -> Vec<(CowStr<'_>, CowStr<'_>)> {
let mut attrs: Vec<(CowStr<'_>, CowStr<'_>)> = Vec::new();
let mut classes: Vec<&str> = Vec::new();
let mut id: Option<&str> = None;
let text = core::str::from_utf8(bytes).unwrap_or("");
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b' ' | b'\t' => {
i += 1;
}
b'#' => {
i += 1;
let start = i;
while i < bytes.len() && !is_attr_shortcut_terminator(bytes[i]) {
i += 1;
}
if i > start {
id = Some(&text[start..i]);
}
}
b'.' => {
i += 1;
let start = i;
while i < bytes.len() && !is_attr_shortcut_terminator(bytes[i]) {
i += 1;
}
if i > start {
classes.push(&text[start..i]);
}
}
_ => {
let name_start = i;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric()
|| bytes[i] == b'-'
|| bytes[i] == b'.'
|| bytes[i] == b':'
|| bytes[i] == b'_')
{
i += 1;
}
if i == name_start {
i += 1;
continue;
}
let name = &text[name_start..i];
if i < bytes.len() && bytes[i] == b'=' {
i += 1;
if i < bytes.len() && (bytes[i] == b'"' || bytes[i] == b'\'') {
let quote = bytes[i];
i += 1;
let val_start = i;
while i < bytes.len() && bytes[i] != quote {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
i += 1;
}
i += 1;
}
let val = &text[val_start..i];
if i < bytes.len() {
i += 1; }
attrs.push((name.into(), val.into()));
} else {
let val_start = i;
while i < bytes.len()
&& bytes[i] != b' '
&& bytes[i] != b'\t'
&& bytes[i] != b'}'
{
i += 1;
}
attrs.push((name.into(), text[val_start..i].into()));
}
} else {
attrs.push((name.into(), "".into()));
}
}
}
}
if let Some(id_val) = id {
attrs.push(("id".into(), id_val.into()));
}
if !classes.is_empty() {
attrs.push(("class".into(), classes.join(" ").into()));
}
attrs
}
fn is_attr_shortcut_terminator(c: u8) -> bool {
matches!(c, b'#' | b'.' | b'}' | b' ' | b'\t')
}
fn parse_directive_after_colons<'a>(
text: &'a str,
bytes: &'a [u8],
start: usize,
) -> Option<(DirectiveAttrData<'a>, usize)> {
let remaining = &bytes[start..];
let (_, name_len) = scan_directive_name(remaining)?;
let name: CowStr<'a> = text[start..start + name_len].into();
let mut pos = start + name_len;
let mut label_start = 0usize;
let mut label_end = 0usize;
if pos < bytes.len()
&& bytes[pos] == b'['
&& let Some((ls, le, consumed)) = scan_directive_label(&bytes[pos..])
{
label_start = pos + ls;
label_end = pos + le;
pos += consumed;
}
let mut attributes = Vec::new();
if pos < bytes.len()
&& bytes[pos] == b'{'
&& let Some((attrs, consumed)) = scan_directive_attributes(&bytes[pos..])
{
attributes = attrs;
pos += consumed;
}
Some((
DirectiveAttrData {
name,
attributes,
label_start,
label_end,
initial_size: 0,
},
pos,
))
}
fn get_html_end_tag(text_bytes: &[u8]) -> Option<&'static str> {
static BEGIN_TAGS: &[&[u8]; 4] = &[b"pre", b"style", b"script", b"textarea"];
static ST_BEGIN_TAGS: &[&[u8]; 3] = &[b"!--", b"?", b"![CDATA["];
for (beg_tag, end_tag) in BEGIN_TAGS
.iter()
.zip(["</pre>", "</style>", "</script>", "</textarea>"].iter())
{
let tag_len = beg_tag.len();
if text_bytes.len() < tag_len {
break;
}
if !text_bytes[..tag_len].eq_ignore_ascii_case(beg_tag) {
continue;
}
if text_bytes.len() == tag_len {
return Some(end_tag);
}
let s = text_bytes[tag_len];
if is_ascii_whitespace(s) || s == b'>' {
return Some(end_tag);
}
}
for (beg_tag, end_tag) in ST_BEGIN_TAGS.iter().zip(["-->", "?>", "]]>"].iter()) {
if text_bytes.starts_with(beg_tag) {
return Some(end_tag);
}
}
if text_bytes.len() > 1 && text_bytes[0] == b'!' && text_bytes[1].is_ascii_alphabetic() {
Some(">")
} else {
None
}
}
fn trim_trailing_newlines_from_code_block(
tree: &mut Tree<Item>,
bytes: &[u8],
max_newlines: usize,
) {
let Some(last_child) = tree.cur() else { return };
if !matches!(tree[last_child].item.body, ItemBody::Text { .. }) {
return;
}
let mut stripped = 0;
while stripped < max_newlines {
let start = tree[last_child].item.start;
let end = tree[last_child].item.end;
if end <= start {
break;
}
let last = bytes[end - 1];
if last == b'\n' {
tree[last_child].item.end = end - 1;
if end >= 2 && bytes[end - 2] == b'\r' {
tree[last_child].item.end = end - 2;
}
stripped += 1;
} else if last == b'\r' {
tree[last_child].item.end = end - 1;
stripped += 1;
} else {
break;
}
}
}
fn surgerize_tight_list(tree: &mut Tree<Item>, list_ix: TreeIndex) {
let mut list_item = tree[list_ix].child;
while let Some(listitem_ix) = list_item {
let mut node_ix = tree[listitem_ix].child;
while let Some(node) = node_ix {
if let ItemBody::Paragraph = tree[node].item.body {
tree[node].item.body = ItemBody::TightParagraph;
}
node_ix = tree[node].next;
}
list_item = tree[listitem_ix].next;
}
}
fn fixup_end_of_definition_list(tree: &mut Tree<Item>, list_ix: TreeIndex) {
let mut list_item = tree[list_ix].child;
let mut previous_list_item = None;
while let Some(listitem_ix) = list_item {
match &mut tree[listitem_ix].item.body {
ItemBody::DefinitionListTitle | ItemBody::DefinitionListDefinition(..) => {
previous_list_item = list_item;
list_item = tree[listitem_ix].next;
}
body @ ItemBody::MaybeDefinitionListTitle => {
*body = ItemBody::Paragraph;
break;
}
_ => break,
}
}
if let Some(previous_list_item) = previous_list_item {
tree.truncate_to_parent(previous_list_item);
}
}
fn delim_run_can_open(
s: &str,
suffix: &str,
run_len: usize,
ix: usize,
mode: TableParseMode,
options: Options,
) -> bool {
let next_char = if let Some(c) = suffix[run_len..].chars().next() {
c
} else {
return false;
};
if next_char.is_whitespace() {
return false;
}
if ix == 0 {
return true;
}
if mode == TableParseMode::Active {
if s.as_bytes()[..ix].ends_with(b"|") && !s.as_bytes()[..ix].ends_with(br"\|") {
return true;
}
if next_char == '|' {
return false;
}
}
let delim = suffix.bytes().next().unwrap();
if delim == b'*' && is_attention_marker(next_char, options) {
return true;
}
if (delim == b'*' || delim == b'^') && !is_punctuation(next_char) {
return true;
}
if delim == b'~' && run_len > 1 && !is_punctuation(next_char) {
return true;
}
let prev_char = s[..ix].chars().last().unwrap();
if delim == b'~' && options.contains(Options::ENABLE_SUBSCRIPT) && !is_punctuation(next_char) {
return true;
}
if delim == b'~' && options.contains(Options::ENABLE_STRIKETHROUGH) && run_len == 1 {
return !is_punctuation(next_char)
|| (is_punctuation(next_char)
&& (prev_char.is_whitespace() || is_punctuation(prev_char)));
}
if delim == b'"' {
return (!is_punctuation(next_char) && !prev_char.is_ascii_digit())
|| prev_char.is_whitespace()
|| is_punctuation(prev_char);
}
prev_char.is_whitespace()
|| is_punctuation(prev_char) && (delim != b'\'' || ![']', ')'].contains(&prev_char))
}
fn delim_run_can_close(
s: &str,
suffix: &str,
run_len: usize,
ix: usize,
mode: TableParseMode,
options: Options,
) -> bool {
if ix == 0 {
return false;
}
let prev_char = s[..ix].chars().last().unwrap();
if prev_char.is_whitespace() {
return false;
}
let next_char = if let Some(c) = suffix[run_len..].chars().next() {
c
} else {
return true;
};
if mode == TableParseMode::Active {
if s.as_bytes()[..ix].ends_with(b"|") && !s.as_bytes()[..ix].ends_with(br"\|") {
return false;
}
if next_char == '|' {
return true;
}
}
let delim = suffix.bytes().next().unwrap();
if delim == b'*' && is_attention_marker(prev_char, options) {
return true;
}
if (delim == b'*' || delim == b'^') && !is_punctuation(prev_char) {
return true;
}
if delim == b'~' && run_len > 1 && !is_punctuation(prev_char) {
return true;
}
if delim == b'~' && options.contains(Options::ENABLE_SUBSCRIPT) {
return true;
}
if delim == b'~' && options.contains(Options::ENABLE_STRIKETHROUGH) && run_len == 1 {
return !is_punctuation(prev_char)
|| (is_punctuation(prev_char)
&& (next_char.is_whitespace() || is_punctuation(next_char)));
}
if delim == b'"' {
return !is_punctuation(prev_char)
|| next_char.is_whitespace()
|| is_punctuation(next_char);
}
next_char.is_whitespace() || is_punctuation(next_char)
}
fn create_lut(options: &Options) -> LookupTable {
special_bytes(options)
}
fn special_bytes(options: &Options) -> [bool; 256] {
let mut bytes = [false; 256];
let standard_bytes = [
b'\n', b'\r', b'*', b'_', b'&', b'\\', b'[', b']', b'<', b'!', b'`',
];
for &byte in &standard_bytes {
bytes[byte as usize] = true;
}
if options.contains(Options::ENABLE_TABLES) {
bytes[b'|' as usize] = true;
}
if tilde_is_delimiter(*options) {
bytes[b'~' as usize] = true;
}
if options.contains(Options::ENABLE_SUPERSCRIPT) {
bytes[b'^' as usize] = true;
}
if options.has_math() {
bytes[b'$' as usize] = true;
bytes[b'{' as usize] = true;
bytes[b'}' as usize] = true;
}
if options.contains(Options::ENABLE_MDX) {
bytes[b'{' as usize] = true;
bytes[b'}' as usize] = true;
}
if options.has_smart_ellipses() {
bytes[b'.' as usize] = true;
}
if options.has_smart_dashes() {
bytes[b'-' as usize] = true;
}
if options.has_smart_quotes() {
bytes[b'"' as usize] = true;
bytes[b'\'' as usize] = true;
}
if options.contains(Options::ENABLE_DIRECTIVE) {
bytes[b':' as usize] = true;
}
if options.contains(Options::ENABLE_GFM) {
bytes[b'h' as usize] = true;
bytes[b'H' as usize] = true;
bytes[b'w' as usize] = true;
bytes[b'W' as usize] = true;
bytes[b'@' as usize] = true;
}
bytes
}
enum LoopInstruction<T> {
ContinueAndSkip(usize),
BreakAtWith(usize, T),
}
pub(crate) struct IndentCodeExtension {
pub end_offset: u32,
pub extra_blank_lines: usize,
}
pub(crate) fn extend_indented_code_block(
item: &Item,
source: &[u8],
parent_body: Option<&ItemBody>,
start_column: u32,
start_from: u32,
) -> Option<IndentCodeExtension> {
if !matches!(item.body, ItemBody::IndentCodeBlock(_)) {
return None;
}
if matches!(item.body, ItemBody::IndentCodeBlock(true)) {
return None;
}
if matches!(parent_body, Some(ItemBody::BlockQuote(_))) {
return None;
}
let required_indent_cols = (start_column as usize).saturating_add(3);
let line_indent_cols = |bytes: &[u8], from: usize| -> usize {
let mut p = from;
let mut cols = 0usize;
while p < bytes.len() {
match bytes[p] {
b' ' => cols += 1,
b'\t' => cols += 4 - (cols % 4),
_ => break,
}
p += 1;
}
cols
};
let mut pos = start_from as usize;
let mut last_indented_end: Option<usize> = None;
let mut last_indented_newlines = 0usize;
let mut newlines_skipped = 0usize;
loop {
while pos < source.len() && (source[pos] == b'\r' || source[pos] == b'\n') {
if source[pos] == b'\r' {
pos += 1;
if pos < source.len() && source[pos] == b'\n' {
pos += 1;
}
} else {
pos += 1;
}
newlines_skipped += 1;
}
if pos >= source.len() {
break;
}
let is_indented = line_indent_cols(source, pos) >= required_indent_cols;
if !is_indented {
let mut p = pos;
while p < source.len() && (source[p] == b' ' || source[p] == b'\t') {
p += 1;
}
if p < source.len() && (source[p] == b'\r' || source[p] == b'\n') {
pos = p;
continue;
}
break;
}
let line_start = pos;
while pos < source.len() && source[pos] != b'\n' && source[pos] != b'\r' {
pos += 1;
}
let line_content = &source[line_start..pos];
if !line_content.iter().all(|&b| b == b' ' || b == b'\t') {
break;
}
last_indented_end = Some(pos);
last_indented_newlines = newlines_skipped;
}
last_indented_end.map(|end| IndentCodeExtension {
end_offset: end as u32,
extra_blank_lines: last_indented_newlines.saturating_sub(1),
})
}
pub(crate) fn extend_list_item_to_next_sibling_content(
tree: &Tree<Item>,
ix: TreeIndex,
source: &[u8],
cont_end: u32,
) -> Option<u32> {
if cont_end == 0 {
return None;
}
if !matches!(source.get(cont_end as usize - 1), Some(b'\n' | b'\r')) {
return None;
}
let next_ix = tree[ix].next?;
if !matches!(tree[next_ix].item.body, ItemBody::ListItem(..)) {
return None;
}
let child_ix = tree[next_ix].child?;
let child_start = tree[child_ix].item.start as u32;
if child_start > cont_end {
Some(child_start)
} else {
None
}
}
pub(crate) fn extend_inner_blockquote_through_outer_markers(
tree: &Tree<Item>,
ix: TreeIndex,
source: &[u8],
cont_end: u32,
) -> Option<u32> {
if !matches!(tree[ix].item.body, ItemBody::BlockQuote(..)) {
return None;
}
if tree[ix].next.is_some() {
return None;
}
let outer_bq_count = tree
.walk_spine()
.filter(|&&i| matches!(tree[i].item.body, ItemBody::BlockQuote(_)))
.count();
if outer_bq_count == 0 {
return None;
}
let next_start = tree[ix]
.next
.map(|n| tree[n].item.start)
.unwrap_or(source.len());
let mut pos = cont_end as usize;
let mut new_end: Option<u32> = None;
while pos < next_start {
if pos < source.len() && source[pos] == b'\r' {
pos += 1;
if pos < source.len() && source[pos] == b'\n' {
pos += 1;
}
} else if pos < source.len() && source[pos] == b'\n' {
pos += 1;
}
if pos >= next_start || pos >= source.len() {
break;
}
let mut scan = pos;
let mut markers = 0usize;
while markers < outer_bq_count && scan < source.len() {
while scan < source.len() && matches!(source[scan], b' ' | b'\t') {
scan += 1;
}
if scan < source.len() && source[scan] == b'>' {
scan += 1;
markers += 1;
} else {
break;
}
}
if markers < outer_bq_count {
break;
}
let mut p = scan;
while p < source.len() && matches!(source[p], b' ' | b'\t') {
p += 1;
}
if p < source.len() && source[p] == b'>' {
break;
}
if p < source.len() && !matches!(source[p], b'\n' | b'\r') {
break;
}
new_end = Some(scan as u32);
pos = p;
}
new_end
}
pub(crate) fn extend_list_in_blockquote_through_marker_lines(
tree: &Tree<Item>,
ix: TreeIndex,
source: &[u8],
cont_end: u32,
) -> Option<u32> {
if !matches!(tree[ix].item.body, ItemBody::List(..)) {
return None;
}
let bq_count = tree
.walk_spine()
.filter(|&&i| matches!(tree[i].item.body, ItemBody::BlockQuote(_)))
.count();
if bq_count == 0 {
return None;
}
let any_outer_listitem = tree
.walk_spine()
.any(|&i| matches!(tree[i].item.body, ItemBody::ListItem(..)));
if any_outer_listitem {
return None;
}
let next_start = tree[ix]
.next
.map(|n| tree[n].item.start)
.unwrap_or(source.len());
if next_start <= cont_end as usize {
return None;
}
let mut pos = cont_end as usize;
let mut new_end = cont_end;
let mut saw_blank = false;
let mut stop = false;
while pos < next_start && !stop {
if pos < source.len() && source[pos] == b'\r' {
pos += 1;
if pos < source.len() && source[pos] == b'\n' {
pos += 1;
}
} else if pos < source.len() && source[pos] == b'\n' {
pos += 1;
}
if pos >= next_start {
break;
}
{
let mut leading_cols = 0usize;
let mut ws = pos;
while ws < source.len() && matches!(source[ws], b' ' | b'\t') && leading_cols < 4 {
leading_cols += if source[ws] == b'\t' {
4 - (leading_cols % 4)
} else {
1
};
ws += 1;
}
if leading_cols >= 4 {
break;
}
}
let mut markers_found = 0usize;
let mut scan = pos;
while markers_found < bq_count && scan < source.len() {
while scan < source.len() && matches!(source[scan], b' ' | b'\t') {
scan += 1;
}
if scan < source.len() && source[scan] == b'>' {
scan += 1;
markers_found += 1;
} else {
break;
}
}
if markers_found < bq_count {
break;
}
let after_markers = scan;
let mut p = after_markers;
while p < source.len() && matches!(source[p], b' ' | b'\t') {
p += 1;
}
let blank = p >= source.len() || matches!(source[p], b'\n' | b'\r');
if blank {
new_end = p as u32;
saw_blank = true;
pos = p;
} else if !saw_blank && source.get(p) == Some(&b'>') {
new_end = after_markers as u32;
stop = true;
} else if !saw_blank {
stop = true;
} else {
stop = true;
}
}
if new_end > cont_end {
Some(new_end)
} else {
None
}
}
pub(crate) fn mdast_position_end(
item: &Item,
source: &[u8],
parent_body: Option<&ItemBody>,
) -> u32 {
let end = item.end as u32;
if !item.body.is_block_level() {
return end;
}
let is_math = matches!(item.body, ItemBody::MathBlock(_));
let math_at_eof = is_math && end as usize >= source.len();
let is_fenced = matches!(item.body, ItemBody::FencedCodeBlock(..));
let fenced_at_eof = is_fenced && end as usize >= source.len();
let parent_is_bq = matches!(parent_body, Some(ItemBody::BlockQuote(_)));
let parent_is_listitem = matches!(parent_body, Some(ItemBody::ListItem(..)));
let fenced_at_eof_in_bq = fenced_at_eof && parent_is_bq;
let math_at_eof_in_bq = math_at_eof && parent_is_bq;
let next_line_opens_container = end > 1
&& matches!(source.get(end as usize - 1), Some(b'\n' | b'\r'))
&& !matches!(source.get(end as usize - 2), Some(b'\n' | b'\r'))
&& parent_is_listitem
&& {
match source.get(end as usize).copied() {
Some(b'-' | b'*' | b'+' | b'>') => true,
Some(c) if c.is_ascii_digit() => {
let mut p = end as usize + 1;
while p < source.len() && source[p].is_ascii_digit() {
p += 1;
}
matches!(source.get(p), Some(b'.' | b')'))
}
_ => false,
}
};
let fenced_unclosed_in_listitem = is_fenced && !fenced_at_eof && next_line_opens_container;
let math_unclosed_in_listitem = is_math && !math_at_eof && next_line_opens_container;
let skip_trim =
(math_at_eof || fenced_at_eof || fenced_unclosed_in_listitem || math_unclosed_in_listitem)
&& !fenced_at_eof_in_bq
&& !math_at_eof_in_bq;
if skip_trim {
return end;
}
let mut e = end;
while e > item.start as u32 && matches!(source.get(e as usize - 1), Some(b'\n' | b'\r')) {
e -= 1;
if is_math || is_fenced {
break;
}
}
e
}
type LookupTable = [bool; 256];
fn iterate_special_bytes<F, T>(
lut: &LookupTable,
bytes: &[u8],
ix: usize,
callback: F,
) -> (usize, Option<T>)
where
F: FnMut(usize, u8) -> LoopInstruction<Option<T>>,
{
scalar_iterate_special_bytes(lut, bytes, ix, callback)
}
fn scalar_iterate_special_bytes<F, T>(
lut: &[bool; 256],
bytes: &[u8],
mut ix: usize,
mut callback: F,
) -> (usize, Option<T>)
where
F: FnMut(usize, u8) -> LoopInstruction<Option<T>>,
{
while ix < bytes.len() {
let b = bytes[ix];
if lut[b as usize] {
match callback(ix, b) {
LoopInstruction::ContinueAndSkip(skip) => {
ix += skip;
}
LoopInstruction::BreakAtWith(ix, val) => {
return (ix, val);
}
}
}
ix += 1;
}
(ix, None)
}
fn extract_attribute_block_content_from_header_text(
heading: &[u8],
) -> (usize, Option<Range<usize>>) {
let heading_len = heading.len();
let mut ix = heading_len;
ix -= scan_rev_while(heading, |b| {
b == b'\n' || b == b'\r' || b == b' ' || b == b'\t'
});
if ix == 0 {
return (heading_len, None);
}
let attr_block_close = ix - 1;
if heading.get(attr_block_close) != Some(&b'}') {
return (heading_len, None);
}
ix -= 1;
ix -= scan_rev_while(&heading[..ix], |b| {
!matches!(b, b'{' | b'}' | b'<' | b'>' | b'\\' | b'\n' | b'\r')
});
if ix == 0 {
return (heading_len, None);
}
let attr_block_open = ix - 1;
if heading[attr_block_open] != b'{' {
return (heading_len, None);
}
(attr_block_open, Some(ix..attr_block_close))
}
fn parse_inside_attribute_block(inside_attr_block: &str) -> Option<HeadingAttributes<'_>> {
let mut id = None;
let mut classes = Vec::new();
let mut attrs = Vec::new();
let bytes = inside_attr_block.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if bytes[i].is_ascii_whitespace() {
i += 1;
continue;
}
let start = i;
let mut quote = None;
while i < len {
let b = bytes[i];
if let Some(q) = quote {
if b == q {
quote = None;
}
} else if (b == b'"' || b == b'\'') && i > start && bytes[i - 1] == b'=' {
quote = Some(b);
} else if b.is_ascii_whitespace() {
break;
}
i += 1;
}
let attr = &inside_attr_block[start..i];
if attr.len() > 1 {
let first_byte = attr.as_bytes()[0];
if first_byte == b'#' {
id = Some(attr[1..].into());
} else if first_byte == b'.' {
classes.push(attr[1..].into());
} else if let Some((key, value)) = attr.split_once('=') {
let value = unquote_attribute_value(value);
match key {
"id" => id = Some(value.into()),
"class" => classes.push(value.into()),
_ => attrs.push((key.into(), Some(value.into()))),
}
} else {
attrs.push((attr.into(), None));
}
}
}
if id.is_none() && classes.is_empty() && attrs.is_empty() {
return None;
}
Some(HeadingAttributes { id, classes, attrs })
}
fn unquote_attribute_value(value: &str) -> &str {
let bytes = value.as_bytes();
if bytes.len() >= 2 {
let quote = bytes[0];
if (quote == b'"' || quote == b'\'') && bytes[bytes.len() - 1] == quote {
return &value[1..value.len() - 1];
}
}
value
}