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;
for part in &content.parts {
match part {
hir::ContentPart::Text(s) => {
template_parts.push(LinePart::Literal(s.clone()));
hash_source.push_str(s);
}
hir::ContentPart::Interpolation(expr) => {
template_parts.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);
}
_ => unreachable!("try_recognize_template already validated"),
}
}
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 {
let mut has_interpolation = false;
let mut has_text = false;
for part in &content.parts {
match part {
hir::ContentPart::Text(s) => {
if !s.trim().is_empty() {
has_text = true;
}
}
hir::ContentPart::Interpolation(_) => {
has_interpolation = true;
}
_ => return false,
}
}
has_interpolation && has_text
}