Fix normalize-space() collapsing NBSP (U+00A0) into a regular space - #2
Merged
leonelsanchesdasilva merged 3 commits intoAug 12, 2026
Merged
Conversation
normalize-space() used JavaScript's native .trim()/\s, which (unlike the XPath/XQuery F&O spec) treats U+00A0 as whitespace. This silently corrupted any NBSP in the source text into a plain space wherever normalize-space() was called - including in real-world stylesheets like SAT's CFDI "cadena original" templates, which wrap nearly every field in normalize-space() and can legitimately contain NBSP in text. Per the spec, normalize-space() must only treat #x9 (tab), #xA (LF), #xD (CR) and #x20 (space) as whitespace. This fixes both call sites that implement it (the BUILT_IN_FUNCTIONS map used for XPath 2.0/3.0 function references, and the normalizeSpace() method used by the XPath 1.0-compatible evaluate() switch) to use an XML-whitespace-only pattern instead of .trim()/\s, and adds regression tests covering NBSP preservation with and without surrounding XML whitespace.
normalize-space() used JavaScript's native .trim()/\s, which (unlike the XPath/XQuery F&O spec) treats U+00A0 as whitespace. This silently corrupted any NBSP in the source text into a plain space wherever normalize-space() was called. Per the spec, normalize-space() must only treat #x9 (tab), #xA (LF), #xD (CR) and #x20 (space) as whitespace. Fixes both call sites that implement it (the BUILT_IN_FUNCTIONS map used for XPath 2.0/3.0 function references, and the normalizeSpace() method used by the XPath 1.0-compatible evaluate() switch) to use an XML-whitespace-only pattern instead of .trim()/\s.
Covers: NBSP inside a string with no surrounding XML whitespace, NBSP runs with surrounding XML whitespace to trim, and NBSP in node text when normalize-space() is called with no argument.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fn:normalize-space()collapses non-breaking spaces (U+00A0) into regular spaces (U+0020), and can also merge/strip them entirely when adjacent to other whitespace. Per the XPath/XQuery Functions & Operators spec,normalize-spaceshould only treat#x9(tab),#xA(LF),#xD(CR), and#x20(space) as whitespace —U+00A0is a distinct character and must be left untouched. This makes the output of any stylesheet that runs field values throughnormalize-space()(a very common pattern) silently lossy for any text containing NBSP.This isn't a hypothetical: SAT (Mexico's tax authority) official CFDI "cadena original" stylesheets wrap nearly every field in a
normalize-space()call, and real invoice text (addresses, product descriptions) legitimately contains NBSP. Since the cadena original is the exact byte string that gets digitally signed and later verified, this bug produces a string that doesn't match what a spec-compliant processor (e.g. Saxon) produces from the same input — i.e. this is a correctness bug, not a cosmetic one. (This is consumed downstream via thexslt-processorpackage, which vendors this repo as a submodule atsrc/xpath/lib.)Root cause
Both implementations of
normalize-spaceused JavaScript's native.trim()and the\sregex character class. Per ECMA-262, both of these treatU+00A0as whitespace — that's a JS-specific definition, not the XML/XPath one.src/expressions/function-call-expression.ts— theBUILT_IN_FUNCTIONS['normalize-space']entry (used for XPath 2.0/3.0 function references):src/expressions/function-call-expression.ts— thenormalizeSpace()private method (used by the XPath 1.0-compatibleevaluate()switch statement):Fix
Both are changed to use an XML-whitespace-only pattern (
[\t\n\r ]) instead of.trim()/\s, matching the spec exactly:This preserves all existing behavior for real XML whitespace (multiple runs still collapse to a single space, leading/trailing still trimmed) while leaving NBSP (and any other non-XML-whitespace character) completely untouched.
Verification
Manually verified against the built package (
xslt-processor@5.0.11, which vendors this repo) with a minimal repro:46 6f 6f 20 42 61 72(NBSP silently becameU+0020)46 6f 6f a0 42 61 72(NBSP preserved, matching plainxsl:value-ofwith nonormalize-space())Tests
Added three regression tests to
tests/expressions/expressions.test.ts, next to the existingnormalize-spacetests:normalize-space()is called with no argument (using nodetextContent)Happy to adjust the approach (e.g. extracting a shared whitespace-trim helper) if you'd prefer a different structure.
Note: drafted with Claude's assistance (root-cause investigation, patch, and tests); verified manually against
xslt-processor@5.0.11before submitting.