diff --git a/CHANGELOG.md b/CHANGELOG.md
index e82692d..1ca8d27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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).
diff --git a/Sources/Schema/ComplexValidator.swift b/Sources/Schema/ComplexValidator.swift
index ad0f3a7..16a42f2 100644
--- a/Sources/Schema/ComplexValidator.swift
+++ b/Sources/Schema/ComplexValidator.swift
@@ -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))
diff --git a/Sources/Schema/ComplexValidatorHelpers.swift b/Sources/Schema/ComplexValidatorHelpers.swift
index 1ec1f3a..71e534d 100644
--- a/Sources/Schema/ComplexValidatorHelpers.swift
+++ b/Sources/Schema/ComplexValidatorHelpers.swift
@@ -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 {
diff --git a/Tests/SchemaStringWhitespaceTests.swift b/Tests/SchemaStringWhitespaceTests.swift
new file mode 100644
index 0000000..709c344
--- /dev/null
+++ b/Tests/SchemaStringWhitespaceTests.swift
@@ -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("""
+
+
+
+
+
+ """)
+ }
+
+ @Test("A tab-only value matches the \\t pattern (whitespace preserved)")
+ func test_tabPreserved() throws {
+ #expect(try patternSchema(#"\t"#).validate(" ").isEmpty)
+ }
+
+ @Test("A length facet on string counts preserved whitespace")
+ func test_lengthCountsWhitespace() throws {
+ let doc = try PureXML.Schema.Document("""
+
+
+
+
+
+ """)
+ #expect(try doc.validate(" a ").isEmpty) // space,a,space = 3 chars
+ #expect(try !doc.validate("a").isEmpty) // 1 char
+ }
+
+ @Test("Element-only content still ignores indentation whitespace")
+ func test_elementOnlyIgnoresWhitespace() throws {
+ let doc = try PureXML.Schema.Document("""
+
+
+
+
+
+ """)
+ #expect(try doc.validate("\n x\n").isEmpty)
+ }
+}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index 457addc..2bb5351 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -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")