From 50711ad59a8d6243094b4c77b025b5cd0a1aaf27 Mon Sep 17 00:00:00 2001 From: "Anthony J. (Tony) Bufort" Date: Thu, 9 Jul 2026 00:56:02 -0700 Subject: [PATCH] Preserve namespaces of literal result elements in output Literal result elements and their attributes lost their namespaces entirely: xml_name in the XSLT compiler hardcoded an empty namespace when building element and attribute names, so a stylesheet producing XHTML emitted with no xmlns and xml:lang became lang. Fix this in three connected places: - xml_name now passes the parsed name's namespace through to the XmlName IR instruction. - A literal result element copies its in-scope namespace bindings to the result (XSLT 3.0 section 11.1.3), so output keeps the prefixes the stylesheet author chose, including default namespace declarations. The parser computes these per element, minus the XSLT namespace and excluded namespaces, and minus bindings the nearest enclosing literal result element already declares, so nested elements don't redeclare them. Content that constructs a separate tree (xsl:variable, xsl:param, xsl:with-param, xsl:message) resets that suppression, since its elements cannot rely on an enclosing element's declarations. exclude-result-prefixes and extension-element-prefixes now resolve each prefix to a namespace at the element bearing the attribute, as the spec requires, replacing the stored prefix lists and the ExcludeResultPrefixes::combine whose TODO asked for exactly this. (An unbound prefix should be a static error, XTSE0808; left as a TODO since the parser has no error-code machinery yet.) - Serialization performs namespace fixup on the normalized document: every top-level element gets create_missing_prefixes, which invents prefixes for namespaces with no binding (e.g. from xsl:element with a namespace attribute, which previously always failed with XPST0081), after explicitly binding the implicit xml prefix so it isn't given an invented one. The html output method needs no fixup: the html5 serializer emits no namespace declarations (covered by a regression test). XSLT conformance goes from 1098 to 1169 tests passing with the filters updated; XPath conformance is unchanged. Fixes #139. Co-Authored-By: Claude Fable 5 --- vendor/xslt-tests/filters | 71 ------ xee-interpreter/src/sequence/serialization.rs | 23 ++ xee-xslt-ast/src/ast_core.rs | 21 +- xee-xslt-ast/src/attributes.rs | 12 +- xee-xslt-ast/src/context.rs | 107 +++++++- xee-xslt-ast/src/instruction.rs | 34 ++- ..._tests__attributes_on_literal_element.snap | 4 +- ...napshot_tests__literal_result_element.snap | 4 +- ...esult_element_with_standard_attribute.snap | 4 +- ...apshot_tests__nested_literal_elements.snap | 5 +- ...constructor_nested_in_literal_element.snap | 4 +- xee-xslt-compiler/src/ast_ir.rs | 27 ++- xee-xslt-compiler/tests/test_xslt.rs | 228 ++++++++++++++++++ 13 files changed, 427 insertions(+), 117 deletions(-) diff --git a/vendor/xslt-tests/filters b/vendor/xslt-tests/filters index f5218eac4..fa9b913a6 100644 --- a/vendor/xslt-tests/filters +++ b/vendor/xslt-tests/filters @@ -592,7 +592,6 @@ attribute-0401 attribute-0501 attribute-0601 attribute-0701 -attribute-0801 attribute-0803 attribute-0804 attribute-0805 @@ -756,7 +755,6 @@ axes-045 axes-046 axes-048 axes-049 -axes-050 axes-051 axes-052 axes-057 @@ -1011,12 +1009,10 @@ bug-0701 bug-0801 bug-0901 bug-1001 -bug-1101 bug-1201 bug-1202 bug-1203 bug-1301 -bug-1401 bug-1402 bug-1403 bug-1405 @@ -1024,8 +1020,6 @@ bug-1406 bug-1501 bug-1601 bug-1701 -bug-1801 -bug-1802 bug-1901 bug-2001 bug-2101 @@ -1051,7 +1045,6 @@ bug-4001 bug-4301 bug-4401 bug-4501 -bug-4601 bug-4701 bug-4702 bug-4703 @@ -1368,9 +1361,6 @@ copy-2801 copy-3001 copy-3002 copy-3003 -copy-3102 -copy-3301 -copy-3302 copy-3501 copy-3601 copy-3602 @@ -1729,14 +1719,12 @@ element-0003 element-0004 element-0005 element-0006 -element-0102 element-0104 element-0105 element-0106 element-0107 element-0108 element-0110 -element-0111 element-0201 element-0301 element-0302 @@ -3223,7 +3211,6 @@ import-schema-060 import-schema-061 import-schema-062 import-schema-063 -import-schema-064 import-schema-065 import-schema-066 import-schema-067 @@ -3647,20 +3634,13 @@ load-xquery-module-002 load-xquery-module-003 load-xquery-module-004 = lre -lre-003 -lre-005 lre-006 -lre-010 lre-011 lre-012 -lre-014 -lre-017 -lre-018 lre-019 lre-020 lre-021 lre-022 -lre-024 lre-025 lre-026 lre-027 @@ -4431,7 +4411,6 @@ mode-1905 = namespace namespace-0101 namespace-0201 -namespace-0202 namespace-0501 namespace-0601 namespace-0602 @@ -4442,7 +4421,6 @@ namespace-0802 namespace-0901 namespace-0902 namespace-0903 -namespace-0904 namespace-0906 namespace-0907 namespace-0908 @@ -4453,7 +4431,6 @@ namespace-0912 namespace-0913 namespace-0914 namespace-1101 -namespace-1102 namespace-1103 namespace-1104 namespace-1502 @@ -4464,13 +4441,10 @@ namespace-1801 namespace-1901 namespace-2001 namespace-2101 -namespace-2201 namespace-2301 namespace-2302 namespace-2401 namespace-2501 -namespace-2503 -namespace-2601 namespace-2602 namespace-2606 namespace-2607 @@ -4479,9 +4453,7 @@ namespace-2609 namespace-2610 namespace-2611 namespace-2612 -namespace-2614 namespace-2615 -namespace-2616 namespace-2617 namespace-2618 namespace-2619 @@ -4502,8 +4474,6 @@ namespace-2701 namespace-2801 namespace-2901 namespace-3002 -namespace-3003 -namespace-3004 namespace-3101 namespace-3102 namespace-3103 @@ -4523,31 +4493,18 @@ namespace-3116 namespace-3117 namespace-3118 namespace-3119 -namespace-3120 -namespace-3121 -namespace-3122 -namespace-3123 namespace-3124 -namespace-3126 namespace-3127 namespace-3128 -namespace-3129 namespace-3130 namespace-3131 namespace-3132 -namespace-3133 namespace-3134 -namespace-3135 namespace-3136 namespace-3137 -namespace-3138 -namespace-3139 namespace-3140 -namespace-3142 namespace-3143 namespace-3144 -namespace-3145 -namespace-3146 namespace-3147 namespace-3148 namespace-3149 @@ -4557,30 +4514,19 @@ namespace-3152 namespace-3153 namespace-3154 namespace-3155 -namespace-3158 -namespace-3159 namespace-3160 namespace-3161 -namespace-3162 namespace-3163 namespace-3164 namespace-3201 namespace-3202 namespace-3203 namespace-3301 -namespace-3302 -namespace-3303 namespace-3304 -namespace-3306 -namespace-3307 -namespace-3308 -namespace-3309 namespace-3310 namespace-3311 -namespace-3312 namespace-3313 namespace-3314 -namespace-3315 namespace-3401 namespace-3501 namespace-3502 @@ -4588,23 +4534,10 @@ namespace-3503 namespace-3504 namespace-3505 namespace-3601 -namespace-4001 -namespace-4002 -namespace-4003 -namespace-4004 -namespace-4005 -namespace-4007 -namespace-4008 -namespace-4009 -namespace-4301 namespace-4302 namespace-4401 -namespace-4501 namespace-4601 namespace-4801 -namespace-4901 -namespace-5001 -namespace-5101 namespace-5201 namespace-5601 namespace-5602 @@ -4612,8 +4545,6 @@ namespace-5701 namespace-5901 namespace-5902 namespace-5903 -namespace-6002 -namespace-6003 namespace-6201 namespace-6202 = namespace-alias @@ -13619,7 +13550,6 @@ xml-to-json-D511 = xml-version xml-version-002 xml-version-003 -xml-version-006 xml-version-007 xml-version-008 xml-version-009 @@ -13678,7 +13608,6 @@ xpath-default-namespace-0107 xpath-default-namespace-0108 xpath-default-namespace-0201 xpath-default-namespace-0202 -xpath-default-namespace-0301 xpath-default-namespace-0401 xpath-default-namespace-0501 xpath-default-namespace-0502 diff --git a/xee-interpreter/src/sequence/serialization.rs b/xee-interpreter/src/sequence/serialization.rs index 3c3157be8..25b1c5e6d 100644 --- a/xee-interpreter/src/sequence/serialization.rs +++ b/xee-interpreter/src/sequence/serialization.rs @@ -207,12 +207,35 @@ pub(crate) fn serialize_sequence( } } +/// Namespace fixup on a normalized document: constructed elements and +/// attributes can use a namespace that has no prefix binding anywhere in +/// the tree; invent prefixes for these so the result can be serialized. +/// The document can have multiple top-level elements, and each is fixed +/// up separately. +fn namespace_fixup(xot: &mut Xot, node: xot::Node) -> Result<(), error::Error> { + let xml_prefix = xot.xml_prefix(); + let xml_namespace = xot.xml_namespace(); + let elements: Vec<_> = xot + .children(node) + .filter(|child| xot.is_element(*child)) + .collect(); + for element in elements { + // bind the implicit xml prefix explicitly so create_missing_prefixes + // doesn't invent a prefix for it; the binding is never serialized + xot.namespaces_mut(element) + .insert(xml_prefix, xml_namespace); + xot.create_missing_prefixes(element)?; + } + Ok(()) +} + fn serialize_xml( arg: &Sequence, parameters: SerializationParameters, xot: &mut Xot, ) -> Result { let node = arg.normalize(¶meters.item_separator, xot)?; + namespace_fixup(xot, node)?; let indentation = xot_indentation(¶meters, xot); let cdata_section_elements = xot_names(¶meters.cdata_section_elements, xot); let declaration = if !parameters.omit_xml_declaration { diff --git a/xee-xslt-ast/src/ast_core.rs b/xee-xslt-ast/src/ast_core.rs index a2056c9af..67abb554e 100644 --- a/xee-xslt-ast/src/ast_core.rs +++ b/xee-xslt-ast/src/ast_core.rs @@ -112,25 +112,6 @@ pub enum ExcludeResultPrefixes { Prefixes(Vec), } -impl ExcludeResultPrefixes { - // TODO: This combine isn't good enough; it should take existing prefixes - // into account, which we do have on context - pub(crate) fn combine(&self, other: ExcludeResultPrefixes) -> Self { - match (self, other) { - (ExcludeResultPrefixes::All, _) => ExcludeResultPrefixes::All, - (_, ExcludeResultPrefixes::All) => ExcludeResultPrefixes::All, - ( - ExcludeResultPrefixes::Prefixes(prefixes), - ExcludeResultPrefixes::Prefixes(other_prefixes), - ) => { - let mut prefixes = prefixes.clone(); - prefixes.extend(other_prefixes); - ExcludeResultPrefixes::Prefixes(prefixes) - } - } - } -} - #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub enum ExcludeResultPrefix { @@ -1827,6 +1808,8 @@ pub type SequenceConstructor = Vec; #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct ElementNode { pub name: Name, + /// prefix/namespace pairs this element declares in the result + pub namespaces: Vec<(String, String)>, pub attributes: Vec<(Name, ValueTemplate)>, pub sequence_constructor: SequenceConstructor, pub span: Span, diff --git a/xee-xslt-ast/src/attributes.rs b/xee-xslt-ast/src/attributes.rs index d345ecc11..dc1c284f1 100644 --- a/xee-xslt-ast/src/attributes.rs +++ b/xee-xslt-ast/src/attributes.rs @@ -31,11 +31,13 @@ impl<'a> Attributes<'a> { pub(crate) fn with_standard(self) -> Result { // create a new content has a context including standard attributes - let content = self.content.with_context( - self.content - .context - .with_standard(self.content.xot_namespaces(), self.standard()?), - ); + let content = self + .content + .with_context(self.content.context.with_standard( + self.content.state, + self.content.xot_namespaces(), + self.standard()?, + )); // we now create a new attributes object that has the new content Ok(Self { content, ..self }) } diff --git a/xee-xslt-ast/src/context.rs b/xee-xslt-ast/src/context.rs index fdbcb0655..154b8a3e4 100644 --- a/xee-xslt-ast/src/context.rs +++ b/xee-xslt-ast/src/context.rs @@ -22,9 +22,13 @@ pub(crate) struct Context { pub(crate) expand_text: bool, version: Decimal, xpath_default_namespace: ast::Uri, - // cumulative - exclude_result_prefixes: ast::ExcludeResultPrefixes, + // cumulative; the namespaces designated by exclude-result-prefixes and + // extension-element-prefixes, resolved against the in-scope bindings of + // the element bearing the attribute + excluded_namespaces: HashSet, extension_element_prefixes: Vec, + // the in-scope prefixes of the nearest enclosing literal result element + literal_result_element_prefixes: xot::Prefixes, } impl Context { @@ -46,8 +50,9 @@ impl Context { expand_text: false, version: Decimal::from_str("3.0").unwrap(), xpath_default_namespace: "".to_string(), - exclude_result_prefixes: ast::ExcludeResultPrefixes::Prefixes(vec![]), + excluded_namespaces: HashSet::new(), extension_element_prefixes: vec![], + literal_result_element_prefixes: xot::Prefixes::new(), } } @@ -96,6 +101,7 @@ impl Context { pub(crate) fn with_standard( &self, + state: &State, namespaces: xot::Namespaces, standard: ast::Standard, ) -> Self { @@ -137,13 +143,42 @@ impl Context { } else { self.xpath_default_namespace.clone() }; - let exclude_result_prefixes = - if let Some(exclude_result_prefixes) = standard.exclude_result_prefixes { - self.exclude_result_prefixes - .combine(exclude_result_prefixes) - } else { - self.exclude_result_prefixes.clone() - }; + // exclude-result-prefixes and extension-element-prefixes designate + // namespaces by resolving each prefix against the bindings in scope + // at the element bearing the attribute + let mut excluded_namespaces = self.excluded_namespaces.clone(); + let exclude_prefix = |wanted: &str, excluded: &mut HashSet| { + // TODO: a prefix without a binding is a static error (XTSE0808) + for (prefix_id, namespace_id) in &expanded_prefixes { + if state.xot.prefix_str(*prefix_id) == wanted { + excluded.insert(*namespace_id); + } + } + }; + if let Some(exclude_result_prefixes) = standard.exclude_result_prefixes { + match exclude_result_prefixes { + ast::ExcludeResultPrefixes::All => { + excluded_namespaces.extend(expanded_prefixes.values().copied()); + } + ast::ExcludeResultPrefixes::Prefixes(prefixes) => { + for prefix in prefixes { + match prefix { + ast::ExcludeResultPrefix::Default => { + exclude_prefix("", &mut excluded_namespaces) + } + ast::ExcludeResultPrefix::Prefix(prefix) => { + exclude_prefix(&prefix, &mut excluded_namespaces) + } + } + } + } + } + } + if let Some(extension_element_prefixes) = &standard.extension_element_prefixes { + for prefix in extension_element_prefixes { + exclude_prefix(prefix, &mut excluded_namespaces); + } + } let extension_element_prefixes = if let Some(extension_element_prefixes) = standard.extension_element_prefixes { // TODO for now just add all prefixes. This isn't right. @@ -164,12 +199,62 @@ impl Context { expand_text, version, xpath_default_namespace, - exclude_result_prefixes, + excluded_namespaces, extension_element_prefixes, ..self.clone() } } + /// A context for the children of a literal result element, which + /// remembers the in-scope prefixes the element declares in the result. + pub(crate) fn with_literal_result_element(&self) -> Self { + Self { + literal_result_element_prefixes: self.prefixes.clone(), + ..self.clone() + } + } + + /// A context for content that constructs a separate tree (such as + /// xsl:variable): its elements cannot rely on an enclosing literal + /// result element to declare their namespaces. + pub(crate) fn without_literal_result_element(&self) -> Self { + Self { + literal_result_element_prefixes: xot::Prefixes::new(), + ..self.clone() + } + } + + /// The namespaces a literal result element declares in the result: its + /// in-scope namespaces, except the XSLT namespace, excluded namespaces + /// (exclude-result-prefixes and extension element prefixes), and + /// namespaces already declared in the result by the nearest enclosing + /// literal result element. See XSLT 3.0 section 11.1.3. + pub(crate) fn literal_result_element_namespaces(&self, state: &State) -> Vec<(String, String)> { + let mut namespaces = Vec::new(); + for (prefix_id, namespace_id) in &self.prefixes { + if self.literal_result_element_prefixes.get(prefix_id) == Some(namespace_id) { + continue; + } + if *namespace_id == state.names.xsl_ns + || self.excluded_namespaces.contains(namespace_id) + { + continue; + } + let namespace = state.xot.namespace_str(*namespace_id); + // an xmlns="" undeclaration is not a namespace + if namespace.is_empty() { + continue; + } + namespaces.push(( + state.xot.prefix_str(*prefix_id).to_string(), + namespace.to_string(), + )); + } + // sort for a stable declaration order + namespaces.sort(); + namespaces + } + pub(crate) fn namespaces<'a>(&'a self, state: &'a State) -> Namespaces { let mut namespaces = HashMap::new(); for (prefix, ns) in &self.prefixes { diff --git a/xee-xslt-ast/src/instruction.rs b/xee-xslt-ast/src/instruction.rs index 7db34b7c9..1b785b06f 100644 --- a/xee-xslt-ast/src/instruction.rs +++ b/xee-xslt-ast/src/instruction.rs @@ -138,8 +138,14 @@ impl InstructionParser for ast::ElementNode { .state .xot .name_ref(attributes.element.name(), content.node)?; + let namespaces = content + .context + .literal_result_element_namespaces(content.state); + // children only declare namespaces this element doesn't already + let content = content.with_context(content.context.with_literal_result_element()); Ok(ast::ElementNode { name: name.to_owned(), + namespaces, attributes: element_attributes, span: content.span()?, sequence_constructor: content.sequence_constructor()?, @@ -1092,7 +1098,12 @@ impl InstructionParser for ast::Message { span: content.span()?, - sequence_constructor: content.sequence_constructor()?, + // this content constructs a separate tree; its elements + // cannot rely on an enclosing literal result element for + // their namespace declarations + sequence_constructor: content + .with_context(content.context.without_literal_result_element()) + .sequence_constructor()?, }) } } @@ -1411,7 +1422,12 @@ impl InstructionParser for ast::Param { span: content.span()?, - sequence_constructor: content.sequence_constructor()?, + // this content constructs a separate tree; its elements + // cannot rely on an enclosing literal result element for + // their namespace declarations + sequence_constructor: content + .with_context(content.context.without_literal_result_element()) + .sequence_constructor()?, }) } } @@ -1684,7 +1700,12 @@ impl InstructionParser for ast::Variable { span: content.span()?, - sequence_constructor: content.sequence_constructor()?, + // this content constructs a separate tree; its elements + // cannot rely on an enclosing literal result element for + // their namespace declarations + sequence_constructor: content + .with_context(content.context.without_literal_result_element()) + .sequence_constructor()?, }) } @@ -1736,7 +1757,12 @@ impl InstructionParser for ast::WithParam { span: content.span()?, - sequence_constructor: content.sequence_constructor()?, + // this content constructs a separate tree; its elements + // cannot rely on an enclosing literal result element for + // their namespace declarations + sequence_constructor: content + .with_context(content.context.without_literal_result_element()) + .sequence_constructor()?, }) } } diff --git a/xee-xslt-ast/tests/snapshots/snapshot_tests__attributes_on_literal_element.snap b/xee-xslt-ast/tests/snapshots/snapshot_tests__attributes_on_literal_element.snap index 9f69d0dce..bdb802221 100644 --- a/xee-xslt-ast/tests/snapshots/snapshot_tests__attributes_on_literal_element.snap +++ b/xee-xslt-ast/tests/snapshots/snapshot_tests__attributes_on_literal_element.snap @@ -1,5 +1,6 @@ --- -source: xee-xslt-ast/src/instruction.rs +source: xee-xslt-ast/tests/snapshot_tests.rs +assertion_line: 258 expression: "parse_sequence_constructor_item(r#\"

