use std::sync::OnceLock;
use regex::Regex;
fn attr_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r#"([\w-]+)="%%SEAM:([^%]+)%%""#).expect("valid regex"))
}
fn style_sentinel_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r#"style="([^"]*%%SEAM:[^"]*)""#).expect("valid regex"))
}
fn text_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"%%SEAM:([^%]+)%%").expect("valid regex"))
}
fn tag_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"<([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>").expect("valid regex"))
}
pub fn sentinel_to_slots(html: &str) -> String {
let attr_re = attr_re();
let text_re = text_re();
let tag_re = tag_re();
let style_re = style_sentinel_re();
let mut result = String::with_capacity(html.len());
let mut last_end = 0;
for cap in tag_re.captures_iter(html) {
let full_match = cap.get(0).expect("capture group exists");
let attrs_part = cap.get(2).expect("capture group exists").as_str();
let has_attr_sentinels = attr_re.is_match(attrs_part);
let has_style_sentinels = style_re.is_match(attrs_part);
if !has_attr_sentinels && !has_style_sentinels {
result.push_str(&html[last_end..full_match.end()]);
last_end = full_match.end();
continue;
}
result.push_str(&html[last_end..full_match.start()]);
let mut working_attrs = attrs_part.to_string();
let mut comments = Vec::new();
if has_style_sentinels && let Some(style_cap) = style_re.captures(&working_attrs) {
let style_value = style_cap[1].to_string();
let mut static_pairs = Vec::new();
for pair in style_value.split(';') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
if pair.contains("%%SEAM:") {
if let Some(colon_pos) = pair.find(':') {
let css_property = &pair[..colon_pos];
let value_part = &pair[colon_pos + 1..];
if let (Some(start), Some(_end)) = (value_part.find("%%SEAM:"), value_part.find("%%")) {
let after_prefix = &value_part[start + 7..];
if let Some(end2) = after_prefix.find("%%") {
let path = &after_prefix[..end2];
comments.push(format!("<!--seam:{path}:style:{css_property}-->"));
}
}
}
} else {
static_pairs.push(pair.to_string());
}
}
let full_style_match = style_cap.get(0).expect("capture group exists").as_str();
if static_pairs.is_empty() {
working_attrs = working_attrs.replace(full_style_match, "");
} else {
let new_style = format!(r#"style="{}""#, static_pairs.join(";"));
working_attrs = working_attrs.replace(full_style_match, &new_style);
}
}
if has_attr_sentinels {
for attr_cap in attr_re.captures_iter(&working_attrs) {
let attr_name = &attr_cap[1];
let path = &attr_cap[2];
comments.push(format!("<!--seam:{path}:attr:{attr_name}-->"));
}
working_attrs = attr_re.replace_all(&working_attrs, "").to_string();
}
for comment in &comments {
result.push_str(comment);
}
let tag_name = cap.get(1).expect("capture group exists").as_str();
let cleaned_attrs = working_attrs.trim();
if cleaned_attrs.is_empty() {
result.push_str(&format!("<{tag_name}>"));
} else {
result.push_str(&format!("<{tag_name} {cleaned_attrs}>"));
}
last_end = full_match.end();
}
result.push_str(&html[last_end..]);
text_re.replace_all(&result, "<!--seam:$1-->").into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_sentinels() {
let html = "<p>%%SEAM:user.name%%</p>";
assert_eq!(sentinel_to_slots(html), "<p><!--seam:user.name--></p>");
}
#[test]
fn attribute_sentinels() {
let html = r#"<img src="%%SEAM:user.avatar%%" alt="avatar">"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:user.avatar:attr:src-->"));
assert!(!result.contains("%%SEAM:"));
assert!(result.contains(r#"alt="avatar">"#));
}
#[test]
fn mixed_sentinels() {
let html = r#"<a href="%%SEAM:url%%">%%SEAM:label%%</a>"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:url:attr:href-->"));
assert!(result.contains("<!--seam:label-->"));
assert!(!result.contains("%%SEAM:"));
}
#[test]
fn no_sentinels() {
let html = "<p>Hello world</p>";
assert_eq!(sentinel_to_slots(html), html);
}
#[test]
fn multiple_text_sentinels() {
let html = "<div>%%SEAM:a%% and %%SEAM:b%%</div>";
let result = sentinel_to_slots(html);
assert_eq!(result, "<div><!--seam:a--> and <!--seam:b--></div>");
}
#[test]
fn preserves_react_ssr_comment_boundaries() {
let html = "<span>by <!-- -->%%SEAM:author%%</span>";
let result = sentinel_to_slots(html);
assert_eq!(result, "<span>by <!-- --><!--seam:author--></span>");
}
#[test]
fn preserves_react_boundary_between_two_text_slots() {
let html = "<span>%%SEAM:first%%<!-- -->%%SEAM:last%%</span>";
let result = sentinel_to_slots(html);
assert_eq!(result, "<span><!--seam:first--><!-- --><!--seam:last--></span>");
}
#[test]
fn preserves_react_boundary_inside_table_cell_text() {
let html = "<td>by <!-- -->%%SEAM:author%%</td>";
let result = sentinel_to_slots(html);
assert_eq!(result, "<td>by <!-- --><!--seam:author--></td>");
}
#[test]
fn preserves_react_suspense_markers() {
let html = "<!--$--><div>%%SEAM:title%%</div><!--/$-->";
let result = sentinel_to_slots(html);
assert_eq!(result, "<!--$--><div><!--seam:title--></div><!--/$-->");
}
#[test]
fn preserves_react_activity_markers() {
let html = "<!--&--><div>%%SEAM:content%%</div><!--/&-->";
let result = sentinel_to_slots(html);
assert_eq!(result, "<!--&--><div><!--seam:content--></div><!--/&-->");
}
#[test]
fn sentinel_in_escaped_html_context() {
let html = "<p>& %%SEAM:user%% <end></p>";
let result = sentinel_to_slots(html);
assert_eq!(result, "<p>& <!--seam:user--> <end></p>");
}
#[test]
fn data_attr_sentinel() {
let html = r#"<div data-testid="%%SEAM:tid%%">content</div>"#;
let result = sentinel_to_slots(html);
assert!(
result.contains("<!--seam:tid:attr:data-testid-->"),
"data-testid sentinel not extracted: {result}"
);
assert!(!result.contains("%%SEAM:"), "raw sentinel remains: {result}");
}
#[test]
fn aria_attr_sentinel() {
let html = r#"<button aria-label="%%SEAM:label%%">click</button>"#;
let result = sentinel_to_slots(html);
assert!(
result.contains("<!--seam:label:attr:aria-label-->"),
"aria-label sentinel not extracted: {result}"
);
assert!(!result.contains("%%SEAM:"), "raw sentinel remains: {result}");
}
#[test]
fn tabindex_void_element_no_trailing_space() {
let html = r#"<input tabIndex="%%SEAM:ti%%">"#;
let result = sentinel_to_slots(html);
assert!(
result.contains("<!--seam:ti:attr:tabIndex-->"),
"tabIndex sentinel not extracted: {result}"
);
assert_eq!(result, "<!--seam:ti:attr:tabIndex--><input>");
}
#[test]
fn data_attr_with_other_attrs() {
let html = r#"<div class="x" data-id="%%SEAM:id%%">text</div>"#;
let result = sentinel_to_slots(html);
assert!(
result.contains("<!--seam:id:attr:data-id-->"),
"data-id sentinel not extracted: {result}"
);
assert!(!result.contains("%%SEAM:"), "raw sentinel remains: {result}");
}
#[test]
fn multiple_hyphenated_attrs() {
let html = r#"<div data-a="%%SEAM:a%%" aria-b="%%SEAM:b%%">text</div>"#;
let result = sentinel_to_slots(html);
assert!(
result.contains("<!--seam:a:attr:data-a-->"),
"data-a sentinel not extracted: {result}"
);
assert!(
result.contains("<!--seam:b:attr:aria-b-->"),
"aria-b sentinel not extracted: {result}"
);
assert!(!result.contains("%%SEAM:"), "raw sentinel remains: {result}");
}
#[test]
fn style_all_dynamic() {
let html = r#"<div style="margin-top:%%SEAM:mt%%">text</div>"#;
let result = sentinel_to_slots(html);
assert_eq!(result, "<!--seam:mt:style:margin-top--><div>text</div>");
}
#[test]
fn style_multi_dynamic() {
let html = r#"<div style="margin-top:%%SEAM:mt%%;font-size:%%SEAM:fs%%">text</div>"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:mt:style:margin-top-->"));
assert!(result.contains("<!--seam:fs:style:font-size-->"));
assert!(result.contains("<div>text</div>"));
assert!(!result.contains("style="));
}
#[test]
fn style_mixed_static_dynamic() {
let html = r#"<div style="color:red;margin-top:%%SEAM:mt%%">text</div>"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:mt:style:margin-top-->"));
assert!(result.contains(r#"style="color:red""#));
assert!(!result.contains("%%SEAM:"));
}
#[test]
fn style_all_static_unchanged() {
let html = r#"<div style="color:red;font-size:14px">text</div>"#;
assert_eq!(sentinel_to_slots(html), html);
}
#[test]
fn title_text_sentinel() {
let html = "<title>%%SEAM:x%%</title>";
assert_eq!(sentinel_to_slots(html), "<title><!--seam:x--></title>");
}
#[test]
fn meta_content_attr_sentinel() {
let html = r#"<meta name="desc" content="%%SEAM:x%%">"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:x:attr:content-->"));
assert!(result.contains(r#"name="desc""#));
assert!(!result.contains("%%SEAM:"));
}
#[test]
fn link_href_attr_sentinel() {
let html = r#"<link rel="canonical" href="%%SEAM:x%%">"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:x:attr:href-->"));
assert!(result.contains(r#"rel="canonical""#));
assert!(!result.contains("%%SEAM:"));
}
#[test]
fn meta_multiple_attr_sentinels() {
let html = r#"<meta property="%%SEAM:a%%" content="%%SEAM:b%%">"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:a:attr:property-->"));
assert!(result.contains("<!--seam:b:attr:content-->"));
assert!(!result.contains("%%SEAM:"));
}
#[test]
fn hoisted_metadata_mixed() {
let html = r#"<title>%%SEAM:t%%</title><meta name="desc" content="%%SEAM:d%%"><link rel="canonical" href="%%SEAM:u%%"><div><p>%%SEAM:body%%</p></div>"#;
let result = sentinel_to_slots(html);
assert!(result.contains("<!--seam:t-->"));
assert!(result.contains("<!--seam:d:attr:content-->"));
assert!(result.contains("<!--seam:u:attr:href-->"));
assert!(result.contains("<!--seam:body-->"));
assert!(!result.contains("%%SEAM:"));
}
#[test]
fn html_suffix_preserved() {
let html = "<div>%%SEAM:content:html%%</div>";
assert_eq!(sentinel_to_slots(html), "<div><!--seam:content:html--></div>");
}
}