use std::{cell::Cell, collections::HashSet};
use oxvg_ast::{
element::Element,
is_element,
visitor::{Context, PrepareOutcome, Visitor},
};
use oxvg_collections::{
atom::Atom,
attribute::{Attr, AttrId},
name::{Prefix, QualName},
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::Tsify;
use crate::error::JobsError;
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "napi", napi(object))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct RemoveUnusedNS(pub bool);
#[derive(Default)]
struct State<'input> {
unused_namespaces: Cell<HashSet<Atom<'input>>>,
}
impl<'input, 'arena> Visitor<'input, 'arena> for RemoveUnusedNS {
type Error = JobsError<'input>;
fn prepare(
&self,
document: &Element<'input, 'arena>,
context: &mut Context<'input, 'arena, '_>,
) -> Result<PrepareOutcome, Self::Error> {
if self.0 {
State::default().start_with_context(document, context)?;
}
Ok(PrepareOutcome::skip)
}
}
impl<'input, 'arena> Visitor<'input, 'arena> for State<'input> {
type Error = JobsError<'input>;
fn document(
&self,
document: &Element<'input, 'arena>,
_content: &Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
let mut unused_namespaces = self.unused_namespaces.take();
document.children_iter().for_each(|e| {
root_element(&e, &mut unused_namespaces);
});
self.unused_namespaces.set(unused_namespaces);
Ok(())
}
fn element(
&self,
element: &Element<'input, 'arena>,
_context: &mut Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
let mut unused_namespaces = self.unused_namespaces.take();
if unused_namespaces.is_empty() {
return Ok(());
}
let prefix = element.prefix();
if !prefix.is_empty() {
unused_namespaces.remove(prefix.ns().uri());
}
for attr in element.attributes() {
let prefix = attr.prefix();
if !prefix.is_empty() {
unused_namespaces.remove(prefix.ns().uri());
}
}
self.unused_namespaces.set(unused_namespaces);
Ok(())
}
fn exit_document(
&self,
document: &Element<'input, 'arena>,
_context: &Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
let mut unused_namespaces = self.unused_namespaces.take();
document.children_iter().for_each(|e| {
exit_root_element(&e, &mut unused_namespaces);
});
self.unused_namespaces.set(unused_namespaces);
Ok(())
}
}
fn root_element<'input>(
element: &Element<'input, '_>,
unused_namespaces: &mut HashSet<Atom<'input>>,
) {
if !is_element!(element, Svg) {
return;
}
for attr in element.attributes() {
if let Attr::Unparsed {
attr_id:
AttrId::Unknown(QualName {
prefix: Prefix::XMLNS,
..
}),
value,
} = &*attr
{
unused_namespaces.insert(value.clone());
}
}
}
fn exit_root_element(element: &Element, unused_namespaces: &mut HashSet<Atom>) {
if !is_element!(element, Svg) {
return;
}
element.attributes().retain(|attr| {
let Attr::Unparsed {
attr_id:
AttrId::Unknown(QualName {
prefix: Prefix::XMLNS,
..
}),
value,
} = attr
else {
return true;
};
!unused_namespaces.contains(value)
});
}
impl Default for RemoveUnusedNS {
fn default() -> Self {
Self(true)
}
}
#[test]
fn remove_unused_n_s() -> anyhow::Result<()> {
use crate::test_config;
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:test="http://test.com/">
<g>
test
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:test="http://test.com/">
<g test:attr="val">
test
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:test="http://test.com/" xmlns:test2="http://test2.com/">
<g test:attr="val">
<g>
test
</g>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:test="http://test.com/" xmlns:test2="http://test2.com/">
<g test:attr="val">
<g test2:attr="val">
test
</g>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:test="http://test.com/" xmlns:test2="http://test2.com/">
<g>
<test:elem>
test
</test:elem>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:test="http://test.com/" xmlns:test2="http://test2.com/">
<test:elem>
<test2:elem>
test
</test2:elem>
</test:elem>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeUnusedNS": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" sodipodi:docname="test.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd">
test
</svg>"#
),
)?);
Ok(())
}