\"#)" --- Ok(Content(Element(ElementNode( @@ -8,6 +9,7 @@ Ok(Content(Element(ElementNode( namespace_str: "", prefix_str: "", ), + namespaces: [], attributes: [ (OwnedName( local_name_str: "foo", diff --git a/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element.snap b/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element.snap index 32bb5ac98..6a8fbe7cd 100644 --- a/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element.snap +++ b/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element.snap @@ -1,5 +1,6 @@ --- -source: xee-xslt-ast/src/instruction.rs +source: xee-xslt-ast/tests/snapshot_tests.rs +assertion_line: 98 expression: "parse_sequence_constructor_item(r#\"\"#)" --- Ok(Content(Element(ElementNode( @@ -8,6 +9,7 @@ Ok(Content(Element(ElementNode( namespace_str: "", prefix_str: "", ), + namespaces: [], attributes: [], sequence_constructor: [], span: Span( diff --git a/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element_with_standard_attribute.snap b/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element_with_standard_attribute.snap index 69299e37a..3b27b417c 100644 --- a/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element_with_standard_attribute.snap +++ b/xee-xslt-ast/tests/snapshots/snapshot_tests__literal_result_element_with_standard_attribute.snap @@ -1,5 +1,6 @@ --- -source: xee-xslt-ast/src/instruction.rs +source: xee-xslt-ast/tests/snapshot_tests.rs +assertion_line: 103 expression: "parse_sequence_constructor_item(r#\"\"#)" --- Ok(Content(Element(ElementNode( @@ -8,6 +9,7 @@ Ok(Content(Element(ElementNode( namespace_str: "", prefix_str: "", ), + namespaces: [], attributes: [], sequence_constructor: [], span: Span( diff --git a/xee-xslt-ast/tests/snapshots/snapshot_tests__nested_literal_elements.snap b/xee-xslt-ast/tests/snapshots/snapshot_tests__nested_literal_elements.snap index 1d5aa5e19..46b70130d 100644 --- a/xee-xslt-ast/tests/snapshots/snapshot_tests__nested_literal_elements.snap +++ b/xee-xslt-ast/tests/snapshots/snapshot_tests__nested_literal_elements.snap @@ -1,5 +1,6 @@ --- -source: xee-xslt-ast/src/instruction.rs +source: xee-xslt-ast/tests/snapshot_tests.rs +assertion_line: 244 expression: "parse_sequence_constructor_item(r#\"

\"#)" --- Ok(Instruction(If(If( @@ -30,6 +31,7 @@ Ok(Instruction(If(If( namespace_str: "", prefix_str: "", ), + namespaces: [], attributes: [], sequence_constructor: [ Content(Element(ElementNode( @@ -38,6 +40,7 @@ Ok(Instruction(If(If( namespace_str: "", prefix_str: "", ), + namespaces: [], attributes: [], sequence_constructor: [], span: Span( diff --git a/xee-xslt-ast/tests/snapshots/snapshot_tests__sequence_constructor_nested_in_literal_element.snap b/xee-xslt-ast/tests/snapshots/snapshot_tests__sequence_constructor_nested_in_literal_element.snap index ca1318c41..43996c06e 100644 --- a/xee-xslt-ast/tests/snapshots/snapshot_tests__sequence_constructor_nested_in_literal_element.snap +++ b/xee-xslt-ast/tests/snapshots/snapshot_tests__sequence_constructor_nested_in_literal_element.snap @@ -1,5 +1,6 @@ --- -source: xee-xslt-ast/src/instruction.rs +source: xee-xslt-ast/tests/snapshot_tests.rs +assertion_line: 251 expression: "parse_sequence_constructor_item(r#\"

foo

\"#)" --- Ok(Instruction(If(If( @@ -30,6 +31,7 @@ Ok(Instruction(If(If( namespace_str: "", prefix_str: "", ), + namespaces: [], attributes: [], sequence_constructor: [ Instruction(If(If( diff --git a/xee-xslt-compiler/src/ast_ir.rs b/xee-xslt-compiler/src/ast_ir.rs index a58500360..1020cf04d 100644 --- a/xee-xslt-compiler/src/ast_ir.rs +++ b/xee-xslt-compiler/src/ast_ir.rs @@ -483,6 +483,22 @@ impl<'a> IrConverter<'a> { let (element_atom, mut bindings) = bindings .bind_expr_no_span(&mut self.variables, name_expr) .atom_bindings(); + for (prefix, namespace) in &element_node.namespaces { + let namespace_expr = ir::Expr::XmlNamespace(ir::XmlNamespace { + prefix: self.constant_string(prefix), + namespace: self.constant_string(namespace), + }); + let (namespace_atom, namespace_bindings) = Bindings::empty() + .bind_expr_no_span(&mut self.variables, namespace_expr) + .atom_bindings(); + let append_expr = ir::Expr::XmlAppend(ir::XmlAppend { + parent: element_atom.clone(), + child: namespace_atom, + }); + let append_bindings = + namespace_bindings.bind_expr_no_span(&mut self.variables, append_expr); + bindings = bindings.concat(append_bindings); + } for (name, value) in &element_node.attributes { let (value_atom, value_bindings) = self.attribute_value_template(value)?.atom_bindings(); @@ -714,8 +730,12 @@ impl<'a> IrConverter<'a> { } fn empty_string(&self) -> ir::AtomS { + self.constant_string("") + } + + fn constant_string(&self, value: &str) -> ir::AtomS { Spanned::new( - ir::Atom::Const(ir::Const::String("".to_string())), + ir::Atom::Const(ir::Const::String(value.to_string())), (0..0).into(), ) } @@ -943,7 +963,10 @@ impl<'a> IrConverter<'a> { ir::Atom::Const(ir::Const::String(name.local_name().to_string())), (0..0).into(), ); - let namespace = self.empty_string(); + let namespace = Spanned::new( + ir::Atom::Const(ir::Const::String(name.namespace().to_string())), + (0..0).into(), + ); let binding = self .variables diff --git a/xee-xslt-compiler/tests/test_xslt.rs b/xee-xslt-compiler/tests/test_xslt.rs index 946324eba..3267ac367 100644 --- a/xee-xslt-compiler/tests/test_xslt.rs +++ b/xee-xslt-compiler/tests/test_xslt.rs @@ -1230,3 +1230,231 @@ fn test_basic_iterate_params() { "124" ); } + +// https://github.com/Paligo/xee/issues/139 +#[test] +fn test_literal_result_element_default_namespace() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!( + xml(&xot, output), + r#""# + ); +} + +#[test] +fn test_literal_result_element_prefixed_namespace() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!( + xml(&xot, output), + r#""# + ); +} + +#[test] +fn test_literal_result_element_stylesheet_default_namespace() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!( + xml(&xot, output), + r#""# + ); +} + +#[test] +fn test_literal_result_element_copies_unused_namespace() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), r#""#); +} + +#[test] +fn test_literal_result_element_exclude_result_prefixes() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), ""); +} + +#[test] +fn test_xsl_element_with_namespace_serializes() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + let serialized = output + .serialize( + xee_interpreter::sequence::SerializationParameters::new(), + &mut xot, + ) + .unwrap(); + assert!( + serialized.contains("http://www.w3.org/1999/xhtml"), + "namespace must survive serialization, got: {}", + serialized + ); +} + +#[test] +fn test_literal_result_element_namespaced_attribute() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), r#""#); +} + +#[test] +fn test_literal_result_element_in_variable_keeps_namespaces() { + // xsl:variable content builds a separate tree, so its elements must + // carry their own in-scope namespace copies + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!( + xml(&xot, output), + r#""# + ); +} + +#[test] +fn test_exclude_result_prefixes_resolved_where_specified() { + // exclude-result-prefixes designates the namespace bound to the prefix + // at the element bearing the attribute, not wherever the prefix is + // later rebound + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), r#""#); +} + +#[test] +fn test_exclude_result_prefixes_all_keeps_local_namespaces() { + // #all designates the namespaces in scope at the bearing element; a + // namespace declared further down on the literal result element itself + // is not excluded + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r##" + + +"##, + ) + .unwrap(); + assert_eq!(xml(&xot, output), r#""#); +} + +#[test] +fn test_serialize_fixes_prefixes_on_all_top_level_elements() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + let serialized = output + .serialize( + xee_interpreter::sequence::SerializationParameters::new(), + &mut xot, + ) + .unwrap(); + assert!( + serialized.contains("http://one") && serialized.contains("http://two"), + "both top-level elements need their namespaces bound, got: {}", + serialized + ); +} + +#[test] +fn test_serialize_html_method_applies_namespace_fixup() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + +"#, + ) + .unwrap(); + let mut parameters = xee_interpreter::sequence::SerializationParameters::new(); + parameters.method = xee_interpreter::sequence::QNameOrString::String("html".to_string()); + let serialized = output.serialize(parameters, &mut xot); + assert!( + serialized.is_ok(), + "html serialization must not fail on a constructed namespace: {:?}", + serialized + ); +}