use crate::attributes::{parts_text, push_value};
use crate::substitution_context::Evaluation;
use crate::substitution_text::Text;
use crate::{AttributePart as P, Diagnostic, Inline, Severity, Span};
#[derive(Clone)]
pub(crate) struct Reference {
pub span: Span,
pub name: String,
pub missing: bool,
pub drops_line: bool,
}
pub(crate) fn reference(src: &str, at: usize) -> Option<(usize, &str, bool)> {
let leading = src[at..].starts_with("\\{");
let open = at + usize::from(leading);
if src.as_bytes().get(open) != Some(&b'{') {
return None;
}
let mut end = open + 1;
for (i, ch) in src[end..].char_indices() {
if crate::unicode_word::is_word(ch) || (i > 0 && ch == '-') {
continue;
}
end += i;
let trailing = src.as_bytes().get(end) == Some(&b'\\');
let close = end + usize::from(trailing);
return (end > open + 1 && src.as_bytes().get(close) == Some(&b'}')).then_some((
close + 1,
&src[open + 1..end],
leading || trailing,
));
}
None
}
pub(crate) fn apply(
text: Text,
attrs: &mut Evaluation<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> (Text, Vec<Reference>) {
let policy = attrs.get("attribute-missing").unwrap_or("skip").to_owned();
apply_with_policy(text, attrs, &policy, false, diagnostics)
}
pub(crate) fn parts(
parts: &[P],
attrs: &mut Evaluation<'_>,
span: Span,
policy: &str,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<P> {
let mut text = Text::default();
for part in parts {
text.append_part(part, 0, span);
}
apply_with_policy(text, attrs, policy, false, diagnostics)
.0
.into_parts()
}
pub(crate) fn implicit_title(
value: &str,
attrs: &mut Evaluation<'_>,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<P> {
let mut text = Text::default();
text.append_part(&P::Text(value.into()), 0, span);
apply_with_policy(text, attrs, "skip", true, diagnostics)
.0
.into_parts()
}
pub(crate) fn header_value(
value: &str,
attrs: &mut Evaluation<'_>,
span: Span,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<P> {
let mut text = Text::default();
text.append_part(&P::Text(value.into()), 0, span);
let policy = attrs.get("attribute-missing").unwrap_or("skip").to_owned();
apply_with_policy(text, attrs, &policy, false, diagnostics)
.0
.into_parts()
}
pub(crate) fn preprocessor(
value: &str,
attrs: &mut Evaluation<'_>,
span: Span,
policy: Option<&str>,
diagnostics: &mut Vec<Diagnostic>,
) -> String {
let mut text = Text::default();
text.append_part(&P::Raw(value.into()), 0, span);
let (fixed, policy) = match policy {
Some(policy) => (true, policy.to_owned()),
None => (
false,
attrs.get("attribute-missing").unwrap_or("skip").to_owned(),
),
};
let parts = apply_with_policy(text, attrs, &policy, fixed, diagnostics)
.0
.into_parts();
crate::attributes::reference_text(&parts)
}
fn apply_with_policy(
text: Text,
attrs: &mut Evaluation<'_>,
policy: &str,
fixed_policy: bool,
diagnostics: &mut Vec<Diagnostic>,
) -> (Text, Vec<Reference>) {
if !text.text.contains('{') || !text.text.contains('}') {
return (text, Vec::new());
}
let mut output = Text::default();
let mut refs = Vec::new();
let mut dropped = Vec::new();
let mut dropped_lines = Vec::new();
let closes: Vec<_> = text.text.match_indices('}').map(|(i, _)| i).collect();
let mut at = 0;
let mut copied = 0;
let mut missing_policy = None;
let mut undefined_policy = None;
while at < text.text.len() {
let stateful = attrs
.stateful()
.then(|| crate::substitution_context::expression(&text.text, at, &closes))
.flatten();
let Some((end, name, escaped)) = stateful.or_else(|| reference(&text.text, at)) else {
at += text.text[at..].chars().next().unwrap().len_utf8();
continue;
};
text.copy(copied..at, &mut output);
if escaped {
let literal = format!("{{{name}}}");
text.replacement(at..end, &literal, Some(&literal), &mut output);
} else if stateful.is_some() {
let span = text.match_span(at..end);
let effect = attrs.execute(name, span, &mut undefined_policy, diagnostics);
if effect.remove_empty {
dropped.push(output.text.len());
}
if effect.remove_line {
dropped_lines.push(output.text.len());
}
let metadata = text.fragments[text.fragments.partition_point(|(r, _)| r.end <= at)]
.1
.part
.as_ref()
.is_some_and(P::is_boundary);
if metadata {
text.replacement(
at..end,
&crate::attributes::reference_text(&effect.parts),
None,
&mut output,
);
} else {
refs.push(Reference {
span,
name: name.into(),
missing: false,
drops_line: false,
});
for part in &effect.parts {
output.append_part(part, refs.len(), span);
}
}
} else {
let span = text.match_span(at..end);
let metadata = text.fragments[text.fragments.partition_point(|(r, _)| r.end <= at)]
.1
.part
.as_ref()
.is_some_and(P::is_boundary);
let parts = attrs.value_parts(name).or_else(|| {
crate::context::intrinsic(&name.to_lowercase())
.map(|value| vec![P::Raw(value.into())])
});
let missing = parts.is_none();
let policy = if missing && attrs.stateful() && !fixed_policy {
missing_policy
.get_or_insert_with(|| {
attrs.get("attribute-missing").unwrap_or("skip").to_owned()
})
.as_str()
} else {
policy
};
let removed = missing && matches!(policy, "drop" | "drop-line");
if !metadata {
refs.push(Reference {
span,
name: name.into(),
missing: missing && !removed,
drops_line: removed && policy == "drop-line",
});
}
if removed {
dropped.push(output.text.len());
if policy == "drop-line" {
dropped_lines.push(output.text.len());
}
}
if missing && matches!(policy, "warn" | "drop-line") {
diagnostics.push(Diagnostic {
omitted_span: None,
report: crate::DiagnosticReport {
location: crate::DiagnosticLocation::Unlocated,
..Default::default()
},
span,
severity: if policy == "warn" {
Severity::Warning
} else {
Severity::Info
},
message: format!(
"{} reference to missing attribute: {}",
if policy == "warn" {
"skipping"
} else {
"dropping line containing"
},
name.to_lowercase()
),
});
}
let parts = parts.unwrap_or_else(|| {
if removed {
Vec::new()
} else {
vec![P::Text(text.text[at..end].into())]
}
});
if metadata {
text.replacement(
at..end,
&crate::attributes::reference_text(&parts),
None,
&mut output,
);
} else {
for part in &parts {
output.append_part(part, refs.len(), span);
}
}
}
copied = end;
at = end;
}
if copied == 0 {
return (text, refs);
}
text.copy(copied..text.text.len(), &mut output);
if !dropped.is_empty() {
let mut retained = Text::default();
let mut positions = dropped.into_iter().peekable();
let mut line_positions = dropped_lines.into_iter().peekable();
let mut start = 0;
let mut previous_end = None;
for line in output.text.split('\n') {
let end = start + line.len();
let mut missing = false;
while positions.peek().is_some_and(|&pos| pos <= end) {
positions.next();
missing = true;
}
let mut drop_line = false;
while line_positions.peek().is_some_and(|&pos| pos <= end) {
line_positions.next();
drop_line = true;
}
if !drop_line && (!missing || !line.is_empty()) {
if let Some(end) = previous_end {
output.copy(end..end + 1, &mut retained);
}
output.copy(start..end, &mut retained);
previous_end = Some(end);
}
start = end + 1;
}
output = retained;
}
(output, refs)
}
pub(crate) fn nodes(text: Text, refs: Vec<Reference>) -> Vec<Inline> {
let mut parts = vec![Vec::new(); refs.len()];
let mut nodes = Vec::new();
for f in text.into_fragments() {
let (owner, span) = (f.owner, f.span);
let part = f.into_part();
if owner > 0 {
push_value(&mut parts[owner - 1], part);
} else {
nodes.push(match part {
P::Text(value) => Inline::Text { value, span },
P::Raw(value) => Inline::Raw { value, span },
part => Inline::Substituted {
value: String::new(),
parts: vec![part],
span,
},
});
}
}
for (r, mut parts) in refs.into_iter().zip(parts) {
let value = if r.missing && !parts.is_empty() {
None
} else {
Some(parts_text(&parts))
};
if value.is_none() || !parts.iter().any(P::is_special) {
parts.clear();
}
nodes.push(Inline::AttributeRef {
name: r.name,
value,
parts,
span: r.span,
});
}
nodes.sort_by_key(|n| n.span().start);
nodes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unmatched_attribute_stage_reuses_buffers_without_effects_or_diagnostics() {
use crate::substitution_text::tests::{assert_reused, marked_text};
for source in [
"Ж🙂 & text\r\n",
"{unfinished",
"stray}",
"{} {bad name}",
"{counter:n",
] {
for policy in ["skip", "warn", "drop", "drop-line"] {
for stateful in [false, true] {
let attrs = crate::Attributes::new();
let locked = std::collections::BTreeSet::new();
let mut counters = std::collections::BTreeMap::new();
let mut evaluation = if stateful {
Evaluation::definition(&attrs, &locked, &mut counters, false)
} else {
Evaluation::read_only(&attrs)
};
let mut diagnostics = Vec::new();
assert_reused(marked_text(source), |text| {
let (result, refs) = apply_with_policy(
text,
&mut evaluation,
policy,
false,
&mut diagnostics,
);
assert!(refs.is_empty());
result
});
assert!(evaluation.changed_attributes().is_none());
assert!(counters.is_empty());
assert!(diagnostics.is_empty());
}
}
}
}
#[test]
fn matched_attributes_keep_reports_and_effects_even_without_visible_changes() {
let attrs = crate::Attributes::new();
let locked = std::collections::BTreeSet::new();
let mut counters = std::collections::BTreeMap::new();
let mut evaluation = Evaluation::definition(&attrs, &locked, &mut counters, false);
let span = Span::in_source(crate::SourceId(7), 10, 22);
let mut diagnostics = Vec::new();
let mut text = Text::default();
text.append_part(&P::Text("{counter2:".into()), 0, span);
text.append_part(&P::Text("n}".into()), 1, span);
let (result, refs) =
apply_with_policy(text, &mut evaluation, "skip", false, &mut diagnostics);
assert!(result.text.is_empty());
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].span, span);
assert_eq!(evaluation.get("n"), Some("1"));
let text = Text::from_parts_at(&[P::Text("{missing}".into())], span);
let (result, refs) =
apply_with_policy(text, &mut evaluation, "warn", true, &mut diagnostics);
assert_eq!(result.text, "{missing}");
assert_eq!(refs.len(), 1);
assert!(refs[0].missing);
assert_eq!(refs[0].span, span);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span, span);
let text = Text::from_parts_at(&[P::Text("\\{n}".into())], span);
let (result, refs) =
apply_with_policy(text, &mut evaluation, "skip", false, &mut diagnostics);
assert_eq!(result.text, "{n}");
assert!(refs.is_empty());
assert_eq!(evaluation.get("n"), Some("1"));
}
use crate::Attributes;
#[test]
fn shared_stage_matches_across_parts_and_drops_the_transformed_line() {
let mut attrs = Attributes::new();
attrs.set_parts("name", vec![P::Raw("first\nsecond".into())]);
let span = Span::new(12, 42);
let mut diagnostics = Vec::new();
let output = parts(
&[
P::Text("before {na".into()),
P::Raw("me} {missing}\nafter".into()),
],
&mut Evaluation::read_only(&attrs),
span,
"drop-line",
&mut diagnostics,
);
assert_eq!(
output,
vec![P::Text("before ".into()), P::Raw("first\nafter".into())]
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span, span);
assert_eq!(diagnostics[0].severity, Severity::Info);
}
}