use html5ever::Namespace;
use std::collections::BTreeMap;
use crate::ns::{defaults::parse::parse_preamble, NsResult};
use super::nsdefaults::NsDefaults;
const ESTIMATED_BYTES_PER_NAMESPACE: usize = 50;
pub struct NsDefaultsBuilder {
namespaces: BTreeMap<String, Namespace>,
}
impl NsDefaultsBuilder {
pub fn new() -> Self {
NsDefaultsBuilder {
namespaces: BTreeMap::new(),
}
}
pub fn namespace(mut self, prefix: impl AsRef<str>, ns: impl Into<Namespace>) -> Self {
let prefix = prefix.as_ref().to_string();
let ns = ns.into();
self.namespaces.insert(prefix, ns);
self
}
pub fn from_string(self, html: impl Into<String>) -> NsResult<NsDefaults> {
let html = html.into();
let tag_info = parse_preamble(&html)?;
let added_xmlns = build_xmlns_decl(&self.namespaces, &tag_info, &html);
Ok(NsDefaults {
html,
tag_info,
added_xmlns,
})
}
}
impl Default for NsDefaultsBuilder {
fn default() -> Self {
Self::new()
}
}
fn build_xmlns_decl(
namespaces: &BTreeMap<String, Namespace>,
tag_info: &super::parse::HtmlTagInfo,
html: &str,
) -> String {
if namespaces.is_empty() {
return String::new();
}
let mut existing_prefixes = std::collections::HashSet::new();
for i in 0..tag_info.xmlns_count() {
if let Ok(prefix) = tag_info.get_prefix(i, html) {
existing_prefixes.insert(prefix.to_string());
}
}
let estimated_capacity = namespaces.len() * ESTIMATED_BYTES_PER_NAMESPACE;
let mut declarations = String::with_capacity(estimated_capacity);
for (prefix, uri) in namespaces {
if !existing_prefixes.contains(prefix) {
declarations.push_str(&format!(" xmlns:{prefix}=\"{uri}\""));
}
}
declarations
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_duplicate_namespace_overwrites() {
let html = r#"<html><body>Test</body></html>"#;
let ns_defaults = NsDefaultsBuilder::new()
.namespace("svg", "http://example.com/fake-svg")
.namespace("svg", "http://www.w3.org/2000/svg")
.from_string(html)
.expect("Failed to parse HTML");
let result = ns_defaults.to_string();
assert!(result.contains("xmlns:svg=\"http://www.w3.org/2000/svg\""));
assert!(!result.contains("http://example.com/fake-svg"));
}
#[test]
fn test_multiple_namespaces() {
let html = r#"<html><body>Test</body></html>"#;
let ns_defaults = NsDefaultsBuilder::new()
.namespace("svg", "http://www.w3.org/2000/svg")
.namespace("custom", "http://example.com/ns")
.namespace("other", "http://other.com/ns")
.from_string(html)
.expect("Failed to parse HTML");
let result = ns_defaults.to_string();
assert!(result.contains("xmlns:svg=\"http://www.w3.org/2000/svg\""));
assert!(result.contains("xmlns:custom=\"http://example.com/ns\""));
assert!(result.contains("xmlns:other=\"http://other.com/ns\""));
}
#[test]
fn test_existing_namespace_not_duplicated() {
let html = r#"<html xmlns:svg="http://www.w3.org/2000/svg"><body>Test</body></html>"#;
let ns_defaults = NsDefaultsBuilder::new()
.namespace("svg", "http://www.w3.org/2000/svg")
.namespace("custom", "http://example.com/ns")
.from_string(html)
.expect("Failed to parse HTML");
let result = ns_defaults.to_string();
assert!(result.contains("xmlns:custom=\"http://example.com/ns\""));
let svg_count = result.matches("xmlns:svg").count();
assert_eq!(svg_count, 1);
}
#[test]
fn test_default_implementation() {
let default_builder = NsDefaultsBuilder::default();
let new_builder = NsDefaultsBuilder::new();
let html = "<html><body>Test</body></html>";
let ns1 = default_builder.from_string(html).expect("Failed to parse");
let ns2 = new_builder.from_string(html).expect("Failed to parse");
assert_eq!(ns1.to_string(), ns2.to_string());
}
#[test]
fn test_empty_builder_no_modifications() {
let html = r#"<html lang="en"><body>Test</body></html>"#;
let ns_defaults = NsDefaultsBuilder::new()
.from_string(html)
.expect("Failed to parse HTML");
let result = ns_defaults.to_string();
assert_eq!(result, html);
}
}