use std::{borrow::Cow, sync::LazyLock};
use cow_utils::CowUtils;
use regex::Regex;
use rustc_hash::FxHashSet;
use super::html_tag::{AttrValue, HtmlTagChildren, HtmlTagDescriptor};
static HEAD_INJECT_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([ \t]*)</head>").unwrap());
static HEAD_PREPEND_INJECT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([ \t]*)<head[^>]*>").unwrap());
static BODY_PREPEND_INJECT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([ \t]*)<body[^>]*>").unwrap());
static DOCTYPE_PREPEND_INJECT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)<!doctype html>").unwrap());
static HTML_PREPEND_INJECT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([ \t]*)<html[^>]*>").unwrap());
static UNARY_TAGS: LazyLock<FxHashSet<&'static str>> =
LazyLock::new(|| FxHashSet::from_iter(["link", "meta", "base"]));
fn increment_indent(indent: &str) -> String {
if indent.is_empty() {
return " ".to_string();
}
rolldown_utils::concat_string!(indent, if indent.starts_with('\t') { "\t" } else { " " })
}
fn serialize_attrs(attrs: Option<&rustc_hash::FxHashMap<&'static str, AttrValue>>) -> String {
let Some(attrs) = attrs else {
return String::new();
};
let mut result = String::new();
for (key, value) in attrs {
match value {
AttrValue::String(s) => {
result.push_str(&rolldown_utils::concat_string!(
" ",
key,
"=\"",
s.cow_replace('&', "&")
.cow_replace('"', """)
.cow_replace('<', "<")
.cow_replace('>', ">"),
"\""
));
}
AttrValue::Boolean(true) => {
result.push_str(&rolldown_utils::concat_string!(" ", key));
}
AttrValue::Boolean(false) | AttrValue::Undefined => {}
}
}
result
}
fn serialize_tag(tag: &HtmlTagDescriptor, indent: &str) -> String {
let attrs_str = serialize_attrs(tag.attrs.as_ref());
if UNARY_TAGS.contains(tag.tag) {
rolldown_utils::concat_string!("<", tag.tag, attrs_str, ">")
} else {
rolldown_utils::concat_string!(
"<",
tag.tag,
attrs_str,
">",
match &tag.children {
Some(HtmlTagChildren::String(s)) => Cow::Borrowed(s.as_str()),
Some(HtmlTagChildren::Tags(tags)) if !tags.is_empty() => Cow::Owned(
tags
.iter()
.map(|tag| rolldown_utils::concat_string!(indent, serialize_tag(tag, indent)))
.collect::<Vec<_>>()
.join("\n")
),
_ => Cow::Borrowed(""),
},
"</",
tag.tag,
">"
)
}
}
fn serialize_tags(tags: &[HtmlTagDescriptor], indent: &str) -> String {
tags
.iter()
.map(|tag| rolldown_utils::concat_string!(indent, serialize_tag(tag, indent)))
.collect::<Vec<_>>()
.join("\n")
}
fn prepend_inject_fallback<'a>(html: &'a str, tags: &[HtmlTagDescriptor]) -> Cow<'a, str> {
if HTML_PREPEND_INJECT_RE.is_match(html) {
return HTML_PREPEND_INJECT_RE.replace(html, |caps: ®ex::Captures| {
rolldown_utils::concat_string!(&caps[0], "\n", serialize_tags(tags, ""))
});
}
if DOCTYPE_PREPEND_INJECT_RE.is_match(html) {
return DOCTYPE_PREPEND_INJECT_RE.replace(html, |caps: ®ex::Captures| {
rolldown_utils::concat_string!(&caps[0], "\n", serialize_tags(tags, ""))
});
}
Cow::Owned(rolldown_utils::concat_string!(serialize_tags(tags, ""), html))
}
pub fn inject_to_head<'a>(
html: &'a str,
tags: &[HtmlTagDescriptor],
prepend: bool,
) -> Cow<'a, str> {
if tags.is_empty() {
return Cow::Borrowed(html);
}
if prepend {
if HEAD_PREPEND_INJECT_RE.is_match(html) {
return HEAD_PREPEND_INJECT_RE.replace(html, |caps: ®ex::Captures| {
rolldown_utils::concat_string!(
&caps[0],
"\n",
serialize_tags(tags, &increment_indent(&caps[1]))
)
});
}
} else {
if HEAD_INJECT_RE.is_match(html) {
return HEAD_INJECT_RE.replace(html, |caps: ®ex::Captures| {
serialize_tags(tags, &increment_indent(&caps[1])) + &caps[0]
});
}
if BODY_PREPEND_INJECT_RE.is_match(html) {
return BODY_PREPEND_INJECT_RE
.replace(html, |caps: ®ex::Captures| serialize_tags(tags, &caps[1]) + "\n" + &caps[0]);
}
}
prepend_inject_fallback(html, tags)
}