fix(schema): preserve significant whitespace in simple-content values
textContent trimmed leading/trailing whitespace unconditionally before the type's whiteSpace facet ran, so an xs:string (whiteSpace="preserve") value that was whitespace-only or had significant edges was emptied or shortened: a tab value did not match a \t pattern, a length facet undercounted, and so on. Simple-content validation now reads the raw character content (rawTextContent) and lets the facet normalize it (collapse types still trim/collapse through process). The element-only and empty "no significant text" check keeps using the trimmed form, so indentation whitespace between child elements is still ignored. XSTS: valid instances rejected 288 to 248. Baseline updated; regression tests cover a preserved tab matching \t, a length facet counting whitespace, and element-only content still ignoring indentation. Refs #147, #129.
This commit is contained in:
parent
092e9a488d
commit
3f652a1b23
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Simple-content values preserve significant whitespace per the type's `whiteSpace` facet (#147). `textContent` trimmed leading/trailing whitespace unconditionally before the facet ran, so an `xs:string` (`whiteSpace="preserve"`) value that was whitespace-only or had significant edges (e.g. matching a `\t` pattern, or counted by a `length` facet) was wrongly emptied or shortened. Simple-content validation now reads the raw character content and lets the facet normalize it; the element-only/empty "no significant text" check still ignores indentation whitespace. XSTS: valid instances rejected 288 to 248.
|
||||
|
||||
### Added
|
||||
|
||||
- `Schema.Document.validate(_:schemaLoader:)` honors an instance's `xsi:schemaLocation` and `xsi:noNamespaceSchemaLocation` (#147). Each referenced schema document is loaded through the supplied loader, compiled, and its global declarations merged in (the primary schema's own declarations win on conflict), so a strict or lax wildcard can resolve an element declared in another document. The plain `validate(_:)` is unchanged. XSTS: valid instances rejected 308 to 288 (the particle clusters whose wildcard-matched elements live in a separate, instance-referenced schema).
|
||||
|
||||
@ -154,7 +154,7 @@ public extension PureXML.Schema {
|
||||
rejectText(element, at: path, into: &errors)
|
||||
case let .simpleContent(type):
|
||||
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
|
||||
if let error = type.validate(Self.textContent(element)) {
|
||||
if let error = type.validate(Self.rawTextContent(element)) {
|
||||
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
||||
}
|
||||
case let .elementOnly(particle):
|
||||
@ -271,7 +271,7 @@ public extension PureXML.Schema {
|
||||
// An empty element takes its `default`/`fixed` value; that value
|
||||
// (not the empty string) is what must be valid against the type,
|
||||
// including any xsi:type override already resolved into `simple`.
|
||||
let text = Self.textContent(child)
|
||||
let text = Self.rawTextContent(child)
|
||||
let effective = text.isEmpty ? (elementConstraints[child.name.localName]?.value ?? text) : text
|
||||
if let error = simple.validate(effective) {
|
||||
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
|
||||
|
||||
@ -86,14 +86,24 @@ extension PureXML.Schema.ComplexValidator {
|
||||
lhs.namespaceURI == rhs.namespaceURI && lhs.localName == rhs.localName
|
||||
}
|
||||
|
||||
/// The element's character content, trimmed: used to decide whether an
|
||||
/// element-only or empty type wrongly carries significant text (indentation
|
||||
/// whitespace between child elements is not significant).
|
||||
static func textContent(_ element: PureXML.Model.Element) -> String {
|
||||
let text = element.children.reduce(into: "") { result, child in
|
||||
rawTextContent(element).trimmingXMLWhitespace()
|
||||
}
|
||||
|
||||
/// The element's character content verbatim. Simple-content validation uses
|
||||
/// this so the type's own `whiteSpace` facet decides normalization: a
|
||||
/// `preserve` type (xs:string) keeps leading/trailing whitespace, while a
|
||||
/// `collapse` type still trims and collapses through `process`.
|
||||
static func rawTextContent(_ element: PureXML.Model.Element) -> String {
|
||||
element.children.reduce(into: "") { result, child in
|
||||
switch child {
|
||||
case let .text(value), let .cdata(value): result += value
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return text.trimmingXMLWhitespace()
|
||||
}
|
||||
|
||||
static func isNamespaceDeclaration(_ attribute: PureXML.Model.Attribute) -> Bool {
|
||||
|
||||
49
Tests/SchemaStringWhitespaceTests.swift
Normal file
49
Tests/SchemaStringWhitespaceTests.swift
Normal file
@ -0,0 +1,49 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// xs:string has whiteSpace="preserve", so a simple-content value keeps its
|
||||
/// leading/trailing and whitespace-only content; validation no longer trims it
|
||||
/// before the facet runs (#147, XSTS reI set). Element-only content still
|
||||
/// ignores indentation whitespace.
|
||||
@Suite("String content whitespace is preserved")
|
||||
struct SchemaStringWhitespaceTests {
|
||||
private func patternSchema(_ pattern: String) throws -> PureXML.Schema.Document {
|
||||
try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="v">
|
||||
<xs:simpleType><xs:restriction base="xs:string"><xs:pattern value="\(pattern)"/></xs:restriction></xs:simpleType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("A tab-only value matches the \\t pattern (whitespace preserved)")
|
||||
func test_tabPreserved() throws {
|
||||
#expect(try patternSchema(#"\t"#).validate("<v>	</v>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A length facet on string counts preserved whitespace")
|
||||
func test_lengthCountsWhitespace() throws {
|
||||
let doc = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="v">
|
||||
<xs:simpleType><xs:restriction base="xs:string"><xs:length value="3"/></xs:restriction></xs:simpleType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
#expect(try doc.validate("<v> a </v>").isEmpty) // space,a,space = 3 chars
|
||||
#expect(try !doc.validate("<v>a</v>").isEmpty) // 1 char
|
||||
}
|
||||
|
||||
@Test("Element-only content still ignores indentation whitespace")
|
||||
func test_elementOnlyIgnoresWhitespace() throws {
|
||||
let doc = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="r">
|
||||
<xs:complexType><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
#expect(try doc.validate("<r>\n <a>x</a>\n</r>").isEmpty)
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ struct XSTSSuiteTests {
|
||||
/// are written to /tmp/xsts-failures.txt for the burn-down.
|
||||
private let knownSchemaValidRejected = 72
|
||||
private let knownSchemaInvalidAccepted = 2461
|
||||
private let knownInstanceValidRejected = 288
|
||||
private let knownInstanceValidRejected = 248
|
||||
private let knownInstanceInvalidAccepted = 165
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user