Skip to content

Preserve namespaces of literal result elements in output#155

Open
ajbufort wants to merge 1 commit into
Paligo:mainfrom
ajbufort:literal-result-namespaces
Open

Preserve namespaces of literal result elements in output#155
ajbufort wants to merge 1 commit into
Paligo:mainfrom
ajbufort:literal-result-namespaces

Conversation

@ajbufort

Copy link
Copy Markdown

Fixes #139.

The problem

Literal result elements and their attributes lost their namespaces entirely. The stylesheet from #139 produced <html lang="en-GB"> - no xmlns, and xml:lang degraded to lang. The root cause: xml_name in the XSLT compiler hardcoded an empty namespace when compiling element and attribute names, so every name reached the interpreter namespace-less. Separately, xsl:element with a namespace attribute always failed with XPST0081, because the constructed name did carry a namespace but nothing ever created a prefix binding for it, so serialization failed with MissingPrefix.

The fix, in three connected parts

  1. xml_name passes the namespace through to the XmlName IR instruction instead of the empty string. The interpreter and Xot already handled namespaced names correctly - nothing downstream needed changes.

  2. Literal result elements copy their in-scope namespace bindings to the result (XSLT 3.0 section 11.1.3), so output preserves the prefixes the stylesheet author chose - a default xmlns stays a default xmlns. The parser computes the bindings per element: in-scope namespaces minus the XSLT namespace and excluded namespaces, and minus whatever the nearest statically-enclosing literal result element already declares, so nested elements don't redeclare. Content that constructs a separate tree (xsl:variable, xsl:param, xsl:with-param, xsl:message) resets that suppression, since its elements can't rely on an enclosing element's declarations. The compiler emits the existing XmlNamespace/XmlAppend instructions.

    Along the way, exclude-result-prefixes and extension-element-prefixes now resolve each prefix to a namespace at the element bearing the attribute, as the spec requires (so a later rebinding of the same prefix isn't wrongly excluded, and #all designates only what was in scope where it was written). This replaces the stored prefix lists and ExcludeResultPrefixes::combine - whose own TODO asked for exactly this resolution.

  3. Serialization performs namespace fixup on the normalized document: every top-level element gets create_missing_prefixes, which invents prefixes for namespaces with no binding anywhere (this is what makes xsl:element with namespace work), after explicitly binding the implicit xml prefix so it isn't given an invented one.

With this, the stylesheet from the issue produces:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB"><head><title>Hello World!</title></head><body/></html>

Testing

  • 12 new tests in xee-xslt-compiler/tests/test_xslt.rs: default-namespace LRE, prefixed LRE (prefix preserved), stylesheet-level default namespace, unused namespace copied, xml:lang attribute, xsl:element with namespace surviving serialization, namespaces inside xsl:variable content, exclude-result-prefixes resolved at the bearing element, #all not stripping later local declarations, multi-element sequence fixup, and the html output method (which needs no fixup - the html5 serializer emits no namespace declarations)
  • XSLT conformance: 1098 -> 1169 passing (+71), filters updated via the update/check flow, no regressions; XPath conformance unchanged (20221, including in debug mode as CI runs it)
  • cargo fmt, clippy --all-targets --all-features -- -D warnings, and full cargo test clean (AST snapshots regenerated for the new namespaces field)

Notes and limitations

  • Full inherit-namespaces/copy-namespaces attribute support (tracked in conformance/xslt.md) is not part of this change; this implements the default section 11.1.3 behavior.
  • An unbound prefix in exclude-result-prefixes should be the static error XTSE0808; the parser has no error-code machinery today, so it's a marked TODO rather than a new error architecture in this PR.
  • Trees composed dynamically across templates can serialize with redundant (valid) re-declarations, since each template's outermost literal result elements carry their own copies. Xot's deduplicate_namespaces could clean this up at serialization time, but calling it unconditionally would also change output for parsed documents, so I left it out; happy to add it if you'd prefer.
  • create_missing_prefixes in Xot invents a prefix for the implicit xml namespace, which the serializer then skips declaring - the explicit pre-binding here works around that; happy to file that upstream on Xot if you agree it's a bug.
  • Namespace fixup adds one traversal per top-level element to XML serialization. It could be made conditional (attempt-then-retry on MissingPrefix) if that cost matters to you.

Generated with Claude Code

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 <html> 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 Paligo#139.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Namespaces in XSLT are not preserved in output

1 participant