use std::{borrow::Cow, ops::Range, rc::Rc};
use crate::{
Error, InlineNode, InlinePreprocessorParserState, Location, Position, ProcessedContent,
SourceLocation,
grammar::{
inline_preprocessor::{ProcessedKind, SourceMap},
utf8_utils,
},
inline_preprocessing,
model::{BibliographyLabel, SourceRange, Substitution, substitution::SubstitutionPlan},
};
use super::{
ParserState,
helpers::BlockParsingMetadata,
inlines::inline_parser,
state::{InlineContext, InlineRules, ParserScope},
};
pub(crate) fn adjust_peg_error_position(
err: &peg::error::ParseError<peg::str::LineCol>,
parsed_text: &str,
doc_start_offset: usize,
state: &ParserState,
) -> Error {
let mut byte_offset = 0;
for (line_idx, line) in parsed_text.lines().enumerate() {
if line_idx + 1 == err.location.line {
byte_offset += err.location.column - 1; break;
}
byte_offset += line.len() + 1; }
let absolute_offset = doc_start_offset + byte_offset;
let (file, position) =
if let Some(range) = SourceRange::find_containing(&state.source_ranges, absolute_offset) {
let doc_position = state
.line_map
.offset_to_position(absolute_offset, state.input);
(
range.file.clone(),
Position::new(
state
.line_map
.source_line(range, state.input, absolute_offset),
doc_position.column,
),
)
} else {
let doc_position = state
.line_map
.offset_to_position(absolute_offset, state.input);
(state.current_file.as_deref().cloned(), doc_position)
};
Error::PegParse(
Box::new(SourceLocation {
file,
location: crate::Location::point(position),
}),
err.to_string()
.split_once(": ")
.map_or(err.to_string(), |(_, msg)| msg.to_string()),
)
}
pub(crate) fn adjust_and_log_parse_error(
err: &peg::error::ParseError<peg::str::LineCol>,
parsed_text: &str,
doc_start_offset: usize,
state: &ParserState,
context: &str,
) {
let adjusted_error = adjust_peg_error_position(err, parsed_text, doc_start_offset, state);
state.add_warning(crate::Warning::new(
crate::WarningKind::ContentRecovery {
message: format!("{context}: {adjusted_error}").into(),
},
adjusted_error.source_location().cloned(),
));
tracing::error!(
doc_start_offset,
"inline parse error; see returned diagnostics"
);
}
#[tracing::instrument(skip_all, fields(content_start = content_start, end = end, offset = offset))]
pub(crate) fn preprocess_inline_content<'a>(
state: &mut ParserState<'a>,
content_start: usize,
end: usize,
offset: usize,
content: &'a str,
substitutions: SubstitutionPlan,
) -> Result<(Location, ProcessedContent<'a>), Error> {
let mut adjusted_end = end + offset;
if adjusted_end > 0 && adjusted_end <= state.input.len() {
while adjusted_end < state.input.len() && !state.input.is_char_boundary(adjusted_end) {
adjusted_end += 1;
}
}
let content_end_offset = if adjusted_end == 0 {
0
} else {
utf8_utils::safe_decrement_offset(state.input, adjusted_end)
};
let location = state.create_location(content_start + offset, content_end_offset);
let needs_preprocessing = content.as_bytes().iter().any(|&b| b == b'{' || b == b'+')
|| (substitutions.enabled(&Substitution::Macros) && content.contains("pass:"));
if !needs_preprocessing {
return Ok((
location,
ProcessedContent {
text: Cow::Borrowed(content),
passthroughs: Vec::new(),
source_map: SourceMap::default(),
attribute_substitutions: crate::model::substitution::SubstitutionPlan::default(),
},
));
}
let mut inline_state = InlinePreprocessorParserState::new(
content,
state.line_map.clone(),
state.input,
state.arena,
substitutions.enabled(&Substitution::Macros),
substitutions.enabled(&Substitution::Attributes),
);
#[cfg(feature = "pre-spec-subs")]
{
inline_state.defer_monospace = !substitutions.enabled(&Substitution::Attributes)
|| substitutions.precedes(&Substitution::Quotes, &Substitution::Attributes);
}
inline_state.set_initial_position(&location, content_start + offset);
inline_state
.attribute_value_ranges
.clone_from(&state.attribute_value_ranges);
tracing::debug!(
input_len = content.len(),
offset,
content_start,
?end,
"before inline preprocessing run"
);
let processed = inline_preprocessing::run(content, &state.document_attributes, &inline_state)?;
for warning in inline_state.drain_warnings() {
state.add_inline_preprocessor_warning(warning);
}
Ok((location, processed))
}
fn processed_text_as_outer<'a>(
processed: &ProcessedContent<'a>,
state: &ParserState<'a>,
) -> &'a str {
match &processed.text {
Cow::Borrowed(s) => s,
Cow::Owned(s) => state.intern_str(s),
}
}
fn attribute_value_ranges(
processed: &ProcessedContent<'_>,
parent: &ParserState<'_>,
location: &Location,
) -> Vec<Range<usize>> {
let mut ranges = processed
.source_map
.attribute_value_ranges(location.absolute_start);
ranges.extend(parent.attribute_value_ranges.iter().filter_map(|range| {
let start = range.start.max(location.absolute_start);
let end = range.end.min(location.absolute_end.saturating_add(1));
(start < end).then(|| {
start.saturating_sub(location.absolute_start)
..end.saturating_sub(location.absolute_start)
})
}));
ranges.sort_unstable_by_key(|range| range.start);
ranges
}
fn inline_context(
state: &ParserState<'_>,
block_metadata: &BlockParsingMetadata<'_>,
autolinks: bool,
) -> InlineContext {
let mut rules = state.inline_ctx.rules;
rules.set(InlineRules::AUTOLINKS, autolinks);
rules.set(
InlineRules::PRESERVE_BLANK_LINES,
block_metadata.metadata.style == Some("verse")
|| rules.contains(InlineRules::PRESERVE_BLANK_LINES),
);
rules.set(
InlineRules::HARD_BREAKS,
block_metadata.hardbreaks || rules.contains(InlineRules::HARD_BREAKS),
);
rules.set(
InlineRules::EOI_HARD_BREAK,
state.scope == ParserScope::Document,
);
InlineContext {
offset: 0,
substitutions: block_metadata.substitutions,
rules,
}
}
#[tracing::instrument(skip_all, fields(input_len = text.len(), autolinks = autolinks, verbatim = verbatim))]
fn parse_processed_inlines<'a>(
processed: &ProcessedContent<'a>,
text: &'a str,
state: &mut ParserState<'a>,
block_metadata: &BlockParsingMetadata,
location: &Location,
autolinks: bool,
verbatim: bool,
) -> Result<Vec<InlineNode<'a>>, Error> {
let inline_ctx = inline_context(state, block_metadata, autolinks);
let mut inline_peg_state = ParserState::for_inline_parsing(text, state, inline_ctx);
if !autolinks {
inline_peg_state.outer_constrained_delimiter = None;
}
if !processed.passthroughs.is_empty() {
inline_peg_state.attribute_passthroughs = if processed
.passthroughs
.iter()
.any(|pass| !pass.attribute_fragments.is_empty())
{
processed.passthroughs.clone().into()
} else {
Rc::default()
};
}
inline_peg_state.empty_attribute_offsets = processed
.source_map
.empty_attribute_offsets(location.absolute_start);
inline_peg_state.empty_attribute_offsets.extend(
state
.empty_attribute_offsets
.iter()
.filter(|offset| {
location.absolute_start <= **offset && **offset <= location.absolute_end + 1
})
.map(|offset| offset - location.absolute_start),
);
inline_peg_state.empty_attribute_offsets.sort_unstable();
inline_peg_state.empty_attribute_offsets.dedup();
inline_peg_state.attribute_value_ranges = attribute_value_ranges(processed, state, location);
if inline_ctx
.substitutions
.precedes(&Substitution::Macros, &Substitution::Attributes)
|| inline_ctx
.substitutions
.precedes(&Substitution::Quotes, &Substitution::Attributes)
{
inline_peg_state.late_attribute_sources =
late_attribute_sources(processed, state, location);
}
let inlines = if verbatim {
inline_parser::verbatim_inlines(text, &mut inline_peg_state)
} else if !autolinks {
inline_parser::inlines_no_autolinks(text, &mut inline_peg_state)
} else if inline_peg_state.quotes_only {
inline_parser::quotes_only_inlines(text, &mut inline_peg_state)
} else {
inline_parser::inlines(text, &mut inline_peg_state)
};
let mut inlines = match inlines {
Ok(inlines) => inlines,
Err(err) => {
return Err(adjust_peg_error_position(
&err,
text,
location.absolute_start,
state,
));
}
};
for inline in &mut inlines {
if let InlineNode::InlineAnchor(anchor) = inline {
anchor.bibliography_label =
parse_bibliography_label(&inline_peg_state, &anchor.location)?;
}
}
Ok(inlines)
}
fn late_attribute_sources<'a>(
processed: &ProcessedContent<'a>,
state: &ParserState<'a>,
location: &Location,
) -> Vec<(Range<usize>, &'a str)> {
let mut sources = Vec::new();
let mut original = location.absolute_start;
let mut expanded = 0;
for replacement in &processed.source_map.replacements {
if replacement.absolute_start < original {
continue;
}
expanded += replacement.absolute_start - original;
let attribute_passthrough = matches!(
replacement.kind,
ProcessedKind::Passthrough | ProcessedKind::Escape
) && processed.passthroughs.iter().any(|pass| {
pass.kind == crate::PassthroughKind::AttributeRef
&& pass.location.absolute_start == replacement.absolute_start
&& pass.location.absolute_end == replacement.absolute_end
});
if replacement.kind.is_attribute() || attribute_passthrough {
sources.push((
expanded..expanded + replacement.byte_len,
&state.input[replacement.absolute_start..replacement.absolute_end],
));
}
expanded += replacement.byte_len;
original = replacement.absolute_end;
}
sources.extend(
state
.late_attribute_sources
.iter()
.filter(|(range, _)| {
range.start >= location.absolute_start && range.end <= location.absolute_end + 1
})
.map(|(range, source)| {
(
range.start - location.absolute_start..range.end - location.absolute_start,
*source,
)
}),
);
sources.sort_unstable_by_key(|(range, _)| range.start);
sources
}
fn parse_bibliography_label<'a>(
state: &ParserState<'a>,
location: &Location,
) -> Result<Option<Box<BibliographyLabel<'a>>>, Error> {
let Some(start) = location.absolute_start.checked_sub(1) else {
return Ok(None);
};
let Some(anchor) = state.input.get(start..location.absolute_end + 2) else {
return Ok(None);
};
let Some(body) = anchor
.strip_prefix("[[[")
.and_then(|s| s.strip_suffix("]]]"))
else {
return Ok(None);
};
let Some((_, label)) = body.split_once(',') else {
return Ok(None);
};
let label = label.trim_start();
let offset = start + anchor.len() - 3 - label.len();
let mut inline_ctx = state.inline_ctx;
inline_ctx.offset = 0;
inline_ctx.rules.remove(InlineRules::EOI_HARD_BREAK);
let mut child = ParserState::for_inline_parsing(label, state, inline_ctx);
child.attribute_value_ranges = state
.attribute_value_ranges
.iter()
.filter_map(|range| {
let start = range.start.max(offset);
let end = range.end.min(offset + label.len());
(start < end).then_some(start.saturating_sub(offset)..end.saturating_sub(offset))
})
.collect();
let mut content = inline_parser::inlines(label, &mut child)
.map_err(|error| adjust_peg_error_position(&error, label, offset, state))?;
for inline in &mut content {
super::location_walk::walk_inline_locations_mut(inline, &mut |location| {
location.absolute_start += offset;
location.absolute_end += offset;
});
}
Ok(Some(Box::new(BibliographyLabel {
source: Some(label),
content,
})))
}
#[tracing::instrument(skip_all, fields(content_start = content_start, end = end, offset = offset))]
pub(crate) fn process_inlines<'a>(
state: &mut ParserState<'a>,
block_metadata: &BlockParsingMetadata,
content_start: usize,
end: usize,
offset: usize,
content: &'a str,
) -> Result<(Vec<InlineNode<'a>>, &'a str), Error> {
process_inline_content(
state,
block_metadata,
content_start,
end,
offset,
content,
false,
)
}
#[cfg(feature = "pre-spec-subs")]
pub(crate) fn process_verbatim_inlines<'a>(
state: &mut ParserState<'a>,
block_metadata: &BlockParsingMetadata,
content_start: usize,
content: &'a str,
) -> Result<Vec<InlineNode<'a>>, Error> {
let (nodes, _) = process_inline_content(
state,
block_metadata,
content_start,
content_start + content.len(),
0,
content,
true,
)?;
Ok(nodes
.into_iter()
.map(|node| {
let InlineNode::PlainText(text) = node else {
return node;
};
if text.escaped {
InlineNode::RawText(crate::Raw {
content: text.content,
location: text.location,
subs: vec![Substitution::SpecialChars],
})
} else {
InlineNode::VerbatimText(crate::Verbatim {
content: text.content,
location: text.location,
})
}
})
.collect())
}
fn process_inline_content<'a>(
state: &mut ParserState<'a>,
block_metadata: &BlockParsingMetadata,
content_start: usize,
end: usize,
offset: usize,
content: &'a str,
verbatim: bool,
) -> Result<(Vec<InlineNode<'a>>, &'a str), Error> {
let (location, mut processed) = preprocess_inline_content(
state,
content_start,
end,
offset,
content,
block_metadata.substitutions,
)?;
let source = processed_text_as_outer(&processed, state);
if processed.text.is_empty()
|| (!verbatim && state.scope == ParserScope::Document && processed.text.trim().is_empty())
{
return Ok((Vec::new(), source));
}
let content = parse_processed_inlines(
&processed,
source,
state,
block_metadata,
&location,
true,
verbatim,
)?;
processed.attribute_substitutions = block_metadata.substitutions;
let inlines =
super::location_mapping::map_inline_locations(state, &processed, content, &location)?;
let source = if processed.passthroughs.is_empty() {
source
} else {
let restored =
super::passthrough_processing::replace_passthrough_placeholders(source, &processed);
state.intern_str(&restored)
};
Ok((inlines, source))
}
#[tracing::instrument(skip_all, fields(content_start = content_start, end = end, offset = offset))]
pub(crate) fn process_inlines_no_autolinks<'a>(
state: &mut ParserState<'a>,
block_metadata: &BlockParsingMetadata,
content_start: usize,
end: usize,
offset: usize,
content: &'a str,
) -> Result<Vec<InlineNode<'a>>, Error> {
let (location, mut processed) = preprocess_inline_content(
state,
content_start,
end,
offset,
content,
block_metadata.substitutions,
)?;
if processed.text.is_empty() {
return Ok(Vec::new());
}
let source = processed_text_as_outer(&processed, state);
let content = parse_processed_inlines(
&processed,
source,
state,
block_metadata,
&location,
false,
false,
)?;
processed.attribute_substitutions = block_metadata.substitutions;
super::location_mapping::map_inline_locations(state, &processed, content, &location)
}