use bumpalo::Bump;
use crate::{
InlineNode, Location, ParseInlineResult, Pass, PassthroughKind, Plain, ProcessedContent, Raw,
Substitution, parsed::OwnedInput,
};
use super::{
ParserState,
inlines::inline_parser,
location_mapping::{clamp_inline_node_locations, remap_inline_node_location},
};
pub(crate) fn process_passthrough_with_quotes<'a>(
arena: &'a Bump,
content: &'a str,
passthrough: &Pass,
) -> Vec<InlineNode<'a>> {
let has_quotes = passthrough.substitutions.contains(&Substitution::Quotes);
if !has_quotes {
let suffix_len = match passthrough.kind {
PassthroughKind::Macro | PassthroughKind::Single => Some(1), PassthroughKind::Double => Some(2), PassthroughKind::Triple => Some(3), PassthroughKind::AttributeRef => None,
};
let content_location = if let Some(suffix_len) = suffix_len {
let total_span =
passthrough.location.absolute_end - passthrough.location.absolute_start;
let prefix_len = total_span - content.len() - suffix_len;
let content_abs_start = passthrough.location.absolute_start + prefix_len;
let content_col_start = passthrough.location.start.column + prefix_len;
Location {
absolute_start: content_abs_start,
absolute_end: content_abs_start + content.len(),
start: crate::Position {
line: passthrough.location.start.line,
column: content_col_start,
},
end: crate::Position {
line: passthrough.location.start.line,
column: content_col_start + content.len(),
},
}
} else {
passthrough.location.clone()
};
return vec![InlineNode::RawText(Raw {
content,
location: content_location,
subs: passthrough
.substitutions
.iter()
.filter(|s| **s != Substitution::Quotes)
.cloned()
.collect(),
})];
}
tracing::debug!(content = ?content, "Parsing passthrough content with quotes");
parse_text_for_quotes_in(arena, content)
}
pub fn parse_text_for_quotes(content: &str) -> ParseInlineResult {
let owner = OwnedInput::new(content.into());
ParseInlineResult::from_infallible(owner, |owner| {
parse_text_for_quotes_in(&owner.arena, &owner.source)
})
}
pub(crate) fn parse_text_for_quotes_in<'a>(
arena: &'a Bump,
content: &'a str,
) -> Vec<InlineNode<'a>> {
if content.is_empty() {
return Vec::new();
}
if !content
.bytes()
.any(|b| matches!(b, b'*' | b'_' | b'`' | b'#' | b'^' | b'~' | b'"' | b'\''))
{
return vec![InlineNode::PlainText(Plain {
content,
location: Location::default(),
escaped: false,
})];
}
let mut state = ParserState::new_quotes_only(content, arena);
match inline_parser::quotes_only_inlines(content, &mut state) {
Ok(nodes) => nodes,
Err(err) => {
tracing::warn!(
?err,
?content,
"quotes-only PEG parse failed, falling back to plain text"
);
vec![InlineNode::PlainText(Plain {
content,
location: Location::default(),
escaped: false,
})]
}
}
}
fn plain_text_at<'a>(text: &'a str, base_location: &Location, offset: usize) -> InlineNode<'a> {
let abs_start = base_location.absolute_start + offset;
let col_start = base_location.start.column + offset;
InlineNode::PlainText(Plain {
content: text,
location: Location {
absolute_start: abs_start,
absolute_end: abs_start + text.len(),
start: crate::Position {
line: base_location.start.line,
column: col_start,
},
end: crate::Position {
line: base_location.start.line,
column: col_start + text.len(),
},
},
escaped: false,
})
}
pub(crate) fn process_passthrough_placeholders<'a>(
content: &'a str,
processed: &'a ProcessedContent<'a>,
state: &ParserState<'a>,
base_location: &Location,
) -> Vec<InlineNode<'a>> {
let mut result = Vec::with_capacity(processed.passthroughs.len() * 2 + 1);
let mut remaining = content;
let mut processed_offset = 0;
for (index, passthrough) in processed.passthroughs.iter().enumerate() {
let placeholder = format!("���{index}���");
if let Some(placeholder_pos) = remaining.find(&placeholder) {
let before_content = if placeholder_pos > 0 {
Some(&remaining[..placeholder_pos])
} else {
None
};
if let Some(before) = before_content
&& !before.is_empty()
{
result.push(plain_text_at(before, base_location, processed_offset));
processed_offset += before.len();
}
if let Some(passthrough_content) = &passthrough.text {
let processed_nodes =
process_passthrough_with_quotes(state.arena, passthrough_content, passthrough);
let macro_prefix_len = "pass:q[".len(); let has_quotes = passthrough.substitutions.contains(&Substitution::Quotes);
let remaining_subs: Vec<Substitution> = passthrough
.substitutions
.iter()
.filter(|s| **s != Substitution::Quotes)
.cloned()
.collect();
for mut node in processed_nodes {
remap_inline_node_location(
&mut node,
passthrough.location.absolute_start + macro_prefix_len,
);
if has_quotes {
if let InlineNode::PlainText(p) = node {
node = InlineNode::RawText(Raw {
content: p.content,
location: p.location,
subs: remaining_subs.clone(),
});
}
}
result.push(node);
}
}
let skip_len = placeholder_pos + placeholder.len();
remaining = &remaining[skip_len..];
processed_offset +=
passthrough.location.absolute_end - passthrough.location.absolute_start;
}
}
if !remaining.is_empty() {
if let Some(InlineNode::PlainText(last_plain)) = result.last_mut() {
last_plain.content =
state.intern_fmt(format_args!("{}{remaining}", last_plain.content));
last_plain.location.absolute_end = base_location.absolute_end;
last_plain.location.end = base_location.end.clone();
} else {
let mut node = plain_text_at(remaining, base_location, processed_offset);
if let InlineNode::PlainText(ref mut p) = node {
p.location.absolute_end = base_location.absolute_end;
p.location.end = base_location.end.clone();
}
result.push(node);
}
}
if result.is_empty() {
result.push(InlineNode::PlainText(Plain {
content,
location: base_location.clone(),
escaped: false,
}));
}
for node in &mut result {
clamp_inline_node_locations(node, state.input);
}
merge_adjacent_plain_text_nodes(state, result)
}
pub(crate) fn merge_adjacent_plain_text_nodes<'a>(
state: &ParserState<'a>,
nodes: Vec<InlineNode<'a>>,
) -> Vec<InlineNode<'a>> {
let mut result: Vec<InlineNode<'a>> = Vec::with_capacity(nodes.len());
for node in nodes {
match (result.last_mut(), node) {
(Some(InlineNode::PlainText(last_plain)), InlineNode::PlainText(current_plain)) => {
last_plain.content = state.intern_fmt(format_args!(
"{}{}",
last_plain.content, current_plain.content
));
last_plain.location.absolute_end = current_plain.location.absolute_end;
last_plain.location.end = current_plain.location.end;
}
(_, node) => {
result.push(node);
}
}
}
result
}
pub(crate) fn replace_passthrough_placeholders(
content: &str,
processed: &ProcessedContent,
) -> String {
let mut result: String = content.into();
for (index, passthrough) in processed.passthroughs.iter().enumerate() {
let placeholder = format!("���{index}���");
if let Some(text) = &passthrough.text {
result = result.replace(&placeholder, text);
}
}
result
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)] mod tests {
use super::*;
#[test]
fn test_constrained_bold_pattern() {
let parsed = parse_text_for_quotes("This is *bold* text.");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(matches!(nodes[0], InlineNode::PlainText(_)));
assert!(
matches!(&nodes[1], InlineNode::BoldText(b) if matches!(b.content.first(), Some(InlineNode::PlainText(p)) if p.content == "bold"))
);
assert!(matches!(nodes[2], InlineNode::PlainText(_)));
}
#[test]
fn test_unconstrained_bold_pattern() {
let parsed = parse_text_for_quotes("This**bold**word");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(
matches!(&nodes[1], InlineNode::BoldText(b) if matches!(b.content.first(), Some(InlineNode::PlainText(p)) if p.content == "bold"))
);
}
#[test]
fn test_constrained_italic_pattern() {
let parsed = parse_text_for_quotes("This is _italic_ text.");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(
matches!(&nodes[1], InlineNode::ItalicText(i) if matches!(i.content.first(), Some(InlineNode::PlainText(p)) if p.content == "italic"))
);
}
#[test]
fn test_unconstrained_italic_pattern() {
let parsed = parse_text_for_quotes("This__italic__word");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(
matches!(&nodes[1], InlineNode::ItalicText(i) if matches!(i.content.first(), Some(InlineNode::PlainText(p)) if p.content == "italic"))
);
}
#[test]
fn test_constrained_monospace_pattern() {
let parsed = parse_text_for_quotes("Use `code` here.");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(
matches!(&nodes[1], InlineNode::MonospaceText(m) if matches!(m.content.first(), Some(InlineNode::PlainText(p)) if p.content == "code"))
);
}
#[test]
fn test_superscript_pattern() {
let parsed = parse_text_for_quotes("E=mc^2^");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 2);
assert!(
matches!(&nodes[1], InlineNode::SuperscriptText(s) if matches!(s.content.first(), Some(InlineNode::PlainText(p)) if p.content == "2"))
);
}
#[test]
fn test_subscript_pattern() {
let parsed = parse_text_for_quotes("H~2~O");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(
matches!(&nodes[1], InlineNode::SubscriptText(s) if matches!(s.content.first(), Some(InlineNode::PlainText(p)) if p.content == "2"))
);
}
#[test]
fn test_highlight_pattern() {
let parsed = parse_text_for_quotes("This is #highlighted# text.");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 3);
assert!(
matches!(&nodes[1], InlineNode::HighlightText(h) if matches!(h.content.first(), Some(InlineNode::PlainText(p)) if p.content == "highlighted"))
);
}
#[test]
fn test_escaped_superscript_not_parsed() {
let parsed = parse_text_for_quotes(r"E=mc\^2^");
let nodes = parsed.inlines();
assert!(
nodes.iter().all(|n| matches!(n, InlineNode::PlainText(_))),
"Escaped superscript should not be parsed"
);
}
#[test]
fn test_escaped_subscript_not_parsed() {
let parsed = parse_text_for_quotes(r"H\~2~O");
let nodes = parsed.inlines();
assert!(
nodes.iter().all(|n| matches!(n, InlineNode::PlainText(_))),
"Escaped subscript should not be parsed"
);
}
#[test]
fn test_multiple_formats_in_sequence() {
let parsed = parse_text_for_quotes("*bold* and _italic_ and `code`");
let nodes = parsed.inlines();
assert!(nodes.iter().any(|n| matches!(n, InlineNode::BoldText(_))));
assert!(nodes.iter().any(|n| matches!(n, InlineNode::ItalicText(_))));
assert!(
nodes
.iter()
.any(|n| matches!(n, InlineNode::MonospaceText(_)))
);
}
#[test]
fn test_plain_text_only() {
let parsed = parse_text_for_quotes("Just plain text here.");
let nodes = parsed.inlines();
assert_eq!(nodes.len(), 1);
assert!(matches!(nodes[0], InlineNode::PlainText(_)));
}
#[test]
fn test_empty_input() {
let parsed = parse_text_for_quotes("");
let nodes = parsed.inlines();
assert!(nodes.is_empty());
}
}