#![allow(clippy::too_many_arguments)]
use crate::{
Admonition, AdmonitionVariant, Anchor, AttributeValue, Attribution, Audio, Author, Block,
BlockMetadata, CalloutList, CalloutListItem, CalloutRef, CiteTitle, Comment, CommentKind,
DelimitedBlock, DelimitedBlockType, DescriptionList, DescriptionListItem, DiscreteHeader,
Document, DocumentAttribute, DocumentAttributes, Error, Header, Image, InlineNode, ListItem,
ListItemCheckedStatus, Location, OrderedList, PageBreak, Paragraph, Plain, Position, Section,
Source, Subtitle, TableOfContents, ThematicBreak, Title, UnorderedList, Video, Warning,
WarningKind,
document_attribute::{AttributeDeclaration, RawAttributeValue},
grammar::{
ParserState,
document::{
author::{derive_author_attrs, register_author_attrs},
callouts::resolve_verbatim_callouts,
delimited::{DelimitedKind, DelimitedParams, build_delimited_block},
doctype::{is_book_doctype, is_manpage_doctype},
lists::{
assemble_principal_text, build_description_list_topology, calculate_item_end,
find_dlist_marker,
},
manpage::{
ManpageNameSection, derive_manpage_header_attrs, derive_name_section_attrs,
prepare_manpage_name_attributes,
},
metadata::{
AttributeOrAnchorLine, BlockAttributeMode, BlockMetadataLine, assign_block_caption,
extract_media_dimensions, finish_block_parsing_metadata, merge_attribute_metadata,
metadata_marks_discrete_heading, order_document_attribute_events,
parse_block_attribute_list, push_metadata_anchor,
},
references::{
ReferenceCatalog, collect_inline_references, collect_metadata_references,
collect_references, finalize_inline_semantics, header_reference_title,
insert_reference, metadata_xreflabel, normalize_bibliography_lists,
register_document_top, register_section_header, resolve_source_target,
resolve_xref_target,
},
revision::{IgnoredRevisionFields, RevisionInfo, process_revision_info},
sections::{apply_leveloffset, expected_child_level},
table::{TableClosing, TableParseParams, parse_table_block_impl},
verbatim::{get_literal_paragraph, resolve_verbatim_inlines, verbatim_substitutions},
},
helpers::{
BlockParsingMetadata, MacroAttributeContext, PositionWithOffset,
is_valid_bibliography_id, restore_url_path, title_looks_like_description_list,
},
inline_preprocessing,
inline_preprocessor::InlinePreprocessorParserState,
inline_processing::{adjust_and_log_parse_error, process_inlines},
setext,
state::BlockContext,
},
model::{
ListLevel, SectionKind, SectionLevel, Substitution, caption, section, substitution::HEADER,
},
};
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
rc::Rc,
};
peg::parser! {
pub(crate) grammar document_parser(state: &mut ParserState<'input>) for str {
use std::str::FromStr;
use crate::model::substitute;
inject span_start(_input, l, _r) -> usize { l }
inject span_end(_input, _l, r) -> usize { r }
pub(crate) rule document() -> Result<Document<'input>, Error>
= eol()* start:position() comments_before_header:(comment:leading_comment() eol()* { comment })* header_result:header() prepare_manpage_front_matter() header_attributes:header_attribute_snapshot() blocks:blocks(0, None, None) end:position!() (eol()* / ![_]) {
let header = header_result?;
let mut blocks: Vec<Block<'_>> = comments_before_header.into_iter().collect::<Result<Vec<_>, Error>>()?.into_iter().chain(blocks?).collect();
let mut document_end_offset = end;
if document_end_offset > state.input.len() {
document_end_offset = state.input.len();
}
while document_end_offset < state.input.len() && !state.input.is_char_boundary(document_end_offset) {
document_end_offset += 1;
}
let document_end_offset = if document_end_offset == 0 {
0
} else {
crate::grammar::utf8_utils::safe_decrement_offset(state.input, document_end_offset)
};
let (absolute_start, absolute_end) = if start.offset > document_end_offset {
(start.offset, start.offset)
} else {
(start.offset, document_end_offset)
};
let (start_position, end_position) = if state.input.is_empty() || (absolute_start == 0 && absolute_end == 0) {
(Position::new(1, 0), Position::new(1, 0))
} else {
(
start.position,
state.line_map.offset_to_position(absolute_end, state.input)
)
};
if !state.toc_entries.is_empty() {
let mut anchored = header.as_ref().is_some_and(|h| !h.title.is_empty());
let mut seen_section = false;
for block in &blocks {
if let Block::Section(section) = block {
if !anchored {
break;
}
if section.level > 1 {
let location = state
.create_error_source_location(section.location.clone());
state.add_warning(Warning::new(
WarningKind::SectionLevelOutOfSequence {
expected: 1,
got: section.level,
},
Some(location),
));
}
seen_section = true;
} else if !seen_section && !matches!(block, Block::Comment(_)) {
anchored = true;
}
}
}
normalize_bibliography_lists(state, &mut blocks);
caption::renumber_captions(&mut blocks);
let header_has_anchor = header.as_ref().is_some_and(|header| {
header.metadata.id.is_some() || !header.metadata.anchors.is_empty()
});
let mut references = ReferenceCatalog {
entries: HashMap::with_capacity(state.toc_entries.len() + usize::from(header_has_anchor)),
next_suffix: HashMap::new(),
natural_targets: HashMap::new(),
};
let mut xrefs = Vec::new();
if let Some(header) = &header {
if let Some(anchor) = header
.metadata
.id
.as_ref()
.or_else(|| header.metadata.anchors.last())
{
let label = metadata_xreflabel(state, &header.metadata);
insert_reference(
state,
&mut references,
anchor,
label,
Some(header_reference_title(header)),
None,
);
}
collect_inline_references(
state,
header.title.as_ref(),
&mut references,
&mut xrefs,
);
if let Some(subtitle) = &header.subtitle {
collect_inline_references(
state,
subtitle.as_ref(),
&mut references,
&mut xrefs,
);
}
collect_metadata_references(
state,
&header.metadata,
&mut references,
&mut xrefs,
);
}
collect_references(state, &mut blocks, &mut references, &mut xrefs);
let is_book = is_book_doctype(&header_attributes);
section::number_parsed_sections(
&mut blocks,
&mut state.toc_entries,
&mut references.entries,
is_book,
);
let toc_entries = state.toc_entries.clone();
let mut document = Document {
header,
location: Location {
absolute_start,
absolute_end,
start: start_position,
end: end_position,
},
attributes: header_attributes,
blocks,
footnotes: state.footnote_tracker.borrow().footnotes.clone(),
toc_entries,
references: references.entries,
};
register_document_top(state, &mut document, &xrefs);
let reference_ids = document.references.keys().copied().collect::<HashSet<_>>();
for xref in xrefs {
let source_target = resolve_source_target(state, xref.target, xref.source_syntax);
let target = source_target.unwrap_or_else(|| resolve_xref_target(
xref.target,
xref.resolve_natural_target,
&reference_ids,
&references.natural_targets,
));
let target_is_local = source_target.is_some()
|| xref.source_syntax.target_is_local(target);
if target_is_local && xref.automatic
&& let Some(reference) = document.references.get_mut(target)
&& reference.is_bibliography()
{
reference.automatic_citation = true;
}
if target_is_local && !reference_ids.contains(target)
{
let source_location = state.create_error_source_location(xref.location);
state.add_warning(Warning::new(
WarningKind::UnresolvedReference {
target: target.to_string(),
},
Some(source_location),
));
}
}
finalize_inline_semantics(
state,
&mut document,
&reference_ids,
&references.natural_targets,
);
Ok(document)
}
rule header_attribute_snapshot() -> DocumentAttributes<'input>
= {
crate::document_attribute::normalize_toc_attributes(Rc::make_mut(&mut state.document_attributes));
DocumentAttributes::clone(&state.document_attributes)
}
rule prepare_manpage_front_matter()
= manpage_name_section_required() section:&manpage_name_section() {
prepare_manpage_name_attributes(state, Some(section));
}
/ {
prepare_manpage_name_attributes(state, None);
}
rule manpage_name_section_required()
= {?
if is_manpage_doctype(&state.document_attributes)
&& !(state.document_attributes.text("manname").is_some()
&& state.document_attributes.text("manpurpose").is_some())
{
Ok(())
} else {
Err("manpage name section is not required")
}
}
rule manpage_name_section() -> ManpageNameSection<'input>
= eol()*
metadata_attributes:(attribute:manpage_name_metadata() eol()* { attribute })*
title:manpage_level_one_title()
eol()*
lines:manpage_name_body_line()+
{?
derive_name_section_attrs(lines)
.map(|attributes| ManpageNameSection {
title,
attributes,
metadata_attributes: metadata_attributes.into_iter().flatten().collect(),
})
.ok_or("non-conforming manpage name section body")
}
rule manpage_level_one_title() -> &'input str
= level:section_level(0, None) whitespace()+ title:$([^'\n']+) (eol() / ![_]) {?
(level.1 == 1)
.then_some(title.trim())
.ok_or("not a level-one manpage name section")
}
/ title:$([^'\n']+) eol()
level:setext_section_level(title.trim().chars().count(), None) {?
(level == 1 && !title_looks_like_description_list(title))
.then_some(title.trim())
.ok_or("not a level-one Setext manpage name section")
}
rule manpage_name_metadata() -> Option<AttributeDeclaration<'input>>
= manpage_comment_block() { None }
/ "//" !"/" [^'\n']* (eol() / ![_]) { None }
/ "[[" (!"]]" [^'\n'])+ "]]" (eol() / ![_]) { None }
/ !empty_list_separator() !double_open_square_bracket()
open_square_bracket() attribute_list_content() (eol() / ![_]) { None }
/ "." ![' ' | '\t' | '\n' | '\r' | '.'] [^'\n']* (eol() / ![_]) { None }
/ attribute:document_attribute_match() (eol() / ![_]) { Some(attribute) }
rule manpage_comment_block()
= delimiter:$(['/']*<4,>) eol()
(!manpage_comment_delimiter(delimiter) [^'\n']* eol())*
manpage_comment_delimiter(delimiter)
rule manpage_comment_delimiter(delimiter: &str)
= candidate:$(['/']*<4,>) (eol() / ![_]) {?
(candidate == delimiter)
.then_some(())
.ok_or("not the matching comment delimiter")
}
rule manpage_name_body_line() -> Option<&'input str>
= "//" !"/" [^'\n']* (eol() / ![_]) { None }
/ line:$([^'\n']+) (eol() / ![_]) { Some(line) }
pub(crate) rule header() -> Result<Option<Header<'input>>, Error>
= start:position!()
((document_attribute() / header_comment()) (eol() / ![_]))*
metadata:header_metadata()
title_authors:(title_authors:title_authors() { title_authors })?
(eol() (document_attribute() / header_comment()))*
end:position!()
(eol()*<,2> / ![_])
{
if let Some((title, subtitle, authors)) = title_authors {
let mut location = state.create_location(start, end);
location.absolute_end = crate::grammar::utf8_utils::safe_decrement_offset(state.input, location.absolute_end);
location.end.column = location.end.column.saturating_sub(1);
let mut header = Header {
metadata,
title,
subtitle,
authors,
location,
};
derive_author_attrs(
state.arena,
&mut header,
Rc::make_mut(&mut state.document_attributes),
);
if is_manpage_doctype(&state.document_attributes) {
derive_manpage_header_attrs(
Some(&header),
Rc::make_mut(&mut state.document_attributes),
state.options.strict,
state.current_file.as_deref().map(std::path::PathBuf::as_path),
)?;
}
Ok(Some(header))
} else {
tracing::debug!("No title or authors found in the document header.");
Ok(None)
}
}
rule header_metadata() -> BlockMetadata<'input>
= lines:(
anchor:anchor() { AttributeOrAnchorLine::Anchor(anchor) }
/ attr:attributes_line() { AttributeOrAnchorLine::Attributes((attr.0, Box::new(attr.1))) }
)+ &document_title()
{
let mut metadata = BlockMetadata::default();
for line in lines {
match line {
AttributeOrAnchorLine::Anchor(anchor) => push_metadata_anchor(&mut metadata, anchor),
AttributeOrAnchorLine::Attributes((_, attr_metadata)) => {
let attr_metadata = *attr_metadata;
if attr_metadata.id.is_some() {
metadata.id = attr_metadata.id;
}
if attr_metadata.style.is_some() {
metadata.style = attr_metadata.style;
}
metadata.roles.extend(attr_metadata.roles);
metadata.options.extend(attr_metadata.options);
for (name, value) in attr_metadata.attributes.iter() {
metadata.attributes.set(name.clone(), value.clone());
}
metadata.positional_attributes = attr_metadata.positional_attributes;
}
}
}
metadata
}
/ { BlockMetadata::default() }
pub(crate) rule title_authors() -> (Title<'input>, Option<Subtitle<'input>>, Vec<Author<'input>>)
= title_and_subtitle:document_title()
(eol() (document_attribute() / header_comment()))*
authors:(eol() authors:authors_and_revision() { authors })?
&(eol() / ![_])
{
let (title, subtitle) = title_and_subtitle;
tracing::debug!("Found title and authors in the document header.");
(title, subtitle, authors.unwrap_or_default())
}
pub(crate) rule document_title() -> (Title<'input>, Option<Subtitle<'input>>)
= document_title_atx()
/ document_title_setext()
rule document_title_atx() -> (Title<'input>, Option<Subtitle<'input>>)
= &atx_heading_prefix() document_title_token() whitespace() start:position!() title:$([^'\n']*) end:position!()
{?
tracing::debug!("Processing ATX document title");
let block_metadata = BlockParsingMetadata::default();
let (title_inlines, subtitle) = if let Some(colon_pos) = title.rfind(": ") {
let subtitle_raw = &title[colon_pos + 1..];
let subtitle_text = subtitle_raw.trim();
if subtitle_text.is_empty() {
let (inlines, _) = process_inlines(state, &block_metadata, start, end, 0, title)
.map_err(|_| "could not process document title")?;
(inlines, None)
} else {
let title_raw = &title[..colon_pos];
let title_text = title_raw.trim_end();
let title_end = start + title_text.len();
let (inlines, _) = process_inlines(state, &block_metadata, start, title_end, 0, title_text)
.map_err(|_| "could not process document title")?;
let sub_leading = subtitle_raw.len() - subtitle_raw.trim_start().len();
let sub_start_offset = start + colon_pos + 1 + sub_leading;
let subtitle_start = PositionWithOffset {
offset: sub_start_offset,
position: state.line_map.offset_to_position(sub_start_offset, state.input),
};
let sub_end = sub_start_offset + subtitle_text.len();
let (subtitle_inlines, _) = process_inlines(state, &block_metadata, subtitle_start.offset, sub_end, 0, subtitle_text)
.map_err(|_| "could not process document subtitle")?;
(inlines, Some(Subtitle::new(subtitle_inlines)))
}
} else {
let (inlines, _) = process_inlines(state, &block_metadata, start, end, 0, title)
.map_err(|_| "could not process document title")?;
(inlines, None)
};
Ok((Title::new(title_inlines), subtitle))
}
rule document_title_setext() -> (Title<'input>, Option<Subtitle<'input>>)
= title:$([^'\n']+) end:position!() eol()
underline:$("="+) &(eol() / ![_])
{?
if !setext::is_enabled(state) {
return Err("setext mode not enabled");
}
let title_text = title.trim();
let title_width = title_text.chars().count();
let underline_width = underline.chars().count();
if !setext::width_ok(title_width, underline_width) {
return Err("underline width out of tolerance");
}
if !underline.starts_with('=') {
return Err("document title must use = underline");
}
tracing::debug!("Processing setext document title");
let block_metadata = BlockParsingMetadata::default();
let (title_inlines, subtitle) = if let Some(colon_pos) = title.rfind(": ") {
let subtitle_raw = &title[colon_pos + 1..];
let subtitle_text = subtitle_raw.trim();
if subtitle_text.is_empty() {
let (inlines, _) = process_inlines(state, &block_metadata, span_start, end, 0, title)
.map_err(|_| "could not process setext document title")?;
(inlines, None)
} else {
let title_raw = &title[..colon_pos];
let title_text = title_raw.trim_end();
let title_end = span_start + title_text.len();
let (inlines, _) = process_inlines(state, &block_metadata, span_start, title_end, 0, title_text)
.map_err(|_| "could not process setext document title")?;
let sub_leading = subtitle_raw.len() - subtitle_raw.trim_start().len();
let sub_start_offset = span_start + colon_pos + 1 + sub_leading;
let subtitle_start = PositionWithOffset {
offset: sub_start_offset,
position: state.line_map.offset_to_position(sub_start_offset, state.input),
};
let sub_end = sub_start_offset + subtitle_text.len();
let (subtitle_inlines, _) = process_inlines(state, &block_metadata, subtitle_start.offset, sub_end, 0, subtitle_text)
.map_err(|_| "could not process setext document subtitle")?;
(inlines, Some(Subtitle::new(subtitle_inlines)))
}
} else {
let (inlines, _) = process_inlines(state, &block_metadata, span_start, end, 0, title)
.map_err(|_| "could not process setext document title")?;
(inlines, None)
};
Ok((Title::new(title_inlines), subtitle))
}
rule document_title_token() = "=" / "#"
rule authors_and_revision() -> Vec<Author<'input>>
= authors:author_line()
(eol() (document_attribute() / header_comment()))*
(eol() revision_pre_substitution())?
{ authors }
rule author_line() -> Vec<Author<'input>>
= !document_attribute_match() !comment()
start:position!() author_line:$([^'\n']+) end:position!() {?
let substituted_cow = substitute(author_line.trim(), HEADER, &state.document_attributes);
let substituted: &'input str = match substituted_cow {
Cow::Borrowed(s) => s,
Cow::Owned(s) => state.intern_str(&s),
};
tracing::debug!("Processing author line with substitution");
let mut temp_state =
ParserState::for_inline_parsing(substituted, state, state.inline_ctx);
let authors = if let Ok(authors) = document_parser::authors(substituted, &mut temp_state) {
tracing::debug!("Parsed authors from line");
authors
} else {
tracing::debug!("Author line did not parse structurally; using whole line as a single author");
let location = state.create_error_source_location(state.create_location(start, end));
state.add_warning(Warning::new(
WarningKind::NonStandardAuthorLine { line: substituted.to_string() },
Some(location),
));
vec![Author::new(state.arena, substituted, None, None)]
};
register_author_attrs(&authors, Rc::make_mut(&mut state.document_attributes));
Ok(authors)
}
pub(crate) rule authors() -> Vec<Author<'input>>
= authors:(author() ++ (";" whitespace()*)) {
authors
}
pub(crate) rule author() -> Author<'input>
= name:author_name() email:author_email()? {
let mut author = name;
if let Some(email_addr) = email {
author.email = Some(email_addr);
}
author
}
rule author_name() -> Author<'input>
= first:name_part() whitespace()+ middle:name_part() whitespace()+ last:$(name_part() ++ whitespace()) {
Author::new(state.arena, first, Some(middle), Some(last))
}
/ first:name_part() whitespace()+ last:name_part() {
Author::new(state.arena, first, None, Some(last))
}
/ first:name_part() {
Author::new(state.arena, first, None, None)
}
rule author_email() -> &'input str
= whitespace()* "<" email:$([^'>']*) ">" { email }
rule name_part() -> &'input str
= name:$([c if c.is_alphanumeric() || c == '.' || c == '-' || c == '\'']+ ("_" [c if c.is_alphanumeric() || c == '.' || c == '-' || c == '\'']+)*) {
name
}
pub(crate) rule revision() -> ()
= "v"? number:$(digits() ++ ".") date:revision_date()? remark:revision_remark()? {
let revision_info = RevisionInfo {
number: Cow::Owned(number.to_string()),
date: date.map(|d| Cow::Owned(d.to_string())),
remark: remark.map(|r| Cow::Owned(r.to_string())),
};
if revision_info.number.is_empty() {
return;
}
let revision_location = state.create_location(span_start, span_end);
let ignored: IgnoredRevisionFields = {
let document_attributes = Rc::make_mut(&mut state.document_attributes);
process_revision_info(revision_info, document_attributes)
};
if ignored.number {
state.add_generic_warning_at(
"Revision number found in revision line but ignoring due to being set through attribute entries.".to_string(),
revision_location.clone(),
);
}
if ignored.date {
state.add_generic_warning_at(
"Revision date found in revision line but ignoring due to being set through attribute entries.".to_string(),
revision_location.clone(),
);
}
if ignored.remark {
state.add_generic_warning_at(
"Revision remark found in revision line but ignoring due to being set through attribute entries.".to_string(),
revision_location,
);
}
}
rule revision_pre_substitution() -> ()
= rev_line:$([^'\n']+) {?
let substituted_cow = substitute(rev_line.trim(), HEADER, &state.document_attributes);
let substituted: &'input str = match substituted_cow {
Cow::Borrowed(s) => s,
Cow::Owned(s) => state.intern_str(&s),
};
tracing::debug!("Processing revision line with substitution");
let mut temp_state =
ParserState::for_inline_parsing(substituted, state, state.inline_ctx);
match document_parser::revision(substituted, &mut temp_state) {
Ok(()) => {
for key in ["revnumber", "revdate", "revremark"] {
if let Some(value) = temp_state.document_attributes.text(key) {
let value = state.intern_str(value);
Rc::make_mut(&mut state.document_attributes)
.insert_text(key.into(), value.into());
}
}
tracing::debug!("Parsed revision from line");
Ok(())
}
Err(_) => Err("line did not parse as revision")
}
}
rule revision_date() -> &'input str
= ", " date:$([^ (':'|'\n')]+) {
date
}
rule revision_remark() -> &'input str
= ": " remark:$([^'\n']+) {
remark
}
rule document_attribute() -> ()
= start:position!() att:document_attribute_match() end:position!() (&eol() / ![_])
{
tracing::debug!("Found document attribute in the document header");
let location = state.create_block_location(start, end, 0);
state.apply_document_attribute(&att, true, location);
}
pub(crate) rule blocks(offset: usize, parent_section_level: Option<SectionLevel>, direct_parent_section_kind: Option<SectionKind>) -> Result<Vec<Block<'input>>, Error>
= blocks:(!trailing_block_metadata_match() block:block(offset, parent_section_level, direct_parent_section_kind) { block })*
trailing:trailing_block_metadata(offset)?
{
let mut blocks = blocks.into_iter().collect::<Result<Vec<_>, Error>>()?;
if let Some(trailing) = trailing {
blocks.extend(trailing?);
}
Ok(order_document_attribute_events(blocks))
}
pub(crate) rule compound_blocks(offset: usize, parent_section_level: Option<SectionLevel>) -> Result<Vec<Block<'input>>, Error>
= content:blocks(offset, parent_section_level, None) eol()* { content }
rule trailing_block_metadata_match()
= eol()* &("[" / ".") (block_metadata_line_match() eol()*)+ ![_]
rule trailing_block_metadata(offset: usize) -> Result<Vec<Block<'input>>, Error>
= &trailing_block_metadata_match() eol()* events:(
event:document_attribute_block(offset) (eol() / ![_]) eol()* { Some(event) }
/ (attribute_or_anchor_line_match() / title_line_match()) eol()* { None }
)+
{
events.into_iter().flatten().collect()
}
pub(crate) rule nested_document_blocks(offset: usize, initial_attributes: &mut Vec<(crate::AttributeName<'input>, crate::DocumentAttributeAssignment<'input>)>) -> Result<Vec<Block<'input>>, Error>
= eol()*
header:(
comment:comment_line_block(offset) eol()* { comment }
/ attribute:document_attribute_block(offset) (eol() / ![_]) { attribute }
)*
header:normalize_nested_header(header, initial_attributes)
body:blocks(offset, None, None)
{
let mut blocks = header?;
blocks.extend(body?);
Ok(order_document_attribute_events(blocks))
}
rule normalize_nested_header(header: Vec<Result<Block<'input>, Error>>, initial_attributes: &mut Vec<(crate::AttributeName<'input>, crate::DocumentAttributeAssignment<'input>)>) -> Result<Vec<Block<'input>>, Error>
= { crate::grammar::document::table::normalize_nested_header(state, header, initial_attributes) }
pub(crate) rule blocks_for_table_cell(offset: usize) -> Result<Vec<Block<'input>>, Error>
= eol()*
blocks:(
start:position!()
content:$((!(eol()*<2,> / eol()* ![_]) [_])+)
end:position!()
eol()*
{
let (content, _) = process_inlines(
state,
&BlockParsingMetadata::default(),
start,
end,
offset,
content,
)?;
Ok(Block::Paragraph(Paragraph::new(
content,
state.create_block_location(start, end, offset),
)))
}
)*
{
blocks.into_iter().collect()
}
pub(crate) rule block(offset: usize, parent_section_level: Option<SectionLevel>, direct_parent_section_kind: Option<SectionKind>) -> Result<Block<'input>, Error>
= eol()*
!same_or_higher_level_section(offset, parent_section_level)
block:(
comment_line_block(offset) /
document_attribute_block(offset) /
&"[" dh:discrete_header(offset) { dh } /
section:section(offset, parent_section_level, direct_parent_section_kind) { section } /
section_setext:section_setext(offset, parent_section_level, direct_parent_section_kind) { section_setext } /
block_generic(offset, parent_section_level)
)
{ block }
rule comment_line_block(offset: usize) -> Result<Block<'input>, Error>
= "//" !("/") content:$([^'\n']*) end:position!() (eol() / ![_])
{
Ok(Block::Comment(Comment {
kind: CommentKind::Line,
content,
location: state.create_location(span_start + offset, end + offset),
}))
}
rule leading_comment() -> Result<Block<'input>, Error>
= comment_line_block(0)
/ start:position!() block:comment_block(start, 0, &BlockParsingMetadata::default()) { block }
rule header_comment()
= comment()
/ start:position!() result:comment_block(start, 0, &BlockParsingMetadata::default()) {?
result.map(|_| ()).map_err(|_| "invalid header comment")
}
rule comment_line_block_keep_eol(offset: usize) -> Result<Block<'input>, Error>
= "//" !("/") content:$([^'\n']*) end:position!() &(eol() / ![_])
{
Ok(Block::Comment(Comment {
kind: CommentKind::Line,
content,
location: state.create_location(span_start + offset, end + offset),
}))
}
rule same_or_higher_level_section(offset: usize, parent_section_level: Option<SectionLevel>) -> ()
= check_section_blocks() !document_attribute_match()
(metadata:$((block_metadata_line_match() eol()*)*)
{? if metadata_marks_discrete_heading(metadata, state) { Err("discrete heading belongs to its parent") } else { Ok(()) } })
(
level:section_level(offset, parent_section_level) &" "
{?
if let Some(parent_level) = parent_section_level {
let upcoming_level = level.1 + 1; if upcoming_level <= parent_level {
Ok(()) } else {
Err("not a same or higher level section")
}
} else {
Err("no parent section level to compare")
}
}
/
&setext_section_lookahead(parent_section_level)
)
rule setext_section_lookahead(parent_section_level: Option<SectionLevel>) -> ()
= title:$([^'\n']+) eol() underline:$(['-' | '~' | '^' | '+']+) &(eol() / ![_])
{?
if title_looks_like_description_list(title) {
return Err("title looks like a description list item");
}
if !setext::is_enabled(state) {
return Err("setext mode not enabled");
}
let title_width = title.trim().chars().count();
let underline_width = underline.chars().count();
if !setext::width_ok(title_width, underline_width) {
return Err("underline width out of tolerance");
}
let underline_char = underline.chars().next().ok_or("empty underline")?;
let level = setext::char_to_level(underline_char).ok_or("invalid setext char")?;
if level == 0 && !is_book_doctype(&state.document_attributes) {
return Err("not a section, seems like you're trying to define a document title");
}
if let Some(parent_level) = parent_section_level {
if level < parent_level {
Ok(()) } else {
Err("not a same or higher level section")
}
} else {
Err("no parent section level to compare")
}
}
rule atx_heading_prefix()
= ("=" / "#")*<1,6> whitespace()+ !eol() &[_]
rule atx_heading_match()
= (block_metadata_line_match() eol()*)* atx_heading_prefix()
rule discrete_header(offset: usize) -> Result<Block<'input>, Error>
= &atx_heading_match() block_metadata:(bm:heading_metadata(offset, None) {?
let bm = bm.map_err(|_| {
tracing::error!("error parsing block metadata in discrete_header");
"block metadata parse error"
})?;
if !bm.discrete {
return Err("not a discrete heading");
}
Ok(bm)
})
section_level:section_level(offset, None) whitespace()
title_start:position!() title:section_title(offset, &block_metadata) title_end:position!() &(eol()*<1,2> / ![_])
{
let (title, _) = title?;
tracing::debug!(title_start, title_end, "parsing discrete header block");
let level = section_level.1;
let location = state.create_block_location(span_start, span_end, offset);
if block_metadata.metadata.style == Some("float") {
let warning_location = state.create_error_source_location(
state.create_block_location(span_start, span_end, offset),
);
state.add_warning(Warning::new(
WarningKind::LegacyFloatDiscreteHeading,
Some(warning_location),
));
}
Ok(Block::DiscreteHeader(DiscreteHeader {
metadata: block_metadata.metadata,
title,
level,
location,
}))
}
pub(crate) rule document_attribute_block(offset: usize) -> Result<Block<'input>, Error>
= att:document_attribute_match()
{
let name = Cow::Borrowed(att.name);
let location = state.create_block_location(span_start, span_end, offset);
let event = state
.apply_document_attribute(&att, false, location.clone())
.unwrap_or_else(|| DocumentAttribute::rejected(name, location));
Ok(Block::DocumentAttribute(event))
}
pub(crate) rule section(offset: usize, parent_section_level: Option<SectionLevel>, direct_parent_section_kind: Option<SectionKind>) -> Result<Block<'input>, Error>
= check_section_blocks() &atx_heading_match()
block_metadata:(bm:heading_metadata(offset, parent_section_level) {?
bm.map_err(|_| {
tracing::error!("error parsing block metadata in section");
"block metadata parse error"
})
})
section_level_start:position!()
section_level:section_level(offset, parent_section_level)
section_level_end:position!()
whitespace()
title_start:position!()
section_header:(title:section_title(offset, &block_metadata) title_end:position!() &(eol()*<1,2> / ![_]) {
let (title, natural_title) = title?;
let location = state.create_block_location(section_level_start, title_end, offset);
Ok::<(Title<'input>, section::SectionNumbering, &'input str), Error>(register_section_header(
state,
&block_metadata,
title,
natural_title,
section_level.1,
location,
direct_parent_section_kind,
))
})
content:section_content(offset, Some(expected_child_level(
section_level.1,
SectionKind::from_style(block_metadata.metadata.style),
is_book_doctype(&state.document_attributes),
)), Some(SectionKind::from_style(block_metadata.metadata.style)))?
{
let (title, numbering, reference_text) = section_header?;
tracing::debug!(offset, "parsing section block");
if let Some(parent_level) = parent_section_level {
if section_level.1 < parent_level || section_level.1 > 5 {
return Err(Error::NestedSectionLevelMismatch(
Box::new(state.create_error_source_location(state.create_block_location(section_level_start, section_level_end, offset))),
section_level.1+1,
parent_level + 1,
));
}
if section_level.1 > parent_level {
let location = state.create_error_source_location(
state.create_block_location(section_level_start, section_level_end, offset),
);
state.add_warning(Warning::new(
WarningKind::SectionLevelOutOfSequence {
expected: parent_level,
got: section_level.1,
},
Some(location),
));
}
}
let level = section_level.1;
let location = state.create_block_location(span_start, span_end, offset);
let kind = SectionKind::from_style(block_metadata.metadata.style);
let mut section = Section::parsed(
block_metadata.metadata,
title,
level,
content.unwrap_or(Ok(Vec::new()))?,
kind,
numbering,
location,
);
section.reference_text = Some(reference_text);
Ok(Block::Section(section))
}
rule setext_section_level(title_width: usize, parent_section_level: Option<SectionLevel>) -> u8
= underline:$(['-' | '~' | '^' | '+']+) &(eol() / ![_])
{?
if !setext::is_enabled(state) {
return Err("setext mode not enabled");
}
let underline_width = underline.chars().count();
if !setext::width_ok(title_width, underline_width) {
return Err("underline width out of tolerance");
}
let underline_char = underline.chars().next().ok_or("empty underline")?;
let level = setext::char_to_level(underline_char).ok_or("invalid setext underline character")?;
if level == 0 && !is_book_doctype(&state.document_attributes) {
return Err("use = underline for document title, not section");
}
if let Some(parent_level) = parent_section_level
&& (level < parent_level || level > parent_level + 1 || level > 5)
{
return Err("section level mismatch with parent");
}
Ok(level)
}
rule setext_section_match(parent_section_level: Option<SectionLevel>)
= (block_metadata_line_match() eol()*)*
title:$([^'\n']+) eol()
setext_section_level(title.trim().chars().count(), parent_section_level)
pub(crate) rule section_setext(offset: usize, parent_section_level: Option<SectionLevel>, direct_parent_section_kind: Option<SectionKind>) -> Result<Block<'input>, Error>
= check_section_blocks() !check_line_is_description_list(offset)
&setext_section_match(parent_section_level)
block_metadata:(bm:heading_metadata(offset, parent_section_level) {?
bm.map_err(|_| {
tracing::error!("error parsing block metadata in section_setext");
"block metadata parse error"
})
})
title_start:position!() title:$([^'\n']+) title_end:position!() eol()
setext_level:setext_section_level(title.trim().chars().count(), parent_section_level)
section_header:({
let (processed_title, natural_title) =
process_inlines(state, &block_metadata, title_start, title_end, offset, title)?;
let title = Title::new(processed_title);
let location = state.create_block_location(title_start, title_end, offset);
Ok::<(Title<'input>, section::SectionNumbering, &'input str), Error>(register_section_header(
state,
&block_metadata,
title,
natural_title,
setext_level,
location,
direct_parent_section_kind,
))
})
content:section_content(offset, Some(expected_child_level(
setext_level,
SectionKind::from_style(block_metadata.metadata.style),
is_book_doctype(&state.document_attributes),
)), Some(SectionKind::from_style(block_metadata.metadata.style)))?
{
let (title, numbering, reference_text) = section_header?;
let location = state.create_block_location(span_start, span_end, offset);
let kind = SectionKind::from_style(block_metadata.metadata.style);
let mut section = Section::parsed(
block_metadata.metadata,
title,
setext_level,
content.unwrap_or(Ok(Vec::new()))?,
kind,
numbering,
location,
);
section.reference_text = Some(reference_text);
Ok(Block::Section(section))
}
rule block_metadata(offset: usize, parent_section_level: Option<SectionLevel>) -> Result<BlockParsingMetadata<'input>, Error>
= metadata:block_metadata_with_title(offset, parent_section_level, true) { metadata }
rule heading_metadata(offset: usize, parent_section_level: Option<SectionLevel>) -> Result<BlockParsingMetadata<'input>, Error>
= metadata:block_metadata_with_title(offset, parent_section_level, false) { metadata }
rule block_metadata_with_title(offset: usize, parent_section_level: Option<SectionLevel>, parse_title: bool) -> Result<BlockParsingMetadata<'input>, Error>
= meta_start:position!() lines:(line:(
anchor:anchor() { BlockMetadataLine::Anchor(anchor) }
/ attr:attributes_line() { BlockMetadataLine::Attributes((attr.0, Box::new(attr.1))) }
/ doc_attr:document_attribute_line(offset) { BlockMetadataLine::DocumentAttribute(doc_attr.0, doc_attr.1) }
/ title_line()
) end:position!() eol()* { (line, end) })*
{
let mut metadata = BlockMetadata::default();
let mut discrete = false;
let mut title_source = None;
let meta_end = lines.last().map_or(meta_start, |(_, end)| *end);
for (line, _) in lines {
match line {
BlockMetadataLine::Anchor(value) => push_metadata_anchor(&mut metadata, value),
BlockMetadataLine::Attributes((attr_discrete, attr_metadata)) => {
discrete = attr_discrete;
merge_attribute_metadata(&mut metadata, *attr_metadata);
},
BlockMetadataLine::DocumentAttribute(declaration, location) => {
if let Some(event) = state.apply_document_attribute(
&declaration,
false,
location,
) {
metadata.push_document_attribute(event);
}
},
BlockMetadataLine::Title { source, start, end } => {
title_source = Some((source, start, end));
}
}
}
let title = if let Some((source, start, end)) = title_source.filter(|_| parse_title) {
match process_inlines(
state, &BlockParsingMetadata::default(), start, end, offset, source,
) {
Ok((title, _)) => title.into(),
Err(error) => {
state.add_generic_warning(format!("failed to parse block title, skipping: {error:?}"));
Title::default()
}
}
} else {
Title::default()
};
if meta_start != meta_end {
metadata.location = Some(state.create_block_location(meta_start, meta_end, offset));
}
finish_block_parsing_metadata(
state,
metadata,
title,
parent_section_level,
discrete,
offset,
)
}
rule block_metadata_line_match()
= attribute_or_anchor_line_match()
/ document_attribute_match() (eol() / ![_])
/ title_line_match()
rule title_line_match()
= period() ![' ' | '\t' | '\n' | '\r' | '.'] [^'\n']+ (eol() / ![_])
rule title_line() -> BlockMetadataLine<'input>
= period() start:position!() title:$(![' ' | '\t' | '\n' | '\r' | '.'] [^'\n']*) end:position!() eol()
{
tracing::debug!(start, end, "Found title line in block metadata");
BlockMetadataLine::Title { source: title, start, end }
}
rule document_attribute_line(offset: usize) -> (AttributeDeclaration<'input>, Location)
= start:position!() attr:document_attribute_match() end:position!() eol()
{
tracing::debug!("Found document attribute in block metadata");
(attr, state.create_block_location(start, end, offset))
}
rule check_section_blocks()
= {? (state.block_context == BlockContext::Document).then_some(()).ok_or("section blocks are not allowed in compound content") }
rule section_level(offset: usize, parent_section_level: Option<SectionLevel>) -> (&'input str, SectionLevel)
= level:$(("=" / "#")*<1,6>)
{
let base_level: SectionLevel = level.len().try_into().unwrap_or(1) - 1;
let byte_offset = span_start + offset;
(level, apply_leveloffset(base_level, byte_offset, &state.leveloffset_ranges, &state.document_attributes))
}
rule at_line_start(offset: usize)
= pos:position!()
{?
let absolute_pos = pos + offset;
let at_line_start = absolute_pos == 0 || {
let prev_byte_pos = absolute_pos.saturating_sub(1);
state.input.as_bytes().get(prev_byte_pos).is_some_and(|&b| b == b'\n')
};
if !at_line_start {
return Err("expected line start");
}
Ok(())
}
rule heading_boundary(offset: usize)
= at_line_start(offset)
(check_section_blocks() (attribute_or_anchor_line_match() eol()*)*
/ (attribute_or_anchor_line_match() eol()*)+)
at_line_start(offset) atx_heading_prefix()
rule section_title(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<(Title<'input>, &'input str), Error>
= title:$([^'\n']*)
{
tracing::debug!(title_start = span_start, title_end = span_end, "Found section title");
let (content, natural_title) = process_inlines(
state,
block_metadata,
span_start,
span_end,
offset,
title,
)?;
Ok((Title::new(content), natural_title))
}
rule section_content(offset: usize, parent_section_level: Option<SectionLevel>, direct_parent_section_kind: Option<SectionKind>) -> Result<Vec<Block<'input>>, Error>
= blocks(offset, parent_section_level, direct_parent_section_kind) / { Ok(vec![]) }
pub(crate) rule block_generic(offset: usize, parent_section_level: Option<SectionLevel>) -> Result<Block<'input>, Error>
= start:position!()
block_metadata:(bm:block_metadata(offset, parent_section_level) {?
bm.map_err(|_| {
tracing::error!("error parsing block metadata in block_generic");
"block metadata parse error"
})
})
block:(
delimited_block:delimited_block(start, offset, &block_metadata) { delimited_block }
/ verbatim:styled_verbatim_paragraph(start, offset, &block_metadata) { verbatim }
/ image:image(start, offset, &block_metadata) { image }
/ audio:audio(start, offset, &block_metadata) { audio }
/ video:video(start, offset, &block_metadata) { video }
/ toc:toc(start, offset, &block_metadata) { toc }
/ thematic_break:thematic_break(start, offset, &block_metadata) { thematic_break }
/ page_break:page_break(start, offset, &block_metadata) { page_break }
/ list:list(start, offset, &block_metadata) { list }
/ quoted_paragraph:quoted_paragraph(start, offset, &block_metadata) { quoted_paragraph }
/ markdown_blockquote:markdown_blockquote(start, offset, &block_metadata) { markdown_blockquote }
/ paragraph:paragraph(start, offset, &block_metadata) { paragraph }
) {
let mut block = block?;
assign_block_caption(state, &mut block);
Ok(block)
}
rule block_in_continuation(offset: usize, parent_section_level: Option<SectionLevel>) -> Result<Block<'input>, Error>
= !trailing_block_metadata_match() start:position!()
block_metadata:(bm:block_metadata(offset, parent_section_level) {?
bm.map_err(|_| {
tracing::error!("error parsing block metadata in block_in_continuation");
"block metadata parse error"
})
})
block:(
comment:(eol()* comment_start:position!() c:(
comment_line_block_keep_eol(offset)
/ comment_block(comment_start, offset, &block_metadata)
) { c }) { comment }
/ delimited_block:delimited_block(start, offset, &block_metadata) { delimited_block }
/ verbatim:styled_verbatim_paragraph(start, offset, &block_metadata) { verbatim }
/ image:image(start, offset, &block_metadata) { image }
/ audio:audio(start, offset, &block_metadata) { audio }
/ video:video(start, offset, &block_metadata) { video }
/ toc:toc(start, offset, &block_metadata) { toc }
/ thematic_break:thematic_break(start, offset, &block_metadata) { thematic_break }
/ page_break:page_break(start, offset, &block_metadata) { page_break }
/ list:list_with_continuation(start, offset, &block_metadata, false) { list }
/ quoted_paragraph:quoted_paragraph(start, offset, &block_metadata) { quoted_paragraph }
/ markdown_blockquote:markdown_blockquote(start, offset, &block_metadata) { markdown_blockquote }
/ paragraph:paragraph(start, offset, &block_metadata) { paragraph }
) {
let mut block = block?;
assign_block_caption(state, &mut block);
Ok(block)
}
rule delimited_block(
start: usize,
offset: usize,
block_metadata: &BlockParsingMetadata<'input>,
) -> Result<Block<'input>, Error>
= generic_delimited_block(start, offset, block_metadata)
/ table_block(start, offset, block_metadata)
rule generic_delimited_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= open_start:position!() open:block_open() (eol() / ![_])
content_start:position!()
source_text:$(until_block_close(open.1) (eol() &block_close_delim(open.1))?) body_end:position!()
close:(close_start:position!() close_delim:block_close_delim(open.1) { (close_start, close_delim) })?
{
let content = if close.is_some() {
source_text.strip_suffix('\n').unwrap_or(source_text)
} else {
source_text
};
let content_end = body_end - (source_text.len() - content.len());
let kind = match (open.0, block_metadata.metadata.style) {
(DelimitedKind::Open, Some("source" | "listing")) => DelimitedKind::Listing,
(DelimitedKind::Open, Some("literal")) => DelimitedKind::Literal,
(kind, _) => kind,
};
build_delimited_block(state, block_metadata, &DelimitedParams {
kind, open_delim: open.1, lang: open.2, content, source_text,
open_start, start, content_start, content_end, end: span_end, offset, close,
})
}
rule block_open() -> (DelimitedKind, &'input str, Option<&'input str>)
= d:comment_delimiter() { (DelimitedKind::Comment, d, None) }
/ d:example_delimiter() { (DelimitedKind::Example, d, None) }
/ d:listing_delimiter() { (DelimitedKind::Listing, d, None) }
/ d:literal_delimiter() { (DelimitedKind::Literal, d, None) }
/ d:open_delimiter() { (DelimitedKind::Open, d, None) }
/ d:sidebar_delimiter() { (DelimitedKind::Sidebar, d, None) }
/ d:pass_delimiter() { (DelimitedKind::Pass, d, None) }
/ d:quote_delimiter() { (DelimitedKind::Quote, d, None) }
/ d:markdown_code_delimiter() lang:markdown_language()? { (DelimitedKind::Listing, d, lang) }
rule until_block_close(expected: &str) -> &'input str
= &block_close_delim(expected) { "" }
/ content:$((!(eol() block_close_delim(expected)) (eol() / [^'\n']+))*) { content }
rule block_close_delim(expected: &str) -> &'input str
= delim:$("="+ / "/"+ / "-"+ / "."+ / "*"+ / "_"+ / "+"+ / "~"+ / "`"+)
&(eol() / ![_])
{? if delim == expected { Ok(delim) } else { Err("delimiter mismatch") } }
rule comment_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= open_start:position!() open_delim:comment_delimiter() (eol() / ![_])
content_start:position!()
source_text:$(until_block_close(open_delim) (eol() &block_close_delim(open_delim))?) body_end:position!()
close:(close_start:position!() close_delim:block_close_delim(open_delim) { (close_start, close_delim) })?
{
let content = if close.is_some() {
source_text.strip_suffix('\n').unwrap_or(source_text)
} else {
source_text
};
let content_end = body_end - (source_text.len() - content.len());
build_delimited_block(state, block_metadata, &DelimitedParams {
kind: DelimitedKind::Comment, open_delim, lang: None, content, source_text,
open_start, start, content_start, content_end, end: span_end, offset, close,
})
}
rule comment_delimiter() -> &'input str = delim:$("/"*<4,>) { delim }
rule example_delimiter() -> &'input str = delim:$("="*<4,>) { delim }
rule listing_delimiter() -> &'input str = delim:$("-"*<4,>) { delim }
rule literal_delimiter() -> &'input str = delim:$("."*<4,>) { delim }
rule open_delimiter() -> &'input str = delim:$("-"*<2,2> / "~"*<4,>) { delim }
rule sidebar_delimiter() -> &'input str = delim:$("*"*<4,>) { delim }
rule table_delimiter() -> &'input str = delim:$((['|' | ',' | ':' | '!'] "="*<3,>)) { delim }
rule pipe_table_delimiter() -> &'input str = delim:$("|" "="*<3,>) { delim }
rule excl_table_delimiter() -> &'input str = delim:$("!" "="*<3,>) { delim }
rule comma_table_delimiter() -> &'input str = delim:$("," "="*<3,>) { delim }
rule colon_table_delimiter() -> &'input str = delim:$(":" "="*<3,>) { delim }
rule pass_delimiter() -> &'input str = delim:$("+"*<4,>) { delim }
rule markdown_code_delimiter() -> &'input str = delim:$("`"*<3,>) { delim }
rule quote_delimiter() -> &'input str = delim:$("_"*<4,>) { delim }
rule until_table_delimiter() -> &'input str
= content:$((!(eol() table_delimiter()) [_])*) { content }
rule until_pipe_table_delimiter() -> &'input str
= content:$((!(eol() pipe_table_delimiter()) [_])*) { content }
rule until_excl_table_delimiter() -> &'input str
= content:$((!(eol() excl_table_delimiter()) [_])*) { content }
rule until_comma_table_delimiter() -> &'input str
= content:$((!(eol() comma_table_delimiter()) [_])*) { content }
rule until_colon_table_delimiter() -> &'input str
= content:$((!(eol() colon_table_delimiter()) [_])*) { content }
rule markdown_language() -> &'input str
= lang:$((['a'..='z'] / ['A'..='Z'] / ['0'..='9'] / "_" / "+" / "-")+) { lang }
rule table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= pipe_table_block(start, offset, block_metadata)
/ excl_table_block(start, offset, block_metadata)
/ comma_table_block(start, offset, block_metadata)
/ colon_table_block(start, offset, block_metadata)
/ unterminated_pipe_table_block(start, offset, block_metadata)
/ unterminated_excl_table_block(start, offset, block_metadata)
/ unterminated_comma_table_block(start, offset, block_metadata)
/ unterminated_colon_table_block(start, offset, block_metadata)
rule pipe_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:pipe_table_delimiter() eol()
content_start:position!() content:until_pipe_table_delimiter() content_end:position!()
eol() close_start:position!() close_delim:pipe_table_delimiter()
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: "|",
closing: TableClosing::Terminated { close_delim, close_start },
},
state,
block_metadata,
)
}
rule excl_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:excl_table_delimiter() eol()
content_start:position!() content:until_excl_table_delimiter() content_end:position!()
eol() close_start:position!() close_delim:excl_table_delimiter()
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: "!",
closing: TableClosing::Terminated { close_delim, close_start },
},
state,
block_metadata,
)
}
rule comma_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:comma_table_delimiter() eol()
content_start:position!() content:until_comma_table_delimiter() content_end:position!()
eol() close_start:position!() close_delim:comma_table_delimiter()
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: ",",
closing: TableClosing::Terminated { close_delim, close_start },
},
state,
block_metadata,
)
}
rule colon_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:colon_table_delimiter() eol()
content_start:position!() content:until_colon_table_delimiter() content_end:position!()
eol() close_start:position!() close_delim:colon_table_delimiter()
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: ":",
closing: TableClosing::Terminated { close_delim, close_start },
},
state,
block_metadata,
)
}
rule unterminated_pipe_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:pipe_table_delimiter() (eol() / ![_])
content_start:position!() content:until_pipe_table_delimiter() content_end:position!()
![_]
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: "|",
closing: TableClosing::Unterminated,
},
state,
block_metadata,
)
}
rule unterminated_excl_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:excl_table_delimiter() (eol() / ![_])
content_start:position!() content:until_excl_table_delimiter() content_end:position!()
![_]
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: "!",
closing: TableClosing::Unterminated,
},
state,
block_metadata,
)
}
rule unterminated_comma_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:comma_table_delimiter() (eol() / ![_])
content_start:position!() content:until_comma_table_delimiter() content_end:position!()
![_]
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: ",",
closing: TableClosing::Unterminated,
},
state,
block_metadata,
)
}
rule unterminated_colon_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= table_start:position!() open_delim:colon_table_delimiter() (eol() / ![_])
content_start:position!() content:until_colon_table_delimiter() content_end:position!()
![_]
{
parse_table_block_impl(
&TableParseParams {
start, offset, table_start, content_start, content_end, end: span_end,
open_delim, content, default_separator: ":",
closing: TableClosing::Unterminated,
},
state,
block_metadata,
)
}
rule toc(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= "toc::" attributes:attributes() end:position!()
trailing:$([^'\n']*)
{
let (_discrete, metadata_from_attributes, _title_position) = attributes;
let mut metadata = block_metadata.metadata.clone();
metadata.merge(&metadata_from_attributes);
metadata.move_positional_attributes_to_attributes();
state.warn_trailing_macro_content("toc", trailing, end, offset);
tracing::debug!("Found Table of Contents block");
Ok(Block::TableOfContents(TableOfContents {
metadata,
location: state.create_location(start+offset, end+offset),
}))
}
rule normal_paragraph_style(block_metadata: &BlockParsingMetadata<'input>)
= {?
if matches!(
block_metadata.metadata.style,
Some("listing" | "source" | "literal" | "verse")
) {
Err("verbatim paragraph style")
} else {
Ok(())
}
}
rule image(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= normal_paragraph_style(block_metadata)
"image::" source:source() attributes:image_macro_attributes() end:position!()
trailing:$([^'\n']*)
{
state.warn_trailing_macro_content("image", trailing, end, offset);
let (_discrete, mut metadata_from_attributes, _title_position) = attributes;
extract_media_dimensions(&mut metadata_from_attributes);
let title = block_metadata.title.clone();
let mut metadata = block_metadata.metadata.clone();
metadata.merge(&metadata_from_attributes);
if let Some(style) = metadata.style {
metadata.style = None; metadata
.attributes
.set("alt".into(), AttributeValue::String(Cow::Borrowed(style)));
}
let _ = metadata.take_positional_attributes::<2>();
metadata.move_positional_attributes_to_attributes();
Ok(Block::Image(Image {
title,
source,
metadata,
location: state.create_block_location(start, end, offset),
}))
}
rule audio(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= "audio::" source:source() attributes:macro_attributes() end:position!()
trailing:$([^'\n']*)
{
state.warn_trailing_macro_content("audio", trailing, end, offset);
let (_discrete, metadata_from_attributes, _title_position) = attributes;
let title = block_metadata.title.clone();
let mut metadata = block_metadata.metadata.clone();
metadata.merge(&metadata_from_attributes);
metadata.move_positional_attributes_to_attributes();
Ok(Block::Audio(Audio {
title,
source,
metadata,
location: state.create_block_location(start, end, offset),
}))
}
rule video(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= "video::" sources:(source() ** comma()) attributes:macro_attributes() end:position!()
trailing:$([^'\n']*)
{
state.warn_trailing_macro_content("video", trailing, end, offset);
let (_discrete, mut metadata_from_attributes, _title_position) = attributes;
extract_media_dimensions(&mut metadata_from_attributes);
let title = block_metadata.title.clone();
let mut metadata = block_metadata.metadata.clone();
metadata.merge(&metadata_from_attributes);
if let Some(style) = metadata.style {
metadata.style = None;
if style == "youtube" || style == "vimeo" {
tracing::debug!("transforming video metadata style into attribute");
metadata
.attributes
.set(Cow::Borrowed(style), AttributeValue::Bool(true));
} else {
tracing::debug!("transforming video metadata style into attribute, assuming poster");
metadata.attributes.set(
"poster".into(),
AttributeValue::String(Cow::Borrowed(style)),
);
}
}
let _ = metadata.take_positional_attributes::<2>();
metadata.move_positional_attributes_to_attributes();
Ok(Block::Video(Video {
title,
sources,
metadata,
location: state.create_block_location(start, end, offset),
}))
}
rule thematic_break(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= ("'''"
/ "---"
/ "- - -"
/ "***"
/ "* * *"
)
{
tracing::debug!("Found thematic break block");
Ok(Block::ThematicBreak(ThematicBreak {
anchors: block_metadata.metadata.anchors.clone(),
title: block_metadata.title.clone(),
location: state.create_block_location(start, span_end, offset),
}))
}
rule page_break(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= "<<<" &eol()*<2,2>
{
tracing::debug!("Found page break block");
let mut metadata = block_metadata.metadata.clone();
metadata.move_positional_attributes_to_attributes();
Ok(Block::PageBreak(PageBreak {
title: block_metadata.title.clone(),
metadata,
location: state.create_location(start+offset, span_end+offset),
}))
}
rule list(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= list_with_continuation(start, offset, block_metadata, true)
rule list_continuation_allowed(allow: bool)
= {? allow.then_some(()).ok_or("continuation belongs to parent") }
rule list_with_continuation(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>, allow_continuation: bool) -> Result<Block<'input>, Error>
= callout_list(start, offset, block_metadata)
/ unordered_list(start, offset, block_metadata, None, allow_continuation, false)
/ ordered_list(start, offset, block_metadata, None, allow_continuation, false)
/ description_list(start, offset, block_metadata, allow_continuation)
rule unordered_list_marker() -> &'input str = $("*"+ / "-")
rule ordered_list_marker() -> &'input str = $(digits()? "."+)
rule description_list_marker() -> &'input str = $("::::" / ":::" / "::" / ";;")
rule callout_list_marker() -> &'input str = $("<" (digits() / ".") ">")
rule section_level_marker() -> &'input str = $(("=" / "#")+)
rule nested_list_metadata(offset: usize, parent_section_level: Option<SectionLevel>) -> Result<BlockParsingMetadata<'input>, Error>
= meta_start:position!() lines:(line:(
anchor:anchor() { AttributeOrAnchorLine::Anchor(anchor) }
/ attr:attributes_line() { AttributeOrAnchorLine::Attributes((attr.0, Box::new(attr.1))) }
) end:position!() eol()* { (line, end) })+
{
let mut metadata = BlockMetadata::default();
let mut discrete = false;
let meta_end = lines.last().map_or(meta_start, |(_, end)| *end);
for (line, _) in lines {
match line {
AttributeOrAnchorLine::Anchor(anchor) => push_metadata_anchor(&mut metadata, anchor),
AttributeOrAnchorLine::Attributes((attr_discrete, attr_metadata)) => {
discrete = attr_discrete;
merge_attribute_metadata(&mut metadata, *attr_metadata);
}
}
}
metadata.location = Some(state.create_block_location(meta_start, meta_end, offset));
finish_block_parsing_metadata(
state,
metadata,
Title::default(),
parent_section_level,
discrete,
offset,
)
}
rule parsed_nested_list_metadata(offset: usize, parent_section_level: Option<SectionLevel>) -> BlockParsingMetadata<'input>
= metadata:nested_list_metadata(offset, parent_section_level) {?
metadata.map_err(|_| {
tracing::error!("error parsing nested list metadata");
"nested list metadata parse error"
})
}
rule attributes_line_match()
= !empty_list_separator() !double_open_square_bracket()
open_square_bracket() attribute_list_content() (eol() / ![_])
rule anchor_line_match()
= double_open_square_bracket() [^'\'' | ',' | ']' | ' ' | '\t' | '\n' | '\r']+
(comma() [^']']+)? double_close_square_bracket() (eol() / ![_])
rule attribute_or_anchor_line_match()
= anchor_line_match() / attributes_line_match()
rule nested_list_metadata_gap()
= eol() / comment_line()
rule nested_unordered_child_after_metadata(offset: usize, current_marker: &str, parent_ordered_marker: Option<&'input str>)
= (attribute_or_anchor_line_match() eol()*)+ nested_list_metadata_gap()* (
!at_ancestor_ordered_marker(parent_ordered_marker)
&(whitespace()* ordered_list_marker() whitespace())
/ &at_deeper_unordered_marker(current_marker)
/ !at_callout_parent_item(offset) at_callout_list_item()
)
rule nested_ordered_child_after_metadata(offset: usize, current_marker: &str, parent_unordered_marker: Option<&'input str>)
= (attribute_or_anchor_line_match() eol()*)+ nested_list_metadata_gap()* (
!at_ancestor_unordered_marker(parent_unordered_marker)
&(whitespace()* unordered_list_marker() whitespace())
/ &at_deeper_ordered_marker(current_marker)
/ !at_callout_parent_item(offset) at_callout_list_item()
)
rule at_list_item_start() = whitespace()* (unordered_list_marker() / ordered_list_marker()) whitespace()
rule at_section_start() = (attribute_or_anchor_line_match() eol()*)* atx_heading_prefix()
rule at_ordered_marker_ahead() = eol()+ whitespace()* ordered_list_marker()
rule at_unordered_marker_ahead() = eol()+ whitespace()* unordered_list_marker()
rule at_ancestor_ordered_marker(ancestor: Option<&'input str>)
= whitespace()* marker:ordered_list_marker() whitespace() {?
match ancestor {
Some(m) if marker.len() <= m.len() => Ok(()),
_ => Err("not ancestor")
}
}
rule at_ancestor_unordered_marker(ancestor: Option<&'input str>)
= whitespace()* marker:unordered_list_marker() whitespace() {?
match ancestor {
Some(m) if marker.len() <= m.len() => Ok(()),
_ => Err("not ancestor")
}
}
rule at_shallower_unordered_marker(base_marker: &str)
= whitespace()* marker:unordered_list_marker() whitespace() {?
if marker.len() < base_marker.len() { Ok(()) } else { Err("same-or-deeper") }
}
rule at_shallower_ordered_marker(base_marker: &str)
= whitespace()* marker:ordered_list_marker() whitespace() {?
if marker.len() < base_marker.len() { Ok(()) } else { Err("same-or-deeper") }
}
rule at_deeper_unordered_marker(base_marker: &str)
= whitespace()* marker:unordered_list_marker() whitespace() {?
if marker.len() > base_marker.len() { Ok(()) } else { Err("same-or-shallower") }
}
rule at_deeper_ordered_marker(base_marker: &str)
= whitespace()* marker:ordered_list_marker() whitespace() {?
if marker.len() > base_marker.len() { Ok(()) } else { Err("same-or-shallower") }
}
rule at_list_separator()
= eol()*<2,> at_list_separator_content()
rule at_list_separator_content()
= "//" [^'\n']* (&eol() / ![_]) / whitespace()* "[" whitespace()* "]" whitespace()* (&eol() / ![_])
rule unordered_list_principal_continuation(offset: usize, current_marker: &str, parent_ordered_marker: Option<&'input str>) -> &'input str
= eol()
!(
&eol()
/ &at_list_item_start()
/ &"+"
/ &at_section_start()
/ &at_list_separator_content()
/ &nested_unordered_child_after_metadata(offset, current_marker, parent_ordered_marker)
/ at_callout_list_item()
)
line:$((!eol() [_])*) { line }
rule ordered_list_principal_continuation(offset: usize, current_marker: &str, parent_unordered_marker: Option<&'input str>) -> &'input str
= eol()
!(
&eol()
/ &at_list_item_start()
/ &"+"
/ &at_section_start()
/ &at_list_separator_content()
/ &nested_ordered_child_after_metadata(offset, current_marker, parent_unordered_marker)
/ at_callout_list_item()
)
line:$((!eol() [_])*) { line }
rule at_dlist_block_boundary()
= eol()*<2,> &(
("[" ![']' | '['] [^']' | '\n']+ "]" whitespace()* eol())
/ ("[[" [^']']+ "]]" whitespace()* eol())
/ ("." ![' ' | '\t' | '\n' | '\r' | '.'] [^'\n']* eol())
)
rule unordered_list(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_ordered_marker: Option<&'input str>, allow_continuation: bool, is_nested: bool) -> Result<Block<'input>, Error>
= whitespace()* marker_start:position!() base_marker:$(unordered_list_marker()) &whitespace()
first:unordered_list_item_after_marker(offset, block_metadata, allow_continuation, base_marker, marker_start, parent_ordered_marker)
rest:(unordered_list_rest_item(offset, block_metadata, parent_ordered_marker, allow_continuation, base_marker))*
{
tracing::debug!("Found unordered list block");
let mut content = vec![first?];
for item in rest {
content.push(item?);
}
let end = content.last().map_or(span_end, |(_, item_end)| *item_end);
let items: Vec<ListItem<'input>> = content.into_iter().map(|(item, _)| item).collect();
let marker = items.first().map_or("", |item| item.marker);
Ok(Block::UnorderedList(UnorderedList {
title: if is_nested { Title::default() } else { block_metadata.title.clone() },
metadata: if is_nested { BlockMetadata::default() } else { block_metadata.metadata.clone() },
items,
marker,
location: state.create_location(start+offset, end+offset),
}))
}
rule unordered_list_item_after_marker(offset: usize, block_metadata: &BlockParsingMetadata<'input>, allow_continuation: bool, marker: &'input str, marker_start: usize, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= list_continuation_allowed(allow_continuation)
item:unordered_list_item_with_continuation_after_marker(offset, block_metadata, marker, marker_start, parent_ordered_marker) { item }
/ item:unordered_list_item_no_continuation_after_marker(offset, block_metadata, marker, marker_start, parent_ordered_marker) { item }
rule parent_is_some(parent: Option<&'input str>) -> ()
= {? if parent.is_some() { Ok(()) } else { Err("parent_is_none") } }
rule parent_is_none(parent: Option<&'input str>) -> ()
= {? if parent.is_none() { Ok(()) } else { Err("parent_is_some") } }
rule unordered_list_rest_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_ordered_marker: Option<&'input str>, allow_continuation: bool, base_marker: &str) -> Result<(ListItem<'input>, usize), Error>
= parent_is_some(parent_ordered_marker) !at_list_separator() !eol() comment_line()* !at_ordered_marker_ahead() item:unordered_list_item(offset, block_metadata, allow_continuation, parent_ordered_marker)
{ item }
/ parent_is_some(parent_ordered_marker) !at_list_separator() eol()+ comment_line()* !at_shallower_unordered_marker(base_marker) !at_ordered_marker_ahead() item:unordered_list_item(offset, block_metadata, allow_continuation, parent_ordered_marker)
{ item }
/ parent_is_none(parent_ordered_marker) !at_list_separator() !eol() comment_line()* item:unordered_list_item(offset, block_metadata, allow_continuation, parent_ordered_marker)
{ item }
/ parent_is_none(parent_ordered_marker) !at_list_separator() eol()+ comment_line()* !at_shallower_unordered_marker(base_marker) item:unordered_list_item(offset, block_metadata, allow_continuation, parent_ordered_marker)
{ item }
rule ordered_list(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_unordered_marker: Option<&'input str>, allow_continuation: bool, is_nested: bool) -> Result<Block<'input>, Error>
= whitespace()* marker_start:position!() base_marker:$(ordered_list_marker()) &whitespace()
first:ordered_list_item_after_marker(offset, block_metadata, allow_continuation, base_marker, marker_start, parent_unordered_marker)
rest:(ordered_list_rest_item(offset, block_metadata, parent_unordered_marker, allow_continuation, base_marker))*
{
tracing::debug!("Found ordered list block");
let mut content = vec![first?];
for item in rest {
content.push(item?);
}
let end = content.last().map_or(span_end, |(_, item_end)| *item_end);
let items: Vec<ListItem<'input>> = content.into_iter().map(|(item, _)| item).collect();
let marker = items.first().map_or("", |item| item.marker);
Ok(Block::OrderedList(OrderedList {
title: if is_nested { Title::default() } else { block_metadata.title.clone() },
metadata: if is_nested { BlockMetadata::default() } else { block_metadata.metadata.clone() },
items,
marker,
location: state.create_location(start+offset, end+offset),
}))
}
rule ordered_list_item_after_marker(offset: usize, block_metadata: &BlockParsingMetadata<'input>, allow_continuation: bool, marker: &'input str, marker_start: usize, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= list_continuation_allowed(allow_continuation)
item:ordered_list_item_with_continuation_after_marker(offset, block_metadata, marker, marker_start, parent_unordered_marker) { item }
/ item:ordered_list_item_no_continuation_after_marker(offset, block_metadata, marker, marker_start, parent_unordered_marker) { item }
rule ordered_list_rest_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_unordered_marker: Option<&'input str>, allow_continuation: bool, base_marker: &str) -> Result<(ListItem<'input>, usize), Error>
= parent_is_some(parent_unordered_marker) !at_list_separator() !eol() comment_line()* !at_unordered_marker_ahead() item:ordered_list_item(offset, block_metadata, allow_continuation, parent_unordered_marker)
{ item }
/ parent_is_some(parent_unordered_marker) !at_list_separator() eol()+ comment_line()* !at_shallower_ordered_marker(base_marker) !at_unordered_marker_ahead() item:ordered_list_item(offset, block_metadata, allow_continuation, parent_unordered_marker)
{ item }
/ parent_is_none(parent_unordered_marker) !at_list_separator() !eol() comment_line()* item:ordered_list_item(offset, block_metadata, allow_continuation, parent_unordered_marker)
{ item }
/ parent_is_none(parent_unordered_marker) !at_list_separator() eol()+ comment_line()* !at_shallower_ordered_marker(base_marker) item:ordered_list_item(offset, block_metadata, allow_continuation, parent_unordered_marker)
{ item }
rule unordered_list_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>, allow_continuation: bool, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= list_continuation_allowed(allow_continuation)
item:unordered_list_item_with_continuation(offset, block_metadata, parent_ordered_marker) { item }
/ item:unordered_list_item_no_continuation(offset, block_metadata, parent_ordered_marker) { item }
rule unordered_list_item_with_continuation(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()*
marker:unordered_list_marker()
whitespace()
checked:checklist_item()?
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:unordered_list_principal_continuation(offset, marker, parent_ordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:unordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_ordered_marker) { nested_content })?
explicit_continuations:(!at_list_separator() cont:(
list_explicit_continuation_immediate(offset, block_metadata)
/ list_explicit_continuation_ancestor(offset, block_metadata)
) { cont })*
list_dangling_continuation()?
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found unordered list item");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(explicit_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked,
location: state.create_location(span_start+offset, actual_end+offset),
}, actual_end))
}
rule unordered_list_item_no_continuation(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()*
marker:unordered_list_marker()
whitespace()
checked:checklist_item()?
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:unordered_list_principal_continuation(offset, marker, parent_ordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:unordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_ordered_marker) { nested_content })?
immediate_continuations:(!at_list_separator() cont:list_explicit_continuation_immediate(offset, block_metadata) { cont })*
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found unordered list item (immediate continuation only)");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(immediate_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked,
location: state.create_location(span_start+offset, actual_end+offset),
}, actual_end))
}
rule unordered_list_item_with_continuation_after_marker(offset: usize, block_metadata: &BlockParsingMetadata<'input>, marker: &'input str, marker_start: usize, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()
checked:checklist_item()?
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:unordered_list_principal_continuation(offset, marker, parent_ordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:unordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_ordered_marker) { nested_content })?
explicit_continuations:(!at_list_separator() cont:(
list_explicit_continuation_immediate(offset, block_metadata)
/ list_explicit_continuation_ancestor(offset, block_metadata)
) { cont })*
list_dangling_continuation()?
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found unordered list item (after marker)");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(explicit_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked,
location: state.create_location(marker_start+offset, actual_end+offset),
}, actual_end))
}
rule unordered_list_item_no_continuation_after_marker(offset: usize, block_metadata: &BlockParsingMetadata<'input>, marker: &'input str, marker_start: usize, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()
checked:checklist_item()?
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:unordered_list_principal_continuation(offset, marker, parent_ordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:unordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_ordered_marker) { nested_content })?
immediate_continuations:(!at_list_separator() cont:list_explicit_continuation_immediate(offset, block_metadata) { cont })*
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found unordered list item (after marker, immediate only)");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(immediate_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked,
location: state.create_location(marker_start+offset, actual_end+offset),
}, actual_end))
}
rule unordered_list_item_nested_after_principal(offset: usize, block_metadata: &BlockParsingMetadata<'input>, current_marker: &'input str, parent_ordered_marker: Option<&'input str>) -> Option<Result<Block<'input>, Error>>
= eol() nested:(
unordered_list_item_nested_content_with_metadata(offset, block_metadata, current_marker, parent_ordered_marker)
/ unordered_list_item_nested_content(offset, block_metadata, current_marker, parent_ordered_marker)
) { nested }
/ eol()+ !at_callout_list_item() nested:unordered_list_item_nested_content(offset, block_metadata, current_marker, parent_ordered_marker) { nested }
rule unordered_list_item_nested_content_with_metadata(offset: usize, block_metadata: &BlockParsingMetadata<'input>, current_marker: &'input str, parent_ordered_marker: Option<&'input str>) -> Option<Result<Block<'input>, Error>>
= nested_start:position!()
metadata:parsed_nested_list_metadata(offset, block_metadata.parent_section_level)
nested_list_metadata_gap()*
list:(
!at_ancestor_ordered_marker(parent_ordered_marker)
list:ordered_list(nested_start, offset, &metadata, Some(current_marker), false, false) { list }
/ &at_deeper_unordered_marker(current_marker)
list:unordered_list_nested(nested_start, offset, &metadata, current_marker, parent_ordered_marker, true) { list }
/ !at_callout_parent_item(offset)
list:callout_list(nested_start, offset, &metadata) { list }
)
{
Some(list)
}
rule unordered_list_item_nested_content(offset: usize, block_metadata: &BlockParsingMetadata<'input>, current_marker: &'input str, parent_ordered_marker: Option<&'input str>) -> Option<Result<Block<'input>, Error>>
= !at_ancestor_ordered_marker(parent_ordered_marker) nested_start:position!() list:ordered_list(nested_start, offset, block_metadata, Some(current_marker), false, true) {
Some(list)
}
/ &at_deeper_unordered_marker(current_marker)
nested_start:position!()
list:unordered_list_nested(nested_start, offset, block_metadata, current_marker, parent_ordered_marker, false)
{
Some(list)
}
/ list:nested_callout_list(offset, block_metadata) { Some(list) }
rule unordered_list_nested(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_marker: &str, parent_ordered_marker: Option<&'input str>, has_own_metadata: bool) -> Result<Block<'input>, Error>
= &at_deeper_unordered_marker(parent_marker)
whitespace()* marker_start:position!() base_marker:$(unordered_list_marker()) &whitespace()
first:unordered_list_item_after_marker(offset, block_metadata, false, base_marker, marker_start, parent_ordered_marker)
rest:(unordered_list_nested_rest_item(offset, block_metadata, parent_marker, base_marker, parent_ordered_marker))*
{
tracing::debug!("Found nested unordered list block");
let mut content = vec![first?];
for item in rest {
content.push(item?);
}
let end = content.last().map_or(span_end, |(_, item_end)| *item_end);
let items: Vec<ListItem<'input>> = content.into_iter().map(|(item, _)| item).collect();
let marker = items.first().map_or("", |item| item.marker);
Ok(Block::UnorderedList(UnorderedList {
title: if has_own_metadata { block_metadata.title.clone() } else { Title::default() },
metadata: if has_own_metadata { block_metadata.metadata.clone() } else { BlockMetadata::default() },
items,
marker,
location: state.create_location(start+offset, end+offset),
}))
}
rule unordered_list_nested_rest_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_marker: &str, base_marker: &str, parent_ordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= !at_list_separator() !eol() comment_line()*
!at_shallower_or_equal_unordered_marker(parent_marker)
item:unordered_list_item(offset, block_metadata, false, parent_ordered_marker)
{ item }
/ !at_list_separator() eol()+ comment_line()*
!at_shallower_or_equal_unordered_marker(parent_marker)
!at_deeper_unordered_marker(base_marker)
item:unordered_list_item(offset, block_metadata, false, parent_ordered_marker)
{ item }
rule at_shallower_or_equal_unordered_marker(parent_marker: &str)
= whitespace()* marker:unordered_list_marker() whitespace() {?
if marker.len() <= parent_marker.len() { Ok(()) } else { Err("deeper") }
}
rule ordered_list_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>, allow_continuation: bool, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= list_continuation_allowed(allow_continuation)
item:ordered_list_item_with_continuation(offset, block_metadata, parent_unordered_marker) { item }
/ item:ordered_list_item_no_continuation(offset, block_metadata, parent_unordered_marker) { item }
rule ordered_list_item_with_continuation(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()*
marker:ordered_list_marker()
whitespace()
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:ordered_list_principal_continuation(offset, marker, parent_unordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:ordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_unordered_marker) { nested_content })?
explicit_continuations:(!at_list_separator() cont:(
list_explicit_continuation_immediate(offset, block_metadata)
/ list_explicit_continuation_ancestor(offset, block_metadata)
) { cont })*
list_dangling_continuation()?
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found ordered list item");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(explicit_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked: None,
location: state.create_location(span_start+offset, actual_end+offset),
}, actual_end))
}
rule ordered_list_item_no_continuation(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()*
marker:ordered_list_marker()
whitespace()
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:ordered_list_principal_continuation(offset, marker, parent_unordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:ordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_unordered_marker) { nested_content })?
immediate_continuations:(!at_list_separator() cont:list_explicit_continuation_immediate(offset, block_metadata) { cont })*
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found ordered list item (immediate continuation only)");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(immediate_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked: None,
location: state.create_location(span_start+offset, actual_end+offset),
}, actual_end))
}
rule ordered_list_item_with_continuation_after_marker(offset: usize, block_metadata: &BlockParsingMetadata<'input>, marker: &'input str, marker_start: usize, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:ordered_list_principal_continuation(offset, marker, parent_unordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:ordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_unordered_marker) { nested_content })?
explicit_continuations:(!at_list_separator() cont:(
list_explicit_continuation_immediate(offset, block_metadata)
/ list_explicit_continuation_ancestor(offset, block_metadata)
) { cont })*
list_dangling_continuation()?
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found ordered list item (after marker)");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(explicit_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked: None,
location: state.create_location(marker_start+offset, actual_end+offset),
}, actual_end))
}
rule ordered_list_item_no_continuation_after_marker(offset: usize, block_metadata: &BlockParsingMetadata<'input>, marker: &'input str, marker_start: usize, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= whitespace()
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:ordered_list_principal_continuation(offset, marker, parent_unordered_marker)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() nested_content:ordered_list_item_nested_after_principal(offset, block_metadata, marker, parent_unordered_marker) { nested_content })?
immediate_continuations:(!at_list_separator() cont:list_explicit_continuation_immediate(offset, block_metadata) { cont })*
{
tracing::debug!(first_line_len = first_line.len(), continuation_count = continuation_lines.len(), "found ordered list item (after marker, immediate only)");
let level = ListLevel::try_from(ListItem::parse_depth_from_marker(marker).unwrap_or(1))?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let principal = principal?;
let mut blocks = Vec::new();
if let Some(Some(Ok(nested_list))) = nested {
blocks.push(nested_list);
}
blocks.extend(immediate_continuations.into_iter().flatten());
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((ListItem {
principal,
blocks,
level,
marker,
checked: None,
location: state.create_location(marker_start+offset, actual_end+offset),
}, actual_end))
}
rule ordered_list_item_nested_after_principal(offset: usize, block_metadata: &BlockParsingMetadata<'input>, current_marker: &'input str, parent_unordered_marker: Option<&'input str>) -> Option<Result<Block<'input>, Error>>
= eol() nested:(
ordered_list_item_nested_content_with_metadata(offset, block_metadata, current_marker, parent_unordered_marker)
/ ordered_list_item_nested_content(offset, block_metadata, current_marker, parent_unordered_marker)
) { nested }
/ eol()+ !at_callout_list_item() nested:ordered_list_item_nested_content(offset, block_metadata, current_marker, parent_unordered_marker) { nested }
rule ordered_list_item_nested_content_with_metadata(offset: usize, block_metadata: &BlockParsingMetadata<'input>, current_marker: &'input str, parent_unordered_marker: Option<&'input str>) -> Option<Result<Block<'input>, Error>>
= nested_start:position!()
metadata:parsed_nested_list_metadata(offset, block_metadata.parent_section_level)
nested_list_metadata_gap()*
list:(
!at_ancestor_unordered_marker(parent_unordered_marker)
list:unordered_list(nested_start, offset, &metadata, Some(current_marker), false, false) { list }
/ &at_deeper_ordered_marker(current_marker)
list:ordered_list_nested(nested_start, offset, &metadata, current_marker, parent_unordered_marker, true) { list }
/ !at_callout_parent_item(offset)
list:callout_list(nested_start, offset, &metadata) { list }
)
{
Some(list)
}
rule ordered_list_item_nested_content(offset: usize, block_metadata: &BlockParsingMetadata<'input>, current_marker: &'input str, parent_unordered_marker: Option<&'input str>) -> Option<Result<Block<'input>, Error>>
= !at_ancestor_unordered_marker(parent_unordered_marker) nested_start:position!() list:unordered_list(nested_start, offset, block_metadata, Some(current_marker), false, true) {
Some(list)
}
/ &at_deeper_ordered_marker(current_marker)
nested_start:position!()
list:ordered_list_nested(nested_start, offset, block_metadata, current_marker, parent_unordered_marker, false)
{
Some(list)
}
/ list:nested_callout_list(offset, block_metadata) { Some(list) }
rule ordered_list_nested(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_marker: &str, parent_unordered_marker: Option<&'input str>, has_own_metadata: bool) -> Result<Block<'input>, Error>
= &at_deeper_ordered_marker(parent_marker)
whitespace()* marker_start:position!() base_marker:$(ordered_list_marker()) &whitespace()
first:ordered_list_item_after_marker(offset, block_metadata, false, base_marker, marker_start, parent_unordered_marker)
rest:(ordered_list_nested_rest_item(offset, block_metadata, parent_marker, base_marker, parent_unordered_marker))*
{
tracing::debug!("Found nested ordered list block");
let mut content = vec![first?];
for item in rest {
content.push(item?);
}
let end = content.last().map_or(span_end, |(_, item_end)| *item_end);
let items: Vec<ListItem<'input>> = content.into_iter().map(|(item, _)| item).collect();
let marker = items.first().map_or("", |item| item.marker);
Ok(Block::OrderedList(OrderedList {
title: if has_own_metadata { block_metadata.title.clone() } else { Title::default() },
metadata: if has_own_metadata { block_metadata.metadata.clone() } else { BlockMetadata::default() },
items,
marker,
location: state.create_location(start+offset, end+offset),
}))
}
rule ordered_list_nested_rest_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>, parent_marker: &str, base_marker: &str, parent_unordered_marker: Option<&'input str>) -> Result<(ListItem<'input>, usize), Error>
= !at_list_separator() !eol() comment_line()*
!at_shallower_or_equal_ordered_marker(parent_marker)
item:ordered_list_item(offset, block_metadata, false, parent_unordered_marker)
{ item }
/ !at_list_separator() eol()+ comment_line()*
!at_shallower_or_equal_ordered_marker(parent_marker)
!at_deeper_ordered_marker(base_marker)
item:ordered_list_item(offset, block_metadata, false, parent_unordered_marker)
{ item }
rule at_shallower_or_equal_ordered_marker(parent_marker: &str)
= whitespace()* marker:ordered_list_marker() whitespace() {?
if marker.len() <= parent_marker.len() { Ok(()) } else { Err("deeper") }
}
rule at_callout_list_item()
= &(callout_list_marker() whitespace()+ [^' ' | '\t' | '\r' | '\n'])
rule at_callout_parent_item(offset: usize)
= at_callout_list_item() {?
(state.callout_list_offset == Some(offset)).then_some(()).ok_or("not a callout parent")
}
rule enter_callout_list(offset: usize) -> (Option<usize>, Vec<CalloutRef>)
= {
let parent = state.callout_list_offset.replace(offset);
(parent, std::mem::take(&mut state.pending_callouts))
}
rule callout_list(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= at_callout_list_item() normal_paragraph_style(block_metadata)
context:enter_callout_list(offset)
first:callout_list_item(offset, block_metadata)
rest:(callout_list_rest_item(offset, block_metadata))*
{
let (parent, callouts) = context;
state.callout_list_offset = parent;
state.pending_callouts.clear();
tracing::debug!("Found callout list block");
let mut content = vec![first?];
for item in rest {
content.push(item?);
}
let end = content.last().map_or(span_end, |(_, _, item_end)| *item_end);
let mut auto_number = 0;
let mut items: Vec<CalloutListItem> = Vec::with_capacity(content.len());
for (expected_number, (mut item, marker, _end)) in (1..).zip(content) {
let actual_number = if marker == "<.>" {
auto_number += 1;
auto_number.to_string()
} else {
marker.trim_start_matches('<').trim_end_matches('>').to_string()
};
if actual_number != expected_number.to_string() {
state.add_generic_warning_at(
format!(
"callout list item index: expected {expected_number}, got {actual_number}"
),
item.location.clone(),
);
}
item.callout.number = expected_number;
let callout_exists = callouts.iter()
.any(|c| c.number == expected_number);
if !callout_exists {
state.add_generic_warning_at(
format!("no callout found for <{expected_number}>"),
item.location.clone(),
);
}
items.push(item);
}
Ok(Block::CalloutList(CalloutList {
title: block_metadata.title.clone(),
metadata: block_metadata.metadata.clone(),
items,
location: state.create_location(start+offset, end+offset),
}))
}
rule callout_list_rest_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<(CalloutListItem<'input>, String, usize), Error>
= eol()+ item:callout_list_item(offset, block_metadata)
{?
Ok(item)
}
rule nested_callout_list(offset: usize, metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= !at_callout_parent_item(offset) start:position!()
child_metadata:callout_child_metadata(metadata)
list:callout_list(start, offset, &child_metadata) { list }
rule callout_child_marker(offset: usize)
= !at_callout_parent_item(offset) (
&(whitespace()* (unordered_list_marker() / ordered_list_marker()) whitespace())
/ check_line_is_description_list(offset)
)
rule callout_list_nested(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= eol() child:(
&((attribute_or_anchor_line_match() eol()*)+ callout_child_marker(offset))
start:position!()
metadata:parsed_nested_list_metadata(offset, block_metadata.parent_section_level)
list:callout_child_list(start, offset, &metadata) { list }
/ eol()* callout_child_marker(offset) start:position!()
metadata:callout_child_metadata(block_metadata)
list:callout_child_list(start, offset, &metadata) { list }
) { child }
rule callout_child_metadata(parent: &BlockParsingMetadata<'input>) -> BlockParsingMetadata<'input>
= {
BlockParsingMetadata {
parent_section_level: parent.parent_section_level,
substitutions: parent.substitutions,
hardbreaks: parent.hardbreaks,
..BlockParsingMetadata::default()
}
}
rule callout_child_list(start: usize, offset: usize, metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= unordered_list(start, offset, metadata, None, false, false)
/ ordered_list(start, offset, metadata, None, false, false)
/ description_list(start, offset, metadata, false)
rule callout_list_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<(CalloutListItem<'input>, String, usize), Error>
= at_callout_list_item()
marker:callout_list_marker()
whitespace()
first_line_start:position!()
first_line:$((!(eol()) [_])*)
continuation_lines:(
eol()
!at_callout_list_item()
!check_line_is_description_list(offset)
!(whitespace()* (unordered_list_marker() / ordered_list_marker() / section_level_marker() whitespace() / "[" / "+" whitespace()* eol() / eol()))
line:$((!(eol()) [_])*)
{ line }
)*
first_line_end:position!()
principal:list_item_principal(offset, block_metadata, first_line_start, first_line_end, first_line, &continuation_lines)
nested:(!at_list_separator() child:callout_list_nested(offset, block_metadata) { child })?
explicit_continuations:(!at_list_separator() cont:(
list_explicit_continuation_immediate(offset, block_metadata)
/ list_explicit_continuation_ancestor(offset, block_metadata)
) { cont })*
list_dangling_continuation()?
{
let principal = principal?;
let item_end = calculate_item_end(first_line.is_empty() && continuation_lines.is_empty(), span_start, first_line_end);
let blocks = nested
.into_iter()
.chain(explicit_continuations)
.collect::<Result<Vec<_>, _>>()?;
let location = state.create_location(span_start+offset, item_end+offset);
let callout = if marker == "<.>" {
CalloutRef::auto(0, location.clone()) } else {
CalloutRef::explicit(0, location.clone())
};
let actual_end = if blocks.is_empty() { item_end } else { span_end.saturating_sub(1) };
Ok((CalloutListItem {
callout,
principal,
blocks,
location: state.create_location(span_start+offset, actual_end+offset),
}, marker.to_string(), actual_end))
}
rule list_item_principal(offset: usize, metadata: &BlockParsingMetadata<'input>, start: usize, end: usize, first: &'input str, rest: &[&'input str]) -> Result<Vec<InlineNode<'input>>, Error>
= {
let text = assemble_principal_text(state, first, rest);
if text.trim().is_empty() {
Ok(Vec::new())
} else {
process_inlines(state, metadata, start, end, offset, text).map(|(nodes, _)| nodes)
}
}
rule checklist_item() -> ListItemCheckedStatus
= checked:(("[x]" / "[X]" / "[*]") { ListItemCheckedStatus::Checked } / "[ ]" { ListItemCheckedStatus::Unchecked }) whitespace()
{
checked
}
rule check_start_of_description_list(offset: usize)
= pos:position!() {?
if find_dlist_marker(state.input.as_bytes(), pos + offset, true, true) {
Ok(())
} else {
Err("no dlist marker before next blank line")
}
}
rule check_line_is_description_list(offset: usize)
= pos:position!() {?
if find_dlist_marker(state.input.as_bytes(), pos + offset, false, true) {
Ok(())
} else {
Err("no dlist marker on current line")
}
}
rule check_start_of_description_list_in_context(offset: usize, scan_across_eol: bool)
= pos:position!() {?
if find_dlist_marker(state.input.as_bytes(), pos + offset, scan_across_eol, true) {
Ok(())
} else {
Err("no dlist marker in this block context")
}
}
rule description_list(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>, scan_across_eol: bool) -> Result<Block<'input>, Error>
= check_start_of_description_list_in_context(offset, scan_across_eol)
first_item:description_list_item(offset, block_metadata)
additional_items:description_list_additional_items(offset, block_metadata)*
{
tracing::debug!("Found description list block with auto-attachment support");
let mut items = vec![first_item?];
for additional in additional_items {
items.push(additional?);
}
let actual_end = items.last().map_or(span_end, |item| {
let loc_end = item.location.absolute_end;
loc_end - offset
});
Ok(Block::DescriptionList(DescriptionList {
title: block_metadata.title.clone(),
metadata: block_metadata.metadata.clone(),
items: build_description_list_topology(items),
location: state.create_location(start+offset, actual_end+offset),
}))
}
rule description_list_additional_items(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<DescriptionListItem<'input>, Error>
= !at_dlist_block_boundary()
eol()*
!attribute_or_anchor_line_match()
!at_callout_list_item()
check_start_of_description_list(offset)
item:description_list_item(offset, block_metadata)
{
tracing::debug!("Found additional description list item");
item
}
rule description_list_item(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<DescriptionListItem<'input>, Error>
= term_start:position!()
term:$((!(description_list_marker() (eol() / " " / ![_]) / eol()*<2,2>) [_])+)
term_end:position!()
delim_start:position!() delimiter:description_list_marker() delim_end:position!()
whitespace()?
principal_start:position!()
principal_content:$(
(!eol() [_])*
(eol()
!eol() !at_callout_list_item()
!check_line_is_description_list(offset)
!(whitespace()* (unordered_list_marker() / ordered_list_marker()) whitespace()) !("+" (whitespace() / eol() / ![_])) !example_delimiter() !listing_delimiter()
!literal_delimiter()
!sidebar_delimiter()
!quote_delimiter()
!pass_delimiter()
!comment_delimiter()
!table_delimiter()
!(open_delimiter() (whitespace()* eol()))
!markdown_code_delimiter()
!attribute_or_anchor_line_match() !heading_boundary(offset) (!eol() [_])+ )*
)
principal:description_list_principal(offset, block_metadata, term, term_start, term_end, principal_content, principal_start)
attached_content:description_list_attached_content(offset, block_metadata)*
{
tracing::debug!("parsing description list item with auto-attachment");
let (term, principal_text) = principal?;
let mut description = Vec::with_capacity(attached_content.len());
for content in attached_content {
match content {
Ok(blocks) => description.extend(blocks),
Err(e) => {
state.add_warning(Warning::new(
WarningKind::ContentRecovery {
message: format!("discarded attached content: {e}").into(),
},
e.source_location().cloned(),
));
tracing::error!("Error processing attached content");
}
}
}
let actual_end = description.last().map_or_else(
|| {
if principal_content.is_empty() {
principal_start
} else {
principal_start + principal_content.len()
}
},
|b| {
let loc = b.location();
loc.absolute_end - offset
},
);
let delimiter_location = state.create_block_location(delim_start, delim_end, offset);
Ok(DescriptionListItem {
anchors: vec![],
term,
delimiter,
delimiter_location: Some(delimiter_location),
principal_text,
description,
location: state.create_location(span_start+offset, actual_end+offset),
})
}
rule description_list_principal(offset: usize, block_metadata: &BlockParsingMetadata<'input>, term: &'input str, term_start: usize, term_end: usize, principal_content: &'input str, principal_start: usize) -> Result<(Vec<InlineNode<'input>>, Vec<InlineNode<'input>>), Error>
= {
let trimmed_term = term.trim();
let leading_whitespace = term.len() - term.trim_start().len();
let term_start = term_start + leading_whitespace;
let term_end = term_end - (term.len() - term.trim_end().len());
let (term, _) = process_inlines(
state,
block_metadata,
term_start,
term_end,
offset,
trimmed_term,
)?;
let trimmed_principal = principal_content.trim();
let principal_text = if trimmed_principal.is_empty() {
Vec::new()
} else {
let content_start = principal_start + principal_content.len()
- principal_content.trim_start().len();
let (principal, _) = process_inlines(
state,
block_metadata,
content_start,
content_start + trimmed_principal.len(),
offset,
trimmed_principal,
)?;
principal
};
Ok((term, principal_text))
}
rule description_list_attached_content(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Vec<Block<'input>>, Error>
= eol() content:(
description_list_explicit_continuation(offset, block_metadata)
/ description_list_auto_attached_list(offset, block_metadata)
)
{
content
}
rule description_list_auto_attached_list(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Vec<Block<'input>>, Error>
= eol()* list:description_list_auto_attached_list_with_metadata(offset, block_metadata) { list }
/ eol()* &(whitespace()* (unordered_list_marker() / ordered_list_marker()) whitespace())
list_start:position!()
list:(unordered_list(list_start, offset, block_metadata, None, true, true) / ordered_list(list_start, offset, block_metadata, None, true, true))
{
tracing::debug!("Auto-attaching list to description list item");
Ok(vec![list?])
}
/ list:nested_callout_list(offset, block_metadata) { Ok(vec![list?]) }
rule description_list_auto_attached_list_with_metadata(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Vec<Block<'input>>, Error>
= &((attribute_or_anchor_line_match() eol()*)+ (whitespace()* (unordered_list_marker() / ordered_list_marker()) whitespace() / !at_callout_parent_item(offset) at_callout_list_item()))
list_start:position!()
metadata:parsed_nested_list_metadata(offset, block_metadata.parent_section_level)
list:(callout_list(list_start, offset, &metadata) / unordered_list(list_start, offset, &metadata, None, true, false) / ordered_list(list_start, offset, &metadata, None, true, false))
{
Ok(vec![list?])
}
rule description_list_explicit_continuation(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Vec<Block<'input>>, Error>
= continuations:(
eol()* "+" eol()
block:block_in_continuation(offset, block_metadata.parent_section_level)
{ block }
)+
{
tracing::debug!(count = continuations.len(), "Description list explicit continuation blocks");
Ok(continuations.into_iter().filter_map(Result::ok).collect())
}
rule list_explicit_continuation_immediate(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= eol() !eol() "+" eol()
block:block_in_continuation(offset, block_metadata.parent_section_level)
{
tracing::debug!("List immediate continuation block (0 empty lines)");
block
}
rule list_explicit_continuation_ancestor(offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= eol() eol()+ "+" eol()
block:block_in_continuation(offset, block_metadata.parent_section_level)
{
tracing::debug!("List ancestor continuation block (1+ empty lines)");
block
}
rule list_dangling_continuation()
= eol()+ "+" whitespace()* eol()? &(eol() / ![_] / trailing_block_metadata_match())
{
tracing::debug!("Dropped dangling list continuation marker");
}
rule quoted_paragraph(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= content_start:position!()
"\"" quoted_content:$((!"\"" [_])+) "\""
eol()
"-- " attr_start:position!() attribution_line:$([^'\n']+)
{
tracing::debug!("found quoted paragraph");
let (attr_str, cite_str): (&'input str, Option<&'input str>) = match attribution_line.split_once(',') {
Some((attr, cite)) => (state.intern_str(attr.trim()), Some(state.intern_str(cite.trim()))),
None => (state.intern_str(attribution_line.trim()), None),
};
let attr_end_offset = attr_start + attr_str.len();
let (attr_inlines, _) = process_inlines(
state,
block_metadata,
attr_start,
attr_end_offset,
offset,
attr_str,
)?;
let cite_inlines = if let Some(cite) = cite_str {
let cite_offset_in_line = attribution_line.find(',').unwrap_or(0) + 1;
let cite_raw_start = attr_start + cite_offset_in_line + (attribution_line[cite_offset_in_line..].len() - attribution_line[cite_offset_in_line..].trim_start().len());
let cite_pos = PositionWithOffset {
offset: cite_raw_start,
position: state.line_map.offset_to_position(cite_raw_start, state.input),
};
let (cite_inlines, _) = process_inlines(
state,
block_metadata,
cite_pos.offset,
cite_raw_start + cite.len(),
offset,
cite,
)?;
Some(cite_inlines)
} else {
None
};
let blocks = document_parser::blocks(quoted_content, state, content_start + offset, block_metadata.parent_section_level, None).unwrap_or_else(|e| {
adjust_and_log_parse_error(&e, quoted_content, content_start + offset, state, "Error parsing content as blocks in quoted paragraph");
Ok(Vec::new())
})?;
let mut metadata = block_metadata.metadata.clone();
metadata.style = Some("quote");
metadata.attribution = Some(Attribution::new(attr_inlines));
if let Some(inlines) = cite_inlines {
metadata.citetitle = Some(CiteTitle::new(inlines));
}
Ok(Block::DelimitedBlock(DelimitedBlock {
source_text: None,
metadata,
delimiter: "\"",
inner: DelimitedBlockType::DelimitedQuote(blocks),
title: block_metadata.title.clone(),
location: state.create_block_location(start, span_end, offset),
open_delimiter_location: None,
close_delimiter_location: None,
}))
}
rule markdown_blockquote(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= lines:markdown_blockquote_content_line()+ attribution:markdown_blockquote_attribution()?
{
tracing::debug!("found markdown blockquote");
let content: &'input str = state.intern_join(lines.iter(), "\n");
let content_start = start;
let mut metadata = block_metadata.metadata.clone();
metadata.style = Some("quote");
if let Some((author, author_start, citation)) = attribution {
let author: &'input str = state.intern_str(&author);
let author_pos = PositionWithOffset {
offset: author_start,
position: state.line_map.offset_to_position(author_start, state.input),
};
let attr_end_offset = author_start + author.len();
let (attr_inlines, _) = process_inlines(
state,
block_metadata,
author_pos.offset,
attr_end_offset,
offset,
author,
)?;
metadata.attribution = Some(Attribution::new(attr_inlines));
if let Some((cite, cite_start)) = citation {
let cite: &'input str = state.intern_str(&cite);
let cite_pos = PositionWithOffset {
offset: cite_start,
position: state.line_map.offset_to_position(cite_start, state.input),
};
let (cite_inlines, _) = process_inlines(
state,
block_metadata,
cite_pos.offset,
cite_start + cite.len(),
offset,
cite,
)?;
metadata.citetitle = Some(CiteTitle::new(cite_inlines));
}
}
let location = state.create_block_location(start, span_end, offset);
let blocks = if content.trim().is_empty() {
Vec::new()
} else {
document_parser::blocks(content, state, content_start + offset, block_metadata.parent_section_level, None).unwrap_or_else(|e| {
adjust_and_log_parse_error(&e, content, content_start + offset, state, "Error parsing content as blocks in markdown blockquote");
Ok(Vec::new())
})?
};
Ok(Block::DelimitedBlock(DelimitedBlock {
source_text: None,
metadata,
delimiter: ">",
inner: DelimitedBlockType::DelimitedQuote(blocks),
title: block_metadata.title.clone(),
location,
open_delimiter_location: None,
close_delimiter_location: None,
}))
}
rule markdown_blockquote_content_line() -> &'input str
= "> " content:$([^'\n']*) eol() &">" { content }
/ "> " !("-- ") content:$([^'\n']*) (eol() / ![_]) { content }
/ ">" eol() &">" { "" }
/ ">" eol() { "" }
/ ">" ![_] { "" }
rule markdown_blockquote_attribution() -> (String, usize, Option<(String, usize)>)
= "> -- " author_start:position!() author:$([^(',' | '\n')]+) ", " cite_start:position!() citation:$([^'\n']+) ((eol() !">") / ![_]) {
(author.trim().to_string(), author_start, Some((citation.trim().to_string(), cite_start)))
}
/ "> -- " author_start:position!() author:$([^'\n']+) ((eol() !">") / ![_]) {
(author.trim().to_string(), author_start, None)
}
rule styled_verbatim_paragraph(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= !normal_paragraph_style(block_metadata) block:paragraph(start, offset, block_metadata) { block }
rule verbatim_paragraph_content() -> &'input str
= content:$((!(
eol()*<2,>
/ eol()* ![_]
/ eol() "+" whitespace()* (eol() / ![_])
) [_])+) { content }
rule paragraph(start: usize, offset: usize, block_metadata: &BlockParsingMetadata<'input>) -> Result<Block<'input>, Error>
= admonition:(normal_paragraph_style(block_metadata) value:admonition() { value })?
content_start:position!()
content:(
!normal_paragraph_style(block_metadata) text:verbatim_paragraph_content() { text }
/ $((
"[[" (!eol() [_])*
/ !(
eol()*<2,>
/ eol()* ![_]
/ eol() &attributes_line()
/ eol() &anchor_line_match()
/ eol() example_delimiter()
/ eol() listing_delimiter()
/ eol() literal_delimiter()
/ eol() sidebar_delimiter()
/ eol() quote_delimiter()
/ eol() pass_delimiter()
/ eol() table_delimiter()
/ eol() markdown_code_delimiter()
/ eol() comment_delimiter()
/ eol() open_delimiter() &(whitespace()* eol())
/ eol() at_callout_parent_item(offset)
/ eol() !at_callout_list_item() list(start, offset, block_metadata)
/ eol() &("+" whitespace()* (eol() / ![_])) / eol()* &heading_boundary(offset)
) [_]
)+))
{
let is_styled_verbatim = matches!(
block_metadata.metadata.style,
Some("source" | "listing" | "literal")
);
if block_metadata.metadata.style == Some("comment") {
return Ok(Block::Comment(Comment {
kind: CommentKind::Paragraph,
content,
location: state.create_block_location(start, span_end, offset),
}));
}
if content.starts_with(' ')
&& !is_styled_verbatim
&& block_metadata.metadata.style != Some("verse")
{
return get_literal_paragraph(state, content, start, content_start, span_end, offset, block_metadata);
}
let source_text = content;
let content = if is_styled_verbatim {
let content_location =
state.create_block_location(content_start, span_end, offset);
let (verbatim_content, callouts) = resolve_verbatim_callouts(
state,
content,
content_location,
block_metadata
.substitutions
.enabled(&Substitution::Callouts),
!block_metadata.metadata.attributes.contains_key("line-comment"),
);
let content = if callouts.is_empty() {
let verbatim_metadata = BlockParsingMetadata {
substitutions: verbatim_substitutions(block_metadata),
..BlockParsingMetadata::default()
};
process_inlines(
state,
&verbatim_metadata,
content_start,
span_end,
offset,
content,
)?
.0
} else {
resolve_verbatim_inlines(state, block_metadata, verbatim_content)?
};
state.pending_callouts.extend(callouts);
content
} else {
process_inlines(
state,
block_metadata,
content_start,
span_end,
offset,
content,
)?
.0
};
let title: Title = if let Some(AttributeValue::String(title)) = block_metadata.metadata.attributes.get("title") {
vec![InlineNode::PlainText(Plain {
content: state.intern_cow(title.clone()),
location: state.create_location(start+offset, (start+offset).saturating_add(title.len()).saturating_sub(1)),
escaped: false,
})].into()
} else {
block_metadata.title.clone()
};
if let Some((variant, admonition_start, admonition_end)) = admonition {
let Ok(parsed_variant) = AdmonitionVariant::from_str(&variant) else {
tracing::error!("invalid admonition variant");
return Err(Error::InvalidAdmonitionVariant(
Box::new(state.create_error_source_location(state.create_location(admonition_start + offset, admonition_end + offset - 1))),
variant
));
};
tracing::debug!("found admonition block with variant");
Ok(Block::Admonition(Admonition{
metadata: block_metadata.metadata.clone(),
title,
blocks: vec![Block::Paragraph(Paragraph {
source_text: Some(source_text),
content,
metadata: block_metadata.metadata.clone(),
title: Title::default(),
location: state.create_block_location(content_start, span_end, offset),
})],
location: state.create_block_location(start, span_end, offset),
variant: parsed_variant,
}))
} else {
let mut metadata = block_metadata.metadata.clone();
metadata.move_positional_attributes_to_attributes();
tracing::debug!(node_count = content.len(), "found paragraph block");
Ok(Block::Paragraph(Paragraph {
source_text: Some(source_text),
content,
metadata,
title,
location: state.create_block_location(start, span_end, offset),
}))
}
}
rule admonition() -> (String, usize, usize)
= variant:$("NOTE" / "WARNING" / "TIP" / "IMPORTANT" / "CAUTION") ": "
{
(variant.to_string(), span_start, span_end)
}
rule warn_anchor_id_with_whitespace() -> ()
= &(
id:$([^'\'' | ',' | ']' | '.' | '#']+)
{?
if id.chars().any(char::is_whitespace) {
let location = state.create_location(span_start, span_end);
state.add_generic_warning_at(
format!("anchor id '{id}' contains whitespace which is not allowed, treating as literal text"),
location,
);
}
Err::<(), &'static str>("")
}
)
rule anchor() -> Anchor<'input>
= result:(
double_open_square_bracket() warn_anchor_id_with_whitespace()? id:$([^'\'' | ',' | ']' | ' ' | '\t' | '\n' | '\r']+) comma() reftext:$([^']']+) double_close_square_bracket() {
(id, Some(reftext))
} /
double_open_square_bracket() warn_anchor_id_with_whitespace()? id:$([^'\'' | ',' | ']' | ' ' | '\t' | '\n' | '\r']+) double_close_square_bracket() {
(id, None)
} /
open_square_bracket() "#" warn_anchor_id_with_whitespace()? id:$([^'\'' | ',' | ']' | '.' | '%' | ' ' | '\t' | '\n' | '\r']+) close_square_bracket() {
(id, None)
}
)
end:position!()
eol()
{
let (id, reftext) = result;
let substituted_id = state.intern_cow(substitute(id, HEADER, &state.document_attributes));
let substituted_reftext = reftext.map(|rt| state.intern_cow(substitute(rt, HEADER, &state.document_attributes)));
Anchor {
id: substituted_id,
xreflabel: substituted_reftext,
location: state.create_location(span_start, end),
bibliography_label: None,
bibliography: false,
}
}
rule inline_anchor(offset: usize) -> InlineNode<'input>
= double_open_square_bracket()
warn_anchor_id_with_whitespace()?
id:$([^'\'' | ',' | ']' | '[' | ' ' | '\t' | '\n' | '\r']+)
reftext:(
comma() reftext:$([^']']+) {
Some(reftext)
} /
{
None
}
)
double_close_square_bracket()
{
let substituted_id = state.intern_cow(substitute(id, HEADER, &state.document_attributes));
let substituted_reftext = reftext.map(|rt| state.intern_cow(substitute(rt, HEADER, &state.document_attributes)));
InlineNode::InlineAnchor(Anchor {
id: substituted_id,
xreflabel: substituted_reftext,
location: state.create_block_location(span_start, span_end, offset),
bibliography_label: None,
bibliography: false,
})
}
rule inline_anchor_match() -> ()
= double_open_square_bracket() [^'\'' | ',' | ']' | '[' | ' ' | '\t' | '\n' | '\r']+ (comma() [^']']+)? double_close_square_bracket()
rule invalid_bibliography_anchor(offset: usize) -> InlineNode<'input>
= syntax:$("[[[" [^']' | '\n']* "]]]") {?
let body = &syntax[3..syntax.len() - 3];
let id = body.split_once(',').map_or(body, |(id, _)| id);
if is_valid_bibliography_id(id) {
Err("valid bibliography anchor")
} else {
Ok(InlineNode::PlainText(Plain {
content: syntax,
location: state.create_block_location(span_start, span_end, offset),
escaped: false,
}))
}
}
rule attributes_line() -> (bool, BlockMetadata<'input>)
= !empty_list_separator() attributes:attributes() eol() {
let (discrete, metadata, _title_position) = attributes;
(discrete, metadata)
}
rule empty_list_separator()
= whitespace()* "[" whitespace()* "]" whitespace()* eol() eol()
pub(crate) rule attributes() -> (bool, BlockMetadata<'input>, Option<(usize, usize)>)
= !double_open_square_bracket()
open_square_bracket()
content_start:position!()
content:attribute_list_content()
{
parse_block_attribute_list(
state,
content,
content_start,
span_end,
BlockAttributeMode::Block,
)
}
pub(crate) rule macro_attributes() -> (bool, BlockMetadata<'input>, Option<(usize, usize)>)
= macro_attributes_for(MacroAttributeContext::General)
rule image_macro_attributes() -> (bool, BlockMetadata<'input>, Option<(usize, usize)>)
= macro_attributes_for(MacroAttributeContext::Image)
rule macro_attributes_for(context: MacroAttributeContext) -> (bool, BlockMetadata<'input>, Option<(usize, usize)>)
= open_square_bracket()
content_start:position!()
content:attribute_list_content()
{
parse_block_attribute_list(
state,
content,
content_start,
span_end,
BlockAttributeMode::Macro(context),
)
}
rule open_square_bracket() = "["
rule close_square_bracket() = "]"
rule attribute_list_content() -> &'input str
= content:$((!last_close_square_bracket() [^'\n' | '\r'])*) close_square_bracket() { content }
rule last_close_square_bracket()
= &("]" [^']' | '\n' | '\r']* (eol() / ![_]))
rule double_open_square_bracket() = "[["
rule double_close_square_bracket() = "]]"
rule comma() = ","
rule period() = "."
pub rule url() -> String =
proto:$("https" / "http" / "ftp" / "irc") "://" path:url_path() { format!("{proto}://{path}") }
/ "mailto:" email:email_address() { format!("mailto:{email}") }
rule email_address() -> String
= local:$(
"\"" [^'"']+ "\""
/ ['a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '_' | '%' | '+' | '-']+
)
"@"
domain:$(
['a'..='z' | 'A'..='Z' | '0'..='9']+
(['.' | '-'] ['a'..='z' | 'A'..='Z' | '0'..='9']+)*
)
{?
if !domain.contains('.') {
return Err("email domain must have TLD (contain a dot)");
}
Ok(format!("{local}@{domain}"))
}
rule url_path() -> String = path:$(url_path_char() (url_path_char() / internal_url_path_spaces())*)
{?
let inline_state = InlinePreprocessorParserState::new_all_enabled(
path,
state.line_map.clone(),
state.input,
state.arena,
);
let processed = inline_preprocessing::run(path, &state.document_attributes, &inline_state)
.map_err(|_| {
tracing::error!("could not preprocess url path");
"could not preprocess url path"
})?;
let result = restore_url_path(processed);
let warnings = inline_state.drain_warnings();
drop(inline_state);
for warning in warnings {
state.add_inline_preprocessor_warning(warning);
}
Ok(result)
}
rule url_path_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '.' | '_' | '~' | ':' | '/' | '?' | '#' | '@' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '%' | '\\' ]
rule internal_url_path_spaces() = [' ']+ &url_path_char()
rule bare_url() -> String =
proto:$("https" / "http" / "ftp" / "irc") "://" path:bare_url_path()
{ format!("{proto}://{path}") }
rule bare_url_path() -> String = path:$(
bare_url_safe_char()
( bare_url_safe_char()
/ bare_url_paren_group()
/ "("
/ bare_url_trailing_char() &bare_url_char()
)*
)
{?
let inline_state = InlinePreprocessorParserState::new_all_enabled(
path,
state.line_map.clone(),
state.input,
state.arena,
);
let processed = inline_preprocessing::run(path, &state.document_attributes, &inline_state)
.map_err(|_| {
tracing::error!("could not preprocess bare url path");
"could not preprocess bare url path"
})?;
let result = restore_url_path(processed);
let warnings = inline_state.drain_warnings();
drop(inline_state);
for warning in warnings {
state.add_inline_preprocessor_warning(warning);
}
Ok(result)
}
rule bare_url_paren_group()
= "(" (bare_url_safe_char() / bare_url_trailing_char() / bare_url_paren_group() / "(")* ")"
rule bare_url_safe_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '~'
| '/' | '#' | '@' | '$' | '&'
| '+' | '=' | '%' | '\\']
rule bare_url_trailing_char() = ['.' | ',' | ';' | '!' | '?' | ':' | '\'' | '*']
rule bare_url_char() = bare_url_safe_char() / bare_url_trailing_char() / "("
rule path_fragment() -> String
= "#" fragment:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-']+)
{
format!("#{fragment}")
}
pub rule path() -> String = path:$(path_char() (path_char() / internal_path_spaces())*)
{?
let inline_state = InlinePreprocessorParserState::new_all_enabled(
path,
state.line_map.clone(),
state.input,
state.arena,
);
let processed = inline_preprocessing::run(path, &state.document_attributes, &inline_state)
.map_err(|_| {
tracing::error!("could not preprocess path");
"could not preprocess path"
})?;
let result = processed.text.into_owned();
let warnings = inline_state.drain_warnings();
drop(inline_state);
for warning in warnings {
state.add_inline_preprocessor_warning(warning);
}
Ok(result)
}
rule path_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '{' | '}' | '_' | '-' | '.' | '/' | '\\' | '%' | '\u{80}'..='\u{10FFFF}' ]
rule internal_path_spaces() = [' ']+ &path_char()
pub rule source() -> Source<'input>
= source:
(
u:url() {?
let interned = state.intern_str(&u);
Source::from_str_borrowed(interned).map_err(|_| "failed to parse URL")
}
/ p:path() {?
let interned = state.intern_str(&p);
Source::from_str_borrowed(interned).map_err(|_| "failed to parse path")
}
)
{ source }
rule digits() = ['0'..='9']+
rule whitespace() = quiet!{ " " / "\t" }
rule eol() = quiet!{ "\n" }
rule comment_line() = quiet!{ comment() (eol() / ![_]) }
rule comment() = quiet!{ "//" !"/" [^'\n']* (&eol() / ![_]) }
rule document_attribute_value() -> Cow<'input, str>
= whitespace()+ value:(
lines:backslash_continuation_lines() { Cow::Owned(lines.join("\n")) }
/ single_line:$([^'\n']+) { Cow::Borrowed(single_line) }
) { value }
rule backslash_continuation_lines() -> Vec<&'input str>
= lines:(line:$((!(" \\" eol()) [^'\n'])+ " \\") eol() { line })+
last:$([^'\n']+)?
{
let mut result = lines;
if let Some(l) = last {
result.push(l);
}
result
}
rule document_attribute_match() -> AttributeDeclaration<'input>
= ":"
key_entry:(
"!" key:$([^':']+) { (false, key) }
/ key:$([^('!' | ':')]+) "!" { (false, key) }
/ key:$([^':']+) { (true, key) }
)
":" &" "?
value:document_attribute_value()?
{
let (set, key) = key_entry;
let attr_value = if !set {
RawAttributeValue::Unset
} else if let Some(v) = value {
RawAttributeValue::Text(v)
} else {
RawAttributeValue::Set
};
AttributeDeclaration { name: key, value: attr_value }
}
/ expected!("document attribute key starting with ':'")
rule position() -> PositionWithOffset = offset:position!() {
PositionWithOffset {
offset,
position: state.line_map.offset_to_position(offset, state.input)
}
}
}
}