Skip to content

fn:normalize-space collapses JavaScript whitespace, not XML S, in builtInFunctions_string.ts #687

Description

@maxonfjvipon

fnNormalizeSpace in src/expressions/functions/builtInFunctions_string.ts:390
trims with String.prototype.trim() and collapses with /\s+/g. Both read
JavaScript's whitespace class. XPath 3.1 F&O 5.4.2 defines fn:normalize-space
over the four characters of XML's S production, #x9, #xA, #xD and
#x20, so a no-break space, an em space, a line separator or a BOM is an
ordinary character to this function and must survive it untouched.

const stringValue = arg.first().value.trim();
return sequenceFactory.singleton(
  createAtomicValue(stringValue.replace(/\s+/g, ' '), ValueType.XSSTRING),
);

Asking 3.34.0 for normalize-space("a" || $c || "b"), one character at a time:

character                normalize-space   tokenize#1
NO-BREAK SPACE U+00A0    "a b"             two tokens
LINE SEPARATOR U+2028    "a b"             two tokens
EM SPACE U+2003          "a b"             two tokens
VERTICAL TAB U+000B      "a b"             two tokens
ZWNBSP U+FEFF            "a b"             two tokens
NEL U+0085               unchanged         one token

Only the last row is right. NEL is the tell: JavaScript's \s is the one
whitespace class that excludes U+0085, and so does this implementation, which
fingerprints the class exactly rather than leaving the cause to guesswork.

The second column is fn:tokenize#1, which inherits the defect. The arity-one
form at line 972 is defined as fn:tokenize(fn:normalize-space($input), ' '),
exactly as the specification words it, so it splits on the wider class too and
answers two tokens where XPath says one. Nothing in the suite notices either
way: the normalize-space() block at
test/specs/parsing/functions/functions.string.tests.ts:105 exercises spaces
and tabs alone, which is the subset on which the two classes agree.

The correct class is already spelled twice in this repository, as WHITESPACE
in src/parsing/tokens.ts:3 and as the attribute-value normalization at
src/parsing/prscParser.ts:833, so the fix is to use it here as well: collapse
with /[\x20\x09\x0D\x0A]+/g and cut the ends with the anchored form of the
same class, instead of trim() and /\s+/g. One more case in that describe
block, holding a U+00A0 between two letters, keeps it from coming back.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions