use brink_format::{LinePart, SlotInfo, SourceLocation};
use crate::hir;
use crate::hir::display_expr;
use super::content::lower_content_parts_pub;
use super::context::LowerCtx;
use super::expr::lower_expr;
use super::lir;
pub fn compose_hir_content(a: &hir::Content, b: &hir::Content) -> hir::Content {
let mut parts = a.parts.clone();
if let (Some(hir::ContentPart::Text(last)), Some(hir::ContentPart::Text(first))) =
(parts.last(), b.parts.first())
{
let merged =
if last.ends_with(char::is_whitespace) && first.starts_with(char::is_whitespace) {
format!("{last}{}", first.trim_start())
} else {
format!("{last}{first}")
};
let len = parts.len();
parts[len - 1] = hir::ContentPart::Text(merged);
parts.extend(b.parts.iter().skip(1).cloned());
} else {
parts.extend(b.parts.iter().cloned());
}
let mut tags = a.tags.clone();
tags.extend(b.tags.iter().cloned());
hir::Content {
ptr: a.ptr,
parts,
tags,
}
}
pub fn compose_hir_content_opt(
a: Option<&hir::Content>,
b: Option<&hir::Content>,
) -> Option<hir::Content> {
match (a, b) {
(None, None) => None,
(Some(c), None) | (None, Some(c)) => Some(c.clone()),
(Some(a_content), Some(b_content)) => Some(compose_hir_content(a_content, b_content)),
}
}
pub fn starts_with_whitespace_only_text(content: &hir::Content) -> bool {
matches!(content.parts.first(), Some(hir::ContentPart::Text(s)) if !s.is_empty() && s.trim().is_empty())
}
pub fn try_recognize(
content: &hir::Content,
ctx: &mut LowerCtx<'_>,
) -> Option<lir::ContentEmission> {
if content.parts.len() == 1
&& let hir::ContentPart::Text(s) = &content.parts[0]
{
let source_hash = brink_format::content_hash(s);
let source_location = build_source_location(content, ctx);
let tags = content
.tags
.iter()
.map(|t| lower_content_parts_pub(&t.parts, ctx))
.collect();
return Some(lir::ContentEmission {
line: lir::RecognizedLine::Plain(s.clone()),
metadata: lir::LineMetadata {
source_hash,
slot_info: Vec::new(),
source_location,
},
tags,
});
}
if try_recognize_template(content, ctx) {
let mut template_parts = Vec::new();
let mut slot_exprs = Vec::new();
let mut slot_info = Vec::new();
let mut hash_source = String::new();
let mut slot_idx: u8 = 0;
build_recognized_parts(
&content.parts,
ctx,
&mut template_parts,
&mut hash_source,
&mut slot_exprs,
&mut slot_info,
&mut slot_idx,
);
let source_hash = brink_format::content_hash(&hash_source);
let source_location = build_source_location(content, ctx);
let tags = content
.tags
.iter()
.map(|t| lower_content_parts_pub(&t.parts, ctx))
.collect();
return Some(lir::ContentEmission {
line: lir::RecognizedLine::Template {
parts: template_parts,
slot_exprs,
},
metadata: lir::LineMetadata {
source_hash,
slot_info,
source_location,
},
tags,
});
}
None
}
pub fn strip_boundary_glue(content: &hir::Content) -> (bool, hir::Content, bool) {
let parts = &content.parts;
let mut start = 0;
let mut has_leading = false;
while start < parts.len() && parts[start] == hir::ContentPart::Glue {
has_leading = true;
start += 1;
}
let mut end = parts.len();
let mut has_trailing = false;
while end > start && parts[end - 1] == hir::ContentPart::Glue {
has_trailing = true;
end -= 1;
}
let interior = &parts[start..end];
let mut merged_parts: Vec<hir::ContentPart> = Vec::with_capacity(interior.len());
for part in interior {
match part {
hir::ContentPart::Glue => {
if matches!(merged_parts.last(), Some(hir::ContentPart::Text(_))) {
merged_parts.push(hir::ContentPart::Glue);
} else {
merged_parts.push(hir::ContentPart::Glue);
}
}
hir::ContentPart::Text(s) => {
if matches!(merged_parts.last(), Some(hir::ContentPart::Glue)) {
merged_parts.pop(); if let Some(hir::ContentPart::Text(prev)) = merged_parts.last_mut() {
prev.push_str(s);
} else {
merged_parts.push(hir::ContentPart::Text(s.clone()));
}
} else {
merged_parts.push(part.clone());
}
}
_ => {
merged_parts.push(part.clone());
}
}
}
let stripped = hir::Content {
ptr: content.ptr,
parts: merged_parts,
tags: content.tags.clone(),
};
(has_leading, stripped, has_trailing)
}
pub fn try_recognize_with_glue(
content: &hir::Content,
ctx: &mut LowerCtx<'_>,
) -> Option<(bool, lir::ContentEmission, bool)> {
let (has_leading, stripped, has_trailing) = strip_boundary_glue(content);
if !has_leading && !has_trailing && stripped.parts.len() == content.parts.len() {
return None;
}
if stripped.parts.is_empty() {
return None;
}
let emission = try_recognize(&stripped, ctx)?;
Some((has_leading, emission, has_trailing))
}
fn build_source_location(content: &hir::Content, ctx: &LowerCtx<'_>) -> Option<SourceLocation> {
let ptr = content.ptr.as_ref()?;
let range = ptr.text_range();
let file = ctx.file_paths.get(&ctx.file)?;
Some(SourceLocation {
file: file.clone(),
range_start: range.start().into(),
range_end: range.end().into(),
})
}
fn try_recognize_template(content: &hir::Content, _ctx: &LowerCtx<'_>) -> bool {
is_template_admissible(&content.parts)
&& content_has_span_or_interpolation(&content.parts)
&& (content_has_nonempty_text(&content.parts) || content_has_span(&content.parts))
}
fn is_template_admissible(parts: &[hir::ContentPart]) -> bool {
parts.iter().all(|p| match p {
hir::ContentPart::Text(_) | hir::ContentPart::Interpolation(_) => true,
hir::ContentPart::Span(span) => is_template_admissible(&span.children),
hir::ContentPart::Glue
| hir::ContentPart::Spring
| hir::ContentPart::InlineConditional(_)
| hir::ContentPart::InlineSequence(_) => false,
})
}
fn content_has_span_or_interpolation(parts: &[hir::ContentPart]) -> bool {
parts.iter().any(|p| {
matches!(
p,
hir::ContentPart::Interpolation(_) | hir::ContentPart::Span(_)
)
})
}
fn content_has_span(parts: &[hir::ContentPart]) -> bool {
parts.iter().any(|p| matches!(p, hir::ContentPart::Span(_)))
}
fn content_has_nonempty_text(parts: &[hir::ContentPart]) -> bool {
parts.iter().any(|p| match p {
hir::ContentPart::Text(s) => !s.trim().is_empty(),
hir::ContentPart::Span(span) => content_has_nonempty_text(&span.children),
_ => false,
})
}
fn build_recognized_parts(
parts: &[hir::ContentPart],
ctx: &mut LowerCtx<'_>,
out: &mut Vec<LinePart>,
hash_source: &mut String,
slot_exprs: &mut Vec<lir::Expr>,
slot_info: &mut Vec<SlotInfo>,
slot_idx: &mut u8,
) {
for part in parts {
match part {
hir::ContentPart::Text(s) => {
out.push(LinePart::Literal(s.clone()));
hash_source.push_str(s);
}
hir::ContentPart::Interpolation(expr) => {
out.push(LinePart::Slot(*slot_idx));
slot_exprs.push(lower_expr(expr, ctx));
slot_info.push(SlotInfo {
index: *slot_idx,
name: display_expr(expr),
});
hash_source.push_str("{…}");
*slot_idx = slot_idx.saturating_add(1);
}
hir::ContentPart::Span(span) => {
let mut children = Vec::with_capacity(span.children.len());
build_recognized_parts(
&span.children,
ctx,
&mut children,
hash_source,
slot_exprs,
slot_info,
slot_idx,
);
out.push(LinePart::Span {
name: span.name.clone(),
attrs: span
.attrs
.iter()
.map(|attr| (attr.name.clone(), attr.value.clone()))
.collect(),
children,
});
}
hir::ContentPart::Glue
| hir::ContentPart::Spring
| hir::ContentPart::InlineConditional(_)
| hir::ContentPart::InlineSequence(_) => {
unreachable!("try_recognize_template already validated")
}
}
}
}