use crate::render_dispatch::RenderedNode;
pub struct SpliceState<'a> {
plan: Option<&'a [RenderedNode]>,
annotation_idx: usize,
skip_depth: usize,
}
impl<'a> SpliceState<'a> {
pub fn new(plan: Option<&'a [RenderedNode]>) -> Self {
Self {
plan,
annotation_idx: 0,
skip_depth: 0,
}
}
pub fn advance_at_start(&mut self, label: &str) -> Option<&str> {
let plan = self.plan?;
if self.skip_depth > 0 {
if let Some(entry) = plan.get(self.annotation_idx) {
if entry.label == label {
self.annotation_idx += 1;
}
}
self.skip_depth += 1;
return None;
}
let entry = plan.get(self.annotation_idx)?;
if entry.label != label {
return None;
}
self.annotation_idx += 1;
let content = entry.output.as_deref()?;
self.skip_depth = 1;
Some(content)
}
pub fn should_skip(&self) -> bool {
self.skip_depth > 0
}
pub fn advance_at_end(&mut self) {
if self.skip_depth > 0 {
self.skip_depth -= 1;
}
}
}
#[derive(Default)]
pub struct SentinelBuffer {
outputs: Vec<String>,
}
impl SentinelBuffer {
pub const PREFIX: &'static str = "LEX-RENDER-SPLICE:";
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, content: String) -> String {
let idx = self.outputs.len();
self.outputs.push(content);
format!("{}{}", Self::PREFIX, idx)
}
pub fn is_empty(&self) -> bool {
self.outputs.is_empty()
}
pub fn replace(&self, html: &str) -> String {
if self.outputs.is_empty() {
return html.to_string();
}
let mut out = String::with_capacity(html.len());
let mut remaining = html;
let pattern_open = format!("<!--{}", Self::PREFIX);
while let Some(start) = remaining.find(&pattern_open) {
out.push_str(&remaining[..start]);
let after_prefix = &remaining[start + pattern_open.len()..];
let Some(end_marker) = after_prefix.find("-->") else {
out.push_str(&remaining[start..]);
remaining = "";
break;
};
let id_str = after_prefix[..end_marker].trim();
match id_str.parse::<usize>() {
Ok(idx) if idx < self.outputs.len() => {
out.push_str(&self.outputs[idx]);
}
_ => {
out.push_str(&remaining[start..start + pattern_open.len() + end_marker + 3]);
}
}
remaining = &after_prefix[end_marker + 3..];
}
out.push_str(remaining);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rendered(label: &str, output: Option<&str>) -> RenderedNode {
RenderedNode {
label: label.into(),
output: output.map(str::to_string),
diagnostic: None,
}
}
#[test]
fn no_plan_never_splices() {
let mut s = SpliceState::new(None);
assert!(s.advance_at_start("acme.task").is_none());
assert!(!s.should_skip());
s.advance_at_end();
}
#[test]
fn plan_entry_without_output_falls_through() {
let plan = [rendered("acme.task", None)];
let mut s = SpliceState::new(Some(&plan));
assert!(s.advance_at_start("acme.task").is_none());
assert!(!s.should_skip());
}
#[test]
fn plan_entry_with_output_returns_content_and_skips_body() {
let plan = [rendered("acme.task", Some("<RENDERED/>"))];
let mut s = SpliceState::new(Some(&plan));
assert_eq!(s.advance_at_start("acme.task"), Some("<RENDERED/>"));
assert!(s.should_skip());
s.advance_at_end();
assert!(!s.should_skip());
}
#[test]
fn label_mismatch_falls_through() {
let plan = [rendered("acme.task", Some("<X/>"))];
let mut s = SpliceState::new(Some(&plan));
assert!(s.advance_at_start("other.label").is_none());
assert!(!s.should_skip());
}
#[test]
fn nested_annotations_inside_splice_keep_depth() {
let plan = [rendered("outer", Some("<OUTER/>"))];
let mut s = SpliceState::new(Some(&plan));
assert_eq!(s.advance_at_start("outer"), Some("<OUTER/>"));
assert!(s.should_skip());
assert!(s.advance_at_start("inner").is_none());
assert!(s.should_skip());
s.advance_at_end(); assert!(s.should_skip());
s.advance_at_end(); assert!(!s.should_skip());
}
#[test]
fn counter_advances_across_unspliced_annotations() {
let plan = [
rendered("a", None),
rendered("b", Some("<B/>")),
rendered("c", None),
];
let mut s = SpliceState::new(Some(&plan));
assert!(s.advance_at_start("a").is_none());
s.advance_at_end();
assert_eq!(s.advance_at_start("b"), Some("<B/>"));
s.advance_at_end();
assert!(s.advance_at_start("c").is_none());
s.advance_at_end();
}
#[test]
fn sentinel_buffer_round_trips_single_splice() {
let mut buf = SentinelBuffer::new();
let sentinel = buf.push("<DIV/>".into());
let html = format!("<p><!--{sentinel}--></p>");
assert_eq!(buf.replace(&html), "<p><DIV/></p>");
}
#[test]
fn sentinel_buffer_round_trips_multiple_in_order() {
let mut buf = SentinelBuffer::new();
let s0 = buf.push("ZERO".into());
let s1 = buf.push("ONE".into());
let html = format!("a<!--{s0}-->b<!--{s1}-->c");
assert_eq!(buf.replace(&html), "aZERObONEc");
}
#[test]
fn sentinel_buffer_empty_is_no_op() {
let buf = SentinelBuffer::new();
assert!(buf.is_empty());
assert_eq!(buf.replace("plain html"), "plain html");
}
#[test]
fn sentinel_buffer_leaves_unknown_index_visible() {
let mut buf = SentinelBuffer::new();
buf.push("recorded".to_string());
let html = "<!--LEX-RENDER-SPLICE:7-->";
assert_eq!(
buf.replace(html),
html,
"out-of-range index must stay visible even when other sentinels are recorded"
);
}
#[test]
fn unregistered_annotation_before_registered_keeps_cursor_aligned() {
let plan = [rendered("acme.task", Some("<HANDLER/>"))];
let mut s = SpliceState::new(Some(&plan));
assert!(s.advance_at_start("unknown.label").is_none());
s.advance_at_end();
assert_eq!(s.advance_at_start("acme.task"), Some("<HANDLER/>"));
s.advance_at_end();
}
#[test]
fn nested_registered_annotation_inside_splice_consumes_its_plan_entry() {
let plan = [
rendered("outer", Some("<OUTER/>")),
rendered("inner", Some("<INNER_UNUSED/>")),
rendered("after", Some("<AFTER/>")),
];
let mut s = SpliceState::new(Some(&plan));
assert_eq!(s.advance_at_start("outer"), Some("<OUTER/>"));
assert!(s.should_skip());
assert!(s.advance_at_start("inner").is_none());
s.advance_at_end(); s.advance_at_end(); assert_eq!(s.advance_at_start("after"), Some("<AFTER/>"));
s.advance_at_end();
}
}