From 07d3ddfa97e86ffef22e282afb64da347307b460 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sat, 13 Jun 2026 18:25:08 +0200 Subject: [PATCH] fix(regex): use the XSD definition of \w and \W \w was the Perl word class (alphabetic, decimal, underscore). XSD 1.0 Datatypes F.1 defines \w as [#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}], so symbols and marks are word characters and the connector `_` (Pc) is not; \W is its complement and had wrongly matched symbols. \w is now computed as "not punctuation, separator, or other" via the category matcher. XSTS: invalid instances accepted 231 to 210. The internal regex fixtures that assumed `_` is a word character encoded Perl semantics and were corrected to the XSD definition (symbols are word chars, `_` is not), confirmed against the XSTS reV cases. Refs #147, #129. --- CHANGELOG.md | 1 + Sources/Regex/RegexSyntax.swift | 5 ++++- Tests/ConformanceRegexTests.swift | 4 +++- Tests/RegexTests.swift | 7 ++++++- Tests/XSTSSuiteTests.swift | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6229b6f..9f81ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The regex `\w` and `\W` escapes now follow the XSD definition (#147). `\w` was the Perl class (alphabetic, decimal, underscore); XSD defines it as `[#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}]`, so symbols and marks are word characters and the connector `_` (Pc) is not. `\W` correspondingly matched symbols it should not. XSTS: invalid instances accepted 231 to 210. (Internal regex fixtures that assumed `_` was a word character were corrected.) - `block="substitution"` on a substitution-group head is now enforced (#147). The substitution filter only dropped members whose type derived from the head's type by a blocked method (`extension`/`restriction`); the `substitution` keyword, which forbids substitution-group substitution outright regardless of the member's type, was ignored, so a same-type member still substituted under a substitution block. XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74. - Element `default` and `fixed` values are honored on empty elements (#147). An empty element with a value constraint takes that value, so the empty content is valid even when the type would reject the empty string; the constraint value (not the empty string) is what is validated against the type, including any `xsi:type` override. A restriction's `enumeration` now also replaces the inherited enumeration rather than unioning with it (the new set is the value space; `pattern` still accumulates and is ANDed), which a value-constraint case under `xsi:type` exposed. XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to 238. - Element-level `block` is now enforced against `xsi:type` substitution (#147). The `block` on an element declaration (distinct from `block` on its type) was parsed but never threaded to the instance validator, so a document substituting a blocked-derivation type via `xsi:type` was wrongly accepted. The element block is now carried through to `blockedSubstitutionError`, which also resolves the element-ref type indirection (`element:foo` to its concrete type) so the check fires for referenced children, not only the root. XSTS: invalid instances accepted 261 to 240. diff --git a/Sources/Regex/RegexSyntax.swift b/Sources/Regex/RegexSyntax.swift index 2baa0c5..13e5f3a 100644 --- a/Sources/Regex/RegexSyntax.swift +++ b/Sources/Regex/RegexSyntax.swift @@ -35,7 +35,10 @@ extension PureXML.Regex { switch self { case .digit: scalar.properties.numericType == .decimal case .space: scalar == " " || scalar == "\t" || scalar == "\n" || scalar == "\r" - case .word: scalar.properties.isAlphabetic || scalar.properties.numericType == .decimal || scalar == "_" + // XSD \w is [#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}]: everything except + // punctuation, separators, and other. So symbols and marks are word + // characters, and `_` (Pc) is not, unlike the Perl definition. + case .word: !(CategoryMatcher.matches("P", scalar) || CategoryMatcher.matches("Z", scalar) || CategoryMatcher.matches("C", scalar)) case .nameStart: PureXML.Parsing.XMLCharacter.isNameStart(scalar) case .nameChar: PureXML.Parsing.XMLCharacter.isNameChar(scalar) case .anyButLineBreak: scalar != "\n" && scalar != "\r" diff --git a/Tests/ConformanceRegexTests.swift b/Tests/ConformanceRegexTests.swift index eada353..2235a1b 100644 --- a/Tests/ConformanceRegexTests.swift +++ b/Tests/ConformanceRegexTests.swift @@ -21,7 +21,9 @@ struct ConformanceRegexTests { // Character-class escapes. PatternSpec(name: "digits-ok", pattern: "\\d{3}", value: "123", valid: true), PatternSpec(name: "digits-fail", pattern: "\\d{3}", value: "12a", valid: false), - PatternSpec(name: "word-chars", pattern: "\\w+", value: "a_1", valid: true), + // XSD \w excludes punctuation, so the connector `_` (Pc) is not a word char. + PatternSpec(name: "word-chars", pattern: "\\w+", value: "abc1", valid: true), + PatternSpec(name: "word-chars-no-underscore", pattern: "\\w", value: "_", valid: false), PatternSpec(name: "whitespace-escape", pattern: "a\\sb", value: "a b", valid: true), // Unicode categories and blocks. PatternSpec(name: "category-letters", pattern: "\\p{L}+", value: "abcé", valid: true), diff --git a/Tests/RegexTests.swift b/Tests/RegexTests.swift index e6065a8..4248dbe 100644 --- a/Tests/RegexTests.swift +++ b/Tests/RegexTests.swift @@ -74,7 +74,12 @@ struct RegexTests { func test_classEscapes() throws { #expect(try matches("\\d{4}", "2026")) #expect(try !matches("\\d{4}", "20x6")) - #expect(try matches("\\w+", "a_1")) + #expect(try matches("\\w+", "abc1")) + // XSD \w is the complement of \p{P}\p{Z}\p{C}: symbols and marks are word + // characters, while the connector `_` (Pc) and whitespace are not. + #expect(try matches("\\w", "+")) + #expect(try !matches("\\w", "_")) + #expect(try !matches("\\w", " ")) #expect(try matches("a\\sb", "a b")) #expect(try matches("\\D+", "abc")) } diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index d9d1ede..c84bc74 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -31,7 +31,7 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 74 private let knownSchemaInvalidAccepted = 2467 private let knownInstanceValidRejected = 369 - private let knownInstanceInvalidAccepted = 231 + private let knownInstanceInvalidAccepted = 210 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws {