use std::collections::{HashMap, HashSet};
use roxmltree::Node;
use super::NodeVisibility;
use super::ns_common::collect_ns_declarations;
use super::prefix::{attribute_prefix, element_prefix};
use super::serialize::NsRenderer;
const MISSING_NAMESPACE_NODE: &str = "\0";
fn nearest_utilizer_key(prefix: &str) -> String {
format!("\0exclusive-nearest-utilizer:{prefix}")
}
pub(crate) struct ExclusiveNsRenderer<'a> {
inclusive_prefixes: &'a HashSet<String>,
}
impl<'a> ExclusiveNsRenderer<'a> {
pub(crate) fn new(inclusive_prefixes: &'a HashSet<String>) -> Self {
Self { inclusive_prefixes }
}
}
impl NsRenderer for ExclusiveNsRenderer<'_> {
fn render_namespaces<'n>(
&self,
node: Node<'n, '_>,
parent_rendered: &HashMap<String, String>,
visibility: Option<&dyn NodeVisibility>,
) -> (Vec<(String, String)>, HashMap<String, String>) {
let utilized = visibly_utilized_prefixes(node, visibility);
let (mut declarations, mut rendered) = collect_ns_declarations(
node,
parent_rendered,
visibility,
|prefix| self.inclusive_prefixes.contains(prefix),
|prefix, _| utilized.contains(prefix) || self.inclusive_prefixes.contains(prefix),
);
if let (Some(visibility), Some(parent)) = (visibility, node.parent())
&& parent.is_element()
{
for namespace in node.namespaces() {
let prefix = namespace.name().unwrap_or("");
let uri = namespace.uri();
let selected_here = visibility.contains_namespace(node, prefix, uri);
let declaration_suppressed = parent_rendered.get(prefix).map(String::as_str)
== Some(uri)
&& !declarations
.iter()
.any(|(declared_prefix, _)| declared_prefix == prefix);
let utilizer_key = nearest_utilizer_key(prefix);
let nearest_utilizer_omitted_namespace = parent_rendered
.get(&utilizer_key)
.is_some_and(|selected_uri| selected_uri == MISSING_NAMESPACE_NODE);
let uses_exclusive_rendering =
utilized.contains(prefix) && !self.inclusive_prefixes.contains(prefix);
if uses_exclusive_rendering
&& selected_here
&& declaration_suppressed
&& nearest_utilizer_omitted_namespace
{
declarations.push((prefix.to_owned(), uri.to_owned()));
declarations.sort_by(|left, right| left.0.cmp(&right.0));
}
if uses_exclusive_rendering {
rendered.insert(
utilizer_key,
if selected_here {
uri.to_owned()
} else {
MISSING_NAMESPACE_NODE.to_owned()
},
);
}
}
}
(declarations, rendered)
}
fn renders_selected_namespace_of_omitted_element(&self, prefix: &str) -> bool {
self.inclusive_prefixes.contains(prefix)
}
}
fn visibly_utilized_prefixes<'a>(
node: Node<'a, '_>,
visibility: Option<&dyn NodeVisibility>,
) -> HashSet<&'a str> {
let mut utilized = HashSet::new();
let el_prefix = element_prefix(node);
if !el_prefix.is_empty() {
utilized.insert(el_prefix);
} else {
utilized.insert("");
}
for attr in node.attributes() {
if visibility
.is_some_and(|set| !set.contains_attribute(node, attr.namespace(), attr.name()))
{
continue;
}
let attr_prefix = attribute_prefix(node, &attr);
if !attr_prefix.is_empty() {
utilized.insert(attr_prefix);
}
}
utilized
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::super::serialize::{
C14nConfig, serialize_canonical, serialize_canonical_visible_with_position,
};
use super::*;
use roxmltree::Document;
use std::collections::HashSet;
struct NamespaceGapVisibility;
impl NodeVisibility for NamespaceGapVisibility {
fn contains_node(&self, _node: Node<'_, '_>) -> bool {
true
}
fn contains_attribute(
&self,
_owner: Node<'_, '_>,
_namespace: Option<&str>,
_local_name: &str,
) -> bool {
true
}
fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, _uri: &str) -> bool {
prefix != "p" || owner.tag_name().name() != "gap"
}
}
fn exc_c14n(xml: &str, prefix_list: &HashSet<String>) -> String {
let doc = Document::parse(xml).expect("parse");
let renderer = ExclusiveNsRenderer::new(prefix_list);
let mut out = Vec::new();
serialize_canonical(
&doc,
None,
false,
&renderer,
C14nConfig {
inherit_xml_attrs: false,
fixup_xml_base: false,
},
&mut out,
)
.expect("c14n");
String::from_utf8(out).expect("utf8")
}
#[test]
fn only_utilized_ns_rendered() {
let xml = r#"<root xmlns:a="http://a.com" xmlns:b="http://b.com"><a:child/></root>"#;
let result = exc_c14n(xml, &HashSet::new());
assert!(!result.contains("xmlns:b"));
assert!(result.contains(r#"<a:child xmlns:a="http://a.com">"#));
}
#[test]
fn forced_prefix_via_prefix_list() {
let xml = r#"<root xmlns:a="http://a.com" xmlns:b="http://b.com"><child/></root>"#;
let mut forced = HashSet::new();
forced.insert("b".to_string());
let result = exc_c14n(xml, &forced);
assert!(result.contains(r#"xmlns:b="http://b.com""#));
}
#[test]
fn sibling_elements_redeclare() {
let xml = r#"<root xmlns:a="http://a.com"><a:one/><a:two/></root>"#;
let result = exc_c14n(xml, &HashSet::new());
assert!(result.contains(r#"<a:one xmlns:a="http://a.com">"#));
assert!(result.contains(r#"<a:two xmlns:a="http://a.com">"#));
}
#[test]
fn default_ns_utilized() {
let xml = r#"<root xmlns="http://example.com"><child/></root>"#;
let result = exc_c14n(xml, &HashSet::new());
assert!(result.contains(r#"<root xmlns="http://example.com">"#));
assert_eq!(
result,
r#"<root xmlns="http://example.com"><child></child></root>"#
);
}
#[test]
fn unprefixed_element_undeclares_default_ns() {
let xml = r#"<root xmlns="http://example.com"><child xmlns=""/></root>"#;
let result = exc_c14n(xml, &HashSet::new());
assert!(
result.contains(r#"<child xmlns="">"#),
"xmlns=\"\" must be emitted for undeclaration. Got: {result}"
);
}
#[test]
fn redeclares_prefix_after_one_namespace_node_discontinuity() {
let xml = r#"<p:root xmlns:p="urn:p"><p:gap><p:leaf/></p:gap></p:root>"#;
let doc = Document::parse(xml).expect("parse");
let prefix_list = HashSet::new();
let renderer = ExclusiveNsRenderer::new(&prefix_list);
let mut out = Vec::new();
serialize_canonical_visible_with_position(
&doc,
Some(&NamespaceGapVisibility),
false,
&renderer,
C14nConfig {
inherit_xml_attrs: false,
fixup_xml_base: false,
},
None,
&mut out,
)
.expect("c14n");
assert_eq!(
String::from_utf8(out).expect("utf8"),
r#"<p:root xmlns:p="urn:p"><p:gap><p:leaf xmlns:p="urn:p"></p:leaf></p:gap></p:root>"#
);
}
}