Compare commits

..

No commits in common. "main" and "v0.4.8" have entirely different histories.
main ... v0.4.8

351 changed files with 2554 additions and 9049 deletions

View File

@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions and ideas
url: https://git.aleahim.com/NodetteHQ/PureXML/issues
about: Ask questions, float ideas, or discuss design in an issue.
url: https://codeberg.org/Mihaela/PureXML/discussions
about: Ask questions, float ideas, or discuss design in Discussions.
- name: Security issue
url: https://git.aleahim.com/NodetteHQ/PureXML/src/branch/main/SECURITY.md
url: https://codeberg.org/Mihaela/PureXML/blob/main/SECURITY.md
about: Do not file security issues publicly. Follow the security policy instead.

View File

@ -1 +1 @@
2.14.0
1.1.2

View File

@ -7,179 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- The XSLT `system-property('xsl:vendor-url')` now returns the project's
address on git.aleahim.com, where the repository moved from Codeberg.
### Fixed
- An element restricting a NON-repeating base choice ({max occurs} = 1) is now
checked by the literal RecurseAsIfGroup pairing (XSD 1.0 section 3.9.6): the
derived element, with its own occurrence range, must be a valid restriction
of some branch. The effective-total-range approximation is kept only for a
repeating base choice, where the particlesZ001 ambiguity actually lives
(occurrence counts accumulated across the choice's own repetitions, which the
literal rule cannot express); at {max occurs} = 1 no such accumulation exists
and the spec text governs. Closes XSTS particlesHa161 (an optional derived
element is not a pointwise restriction of a required branch) and
particlesM034 (a derived `d1+` cannot map into a branch `(d1*, d2+)` whose
required `d2` is unmapped and not emptiable; the settled-valid twin
particlesM035, `d2+` over the same branch, still passes), taking
invalid-schemas-accepted 6 -> 4; stricter than libxml2, which implements
neither edge. Separately, particlesIg004 is reclassified as a spec-divergent
schema test (4 -> 3): section 3.3.2 maps `block="#all"` and the explicit
`substitution extension restriction` list to the same {disallowed
substitutions} set, so NameAndTypeOK clause 5's superset requirement holds
and the `invalid` expectation has no spec path (jointly unsatisfiable with
the settled Ig001/002/003 and Ig005 expectations). The remaining three
schema deviations (Ik025, Ik027, schN12) are documented policy divergences
in `docs/xsts-deviations.md`, including why src-redefine 6.2.2 cannot be
enforced uniformly (mgO013's settled-valid schema violates it). Valid
buckets are unchanged at zero.
- A complex-type restriction that swaps an element reference for one of its
substitution-group members is now rejected (NameAndTypeOK, XSD 1.0 section
3.9.6 Elt:Elt clause 1: the declared names must match, and no clause grants
substitution an allowance in particle restriction). The substitution-group
expansion rewrites the compiled term (an abstract head is dropped, members
join a synthetic choice), so the checker previously compared the expanded
names and the swap looked name-identical; `Particle.declaredReference` now
preserves the pre-expansion identity and the restriction check pairs the
declared references first. Closes XSTS elemZ026 and particlesV020
(invalid-schemas-accepted 8 -> 6); stricter than libxml2, which does not
implement this edge. particlesZ008, byte-for-byte the same construct with a
settled `valid` expectation contradicting elemZ026's settled `invalid`, is
reclassified as a spec-divergent schema test with the adjudication recorded
in the suite. Valid buckets are unchanged at zero.
### Changed
- A fifth serialize-path optimization pass (#139, `docs/benchmarks.md` pass
31): `textNeedsEscaping`/`attributeNeedsEscaping` and the `comment`/
`processingInstruction` existence fast-paths now scan a raw contiguous
buffer (`String.utf8.withContiguousStorageIfAvailable`) instead of going
through `UTF8View`'s default `Collection` iteration, falling back to the
original scan when no contiguous storage is available (every string this
library builds is natively contiguous, so the fallback is believed dead in
practice). Found by profiling a new, deeply-nested/attribute-dense corpus
the standard flat catalog corpus has no density to indict; a standalone
micro-benchmark confirmed the raw-buffer scan ~2-2.75x faster across
several call shapes, order-swap-checked against warm-up bias. Two
abstraction attempts were tried and rejected: a shared generic helper
taking a predicate closure recovered most but not all of the win, and
calling a separate small function from inside the buffer loop (to dodge a
cyclomatic-complexity lint violation) erased nearly the entire win even
though it is not a closure value; the violation is instead resolved by
splitting each function into a contiguous/collection pair, so the call
boundary sits outside the loop rather than inside it. Output is
byte-identical by construction (same predicate, same trigger bytes, only
the iteration mechanism changed); full local suite (1925 tests) and full
lint unchanged. Measured with interleaved same-state A/B under real
concurrent machine load (load average ~2.4): clean with zero overlap
across 5 pairs at the new attribute-heavy nested corpus (~9%); noisier at
the 100k-item standard corpus (2 of 5 pairs individually reversed by a
small margin) but the means still separate clearly (~4%), the same
trust-the-aggregate-under-diagnosed-noise call this log made for pass 28.
- Two serialize-path optimization passes (#139, `docs/benchmarks.md` passes
29-30), found by sample-profiling the serialize loop: the attribute writer
precomputes the quote character as a `String` once per element instead of
interpolating a `Character` into every attribute (a `Character`
interpolation goes through the string's grapheme-breadcrumb machinery, work
repeated twice per attribute for a value that never changes within an
element), and `Escaping.comment`/`processingInstruction`'s escaping
fallback (taken whenever a comment has a hyphen or PI data has a `?`, a
case pass 8 could only skip past, not speed up) is rewritten in the same
byte-scan bulk-copy shape as passes 23-24, so a comment or PI value with
one rare trigger no longer pays a full character-by-character rebuild. Both
are same-observable-behavior changes: the quote change is byte-identical by
construction (a `Character` and its single-scalar `String` render
identically); the comment/PI rewrite is verified against a new differential
suite (`Tests/CommentAndPIEscapingDifferentialTests.swift`) that
reimplements the original character-loop as an independent oracle and
checks hand-picked adjacency/trailing/combining-mark cases plus two
20000-case seeded fuzzes, since a byte match inside a combining-mark
cluster is not, on its own, the bare trigger character. Full local suite
(1925 tests) unchanged. Measured with interleaved same-state A/B: pass 29
~1.5% at a 100k-item corpus (clean, two pairs); pass 30 ~2x on a dedicated
comment/PI micro-benchmark (no comment/PI content exists in the standard
corpus, so it does not move the headline ratio there, but real-world
documents with copyright banners or hyphenated prose in comments exercise
it directly). A same-session multi-append restructuring attempt (splitting
a single interpolated `output +=` into several direct appends) measured a
clear ~6% regression and was reverted rather than landed; a suspected
`hasInlineContent` double-scan of an element's children was profiled and
found to contribute ~0.2% of samples, not worth a pass. Both outcomes are
recorded in `docs/benchmarks.md` rather than silently dropped.
- A fourth parse-path optimization pass (#139, `docs/benchmarks.md` pass 28):
`scanAttributes`' duplicate-name check reuses one stored scratch
`Set<String>` (cleared with `removeAll(keepingCapacity: true)` per element)
instead of allocating fresh native Set storage for every element with
attributes. Safe because `scanAttributes` cannot re-enter itself; duplicate
detection, recovering-mode behavior, and thrown errors are unchanged (full
local suite and full W3C XSTS pass unchanged). Measured ~2-4% on parse with
an interleaved same-state A/B (both patched runs beat both baseline runs),
the protocol chosen because the session had concurrent machine load. Two
companion outcomes from the same profiling round, recorded rather than
hidden: a children-capacity reservation candidate measured neutral and was
reverted per the benchmarks log's ship-only-measured-wins rule, and the
`TreeNode` weak-parent side-table cost (~9% of parse) is documented in
`docs/benchmarks.md` as a structural finding that needs a design decision,
not an optimization pass, since `parent`'s nil-after-release semantics are
documented public API.
- A third parse-path optimization pass (#139, `docs/benchmarks.md` pass 27),
found by sample-profiling `scanContentMarkup()` in isolation after passes
25-26 (it dominates parse time, ~46%, since it inlines
`scanStartTag`/`scanAttributes`/`scanAttributeValue`/`NamespaceContext.enterElement`):
`scanAttributeValue`'s opening and closing quote checks now use the byte
fast path (`peekByte()` + `reader.consume(_:)`/a byte peek before the
Character peek) instead of the Character path, for every attribute value in
the document; `NamespaceContext.checkExpandedNames` skips its `Set<String>`
allocation and key-string building entirely when fewer than two attributes
carry a namespace URI (the check can only ever throw on a collision between
two such attributes, so 0 or 1 can never trigger it, and this is the
overwhelming majority of elements); and `EntityDecoder`'s `raw.contains("&")`
guard is now a UTF-8 byte scan instead of a `Character`-based one, the same
shape as `NamespaceContext.checkNameShape`'s colon check in pass 26. All
three are same-observable-behavior changes, verified against the full local
suite (1858 tests, 6 new covering the quote-handling edge cases directly:
empty value, single-quoted, a quote-kind nested inside the other, and
unterminated-at-end-of-input, plus byte/Character-path agreement for each)
and the full W3C XSTS suite (14383 groups), both unchanged. Measured parse
~6.00x -> ~4.5-4.75x versus libxml2 at the 20000-item/4.2MB corpus, best of
5; see `docs/benchmarks.md` for the full before/after and reasoning.
- Two parse-path optimization passes (#139, `docs/benchmarks.md` passes 25-26),
found by sample-profiling a 42MB generated corpus after re-measuring the
parse/libxml2 ratio fresh rather than trusting the doc's last-recorded
numbers: `scanAttributeValue`'s closing-quote check now compares the
already-buffered peek directly instead of re-deriving the same answer
through `Reader.consume`'s general `String`-iteration path (which allocated
a fresh `String(quote)` per attribute value in the document), and
`NamespaceContext.enterElement` scans a name's UTF-8 bytes instead of its
Characters to find an illegal embedded colon, and skips its per-element
attribute-array allocation entirely when no attribute actually needs
namespace resolution (the common all-unprefixed-attributes case). Both are
same-observable-behavior changes (verified with the full local suite, 1852
tests, and the full W3C XSTS suite, 14383 groups, both unchanged) that
measured parse ~6.59x -> ~6.00x versus libxml2 at the 20000-item/4.2MB
corpus, best of 5; see `docs/benchmarks.md` for the full before/after and
the reasoning behind each change.
- Update Codeberg links after the NodetteHQ transfer, including README badges,
support and roadmap issue URLs, and the XSLT `system-property('xsl:vendor-url')`
result.
- Rebuild the issue tracker on Codeberg after the GitHub tracker loss: the
production-readiness epic, both XSTS gate trackers, the performance epic,
and the Xalan burn-down are recreated with current dated statuses, and
`docs/roadmap.md` and `README.md` now cite the new numbers. The stale
baseline figures in both documents are corrected to the enforced ratchet
in `Tests/XSTSSuiteTests.swift` (0 / 8 / 0 / 4); shipped items (located
diagnostics, the schema differential harness) no longer show as open.
Nothing yet.
## [0.4.8] - 2026-07-05

View File

@ -39,9 +39,7 @@ struct PublicSurfaceTests {
var chunks = ["<r><a>", "hello", "</a></r>"].makeIterator()
var characters: [Character] = []
var reader = PureXML.Parsing.EventReader(pulling: {
if characters.isEmpty, let chunk = chunks.next() {
characters = Array(chunk)
}
if characters.isEmpty, let chunk = chunks.next() { characters = Array(chunk) }
return characters.isEmpty ? nil : characters.removeFirst()
})
var events = 0

View File

@ -1,11 +1,11 @@
# PureXML
[![Style and namespacing](https://git.aleahim.com/NodetteHQ/PureXML/actions/workflows/style.yml/badge.svg?branch=main)](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=style.yml)
[![Swift macOS](https://git.aleahim.com/NodetteHQ/PureXML/actions/workflows/swift-macos.yml/badge.svg?branch=main)](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-macos.yml)
[![Swift Linux](https://git.aleahim.com/NodetteHQ/PureXML/actions/workflows/swift-linux.yml/badge.svg?branch=main)](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-linux.yml)
[![Swift Windows](https://git.aleahim.com/NodetteHQ/PureXML/actions/workflows/swift-windows.yml/badge.svg?branch=main)](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-windows.yml)
[![Swift WASM](https://git.aleahim.com/NodetteHQ/PureXML/actions/workflows/swift-wasm.yml/badge.svg?branch=main)](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-wasm.yml)
[![XSTS conformance](https://git.aleahim.com/NodetteHQ/PureXML/actions/workflows/swift-xsts.yml/badge.svg?branch=main)](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-xsts.yml)
[![Style and namespacing](https://codeberg.org/Mihaela/PureXML/actions/workflows/style.yml/badge.svg?branch=main)](https://codeberg.org/Mihaela/PureXML/actions?workflow=style.yml)
[![Swift macOS](https://codeberg.org/Mihaela/PureXML/actions/workflows/swift-macos.yml/badge.svg?branch=main)](https://codeberg.org/Mihaela/PureXML/actions?workflow=swift-macos.yml)
[![Swift Linux](https://codeberg.org/Mihaela/PureXML/actions/workflows/swift-linux.yml/badge.svg?branch=main)](https://codeberg.org/Mihaela/PureXML/actions?workflow=swift-linux.yml)
[![Swift Windows](https://codeberg.org/Mihaela/PureXML/actions/workflows/swift-windows.yml/badge.svg?branch=main)](https://codeberg.org/Mihaela/PureXML/actions?workflow=swift-windows.yml)
[![Swift WASM](https://codeberg.org/Mihaela/PureXML/actions/workflows/swift-wasm.yml/badge.svg?branch=main)](https://codeberg.org/Mihaela/PureXML/actions?workflow=swift-wasm.yml)
[![XSTS conformance](https://codeberg.org/Mihaela/PureXML/actions/workflows/swift-xsts.yml/badge.svg?branch=main)](https://codeberg.org/Mihaela/PureXML/actions?workflow=swift-xsts.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
PureXML is a dependency-free XML package written entirely in Swift.
@ -20,7 +20,7 @@ intentionally strict about portability:
- root Swift package layout
- macOS, Linux, Windows, and WASI build gates
It is a sibling project to [PureYAML](https://git.aleahim.com/NodetteHQ/PureYAML) and
It is a sibling project to [PureYAML](https://codeberg.org/Mihaela/PureYAML) and
follows the same structure, rules, and verification gates.
## Roadmap
@ -73,12 +73,9 @@ are all shipped (summarized above). The beyond-parity libxml2 conformance audit
output encodings, streaming validation, the last encodings, the analysis
findings, and the first W3C-suite pass) are all **complete**. Feature parity is
done; the remaining distance is **production readiness for IDE use**, tracked under
epic [#3](https://git.aleahim.com/NodetteHQ/PureXML/issues/3) (schema authority,
differential oracle, located diagnostics). Conformance suite maintenance is
closed, leaving [#7](https://git.aleahim.com/NodetteHQ/PureXML/issues/7) (XSLT xalan
burn-down) as the only active runner work; see `docs/roadmap.md`. Issue numbers
in the historical narrative below refer to the pre-migration tracker that was
lost with the GitHub repository.
epic **#167** (schema authority, differential oracle, located diagnostics). Conformance
suite maintenance (**#122**, closed) leaves **#130** (XSLT xalan burn-down) as the only
active runner work; see `docs/roadmap.md`.
```mermaid
flowchart TB
@ -105,19 +102,19 @@ flowchart TB
BaseURI["#138 per-entity base URIs: complete"]:::done
Epic122["#122 conformance suite maintenance: complete (#130 survivor)"]:::epic
XSTS["#129 XSTS runner shipped; gates #145-#148 at 0/39/0/26"]:::review
XSLTTS["#7 XSLT 1.0 suite (xalan-test): 26 pinned baseline failures"]:::review
XSLTTS["#130 XSLT 1.0 suite (xalan-test): ~126 baseline failures"]:::review
Parity100["Behavioral parity closed: #141 top-level params, #142 public streaming, #143 XPath budget, #144 WASI runtime proof, pl/ru sort collation"]:::done
Epic139["#6 faster than libxml2, measured + streaming (benchmarks in docs/benchmarks.md; quadratics killed; unsafe authorized)"]:::epic
Epic139["#139 faster than libxml2, measured + streaming (benchmarks in docs/benchmarks.md; quadratics killed; unsafe authorized)"]:::epic
Audit140["#140 XSLT validation-framework audit"]:::todo
Epic139 --> Parity100
Epic139 --> Audit140
RNGTS["#131 RELAX NG spec suite: complete, one documented class"]:::done
C14NTS["#132 C14N spec vectors: complete"]:::done
Epic167["#3 production readiness for IDE use"]:::epic
Oracle171["schema differential harness (shipped)"]:::done
Epic167["#167 production readiness for IDE use (v0.2.0)"]:::epic
Oracle171["#171 schema differential harness"]:::todo
CrossDoc161["#161 cross-document composition"]:::todo
Gates145["#4 / #5 XSTS gates: burn-down 8 and 4 to 0"]:::review
Loc169["located schema diagnostics (shipped)"]:::done
Gates145["#145-#148 XSTS gates: burn-down to 0"]:::review
Loc169["#169 located schema diagnostics"]:::todo
Epic195["#195 xsi:type and substitution-group derivation correctness"]:::epic
Epic105 --> Epic121
Epic105 --> Epic122
@ -145,7 +142,7 @@ flowchart TB
## Status
**Current focus:** production readiness for IDE use ([#3](https://git.aleahim.com/NodetteHQ/PureXML/issues/3)). XSTS settled baselines (valid schemas rejected / invalid schemas accepted / valid instances rejected / invalid instances accepted), enforced in `Tests/XSTSSuiteTests.swift`: **0 / 8 / 0 / 4**. See [`docs/roadmap.md`](docs/roadmap.md) for milestones and open tasks.
**Current focus:** production readiness for IDE use ([#167](https://codeberg.org/Mihaela/PureXML/issues/167)). XSTS settled baselines (valid schemas rejected / invalid schemas accepted / valid instances rejected / invalid instances accepted), enforced in `Tests/XSTSSuiteTests.swift`: **0 / 39 / 0 / 26**. See [`docs/roadmap.md`](docs/roadmap.md) for milestones and open tasks.
PureXML is a working, dependency-free XML library today: parse, emit, validate,
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
@ -224,13 +221,11 @@ beyond-parity audit epics (**#71**, its parser-breadth sub-epic **#74**) and the
parity-frontier epic (**#105**) are closed.
The remaining distance is **production readiness for IDE use**, tracked under epic
[#3](https://git.aleahim.com/NodetteHQ/PureXML/issues/3): driving the XSTS conformance
buckets toward zero (currently **0 / 8 / 0 / 4**, gates
[#4](https://git.aleahim.com/NodetteHQ/PureXML/issues/4) and
[#5](https://git.aleahim.com/NodetteHQ/PureXML/issues/5)); located schema diagnostics
and the schema differential oracle are shipped. The one active runner burn-down
is [#7](https://git.aleahim.com/NodetteHQ/PureXML/issues/7) (the XSLT 1.0 xalan
suite). See [`docs/roadmap.md`](docs/roadmap.md).
**#167**: driving the XSTS conformance buckets toward zero (currently
**0 / 39 / 0 / 26**), located schema diagnostics (#169), the schema differential
oracle (#171), and cross-document composition (#161). The one active runner
burn-down is **#130** (the XSLT 1.0 xalan suite). See
[`docs/roadmap.md`](docs/roadmap.md).
## Usage

View File

@ -2,8 +2,8 @@
Need help with PureXML? Here is where to go.
- **Questions and ideas**: open an [issue](https://git.aleahim.com/NodetteHQ/PureXML/issues).
- **Bug reports**: open an [issue](https://git.aleahim.com/NodetteHQ/PureXML/issues/new/choose)
- **Questions and ideas**: open a [Discussion](https://codeberg.org/Mihaela/PureXML/discussions).
- **Bug reports**: open an [issue](https://codeberg.org/Mihaela/PureXML/issues/new/choose)
using the bug form.
- **Feature requests**: open an issue using the feature form.
- **Security issues**: do not open a public issue. See [SECURITY.md](SECURITY.md).

View File

@ -18,9 +18,7 @@ extension PureXML.Canonical.Canonicalizer {
let declared = Set(apex.attributes.map(\.name.description))
for (prefix, uri) in ancestorNamespaces {
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
if !declared.contains(name) {
attributes.append(PureXML.Model.Attribute(name, uri))
}
if !declared.contains(name) { attributes.append(PureXML.Model.Attribute(name, uri)) }
}
}
return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children)
@ -45,9 +43,7 @@ extension PureXML.Canonical.Canonicalizer {
/// apex does not already set, nearest ancestor winning.
static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] {
let present = Set(apex.attributes.map(\.name.description))
if mergeBase {
return inherited11XMLAttributes(above: node, apex: apex, present: present)
}
if mergeBase { return inherited11XMLAttributes(above: node, apex: apex, present: present) }
// Canonical XML 1.0: the nearest of every in-scope xml:* attribute the
// apex does not already set.
var seen: Set<String> = []
@ -67,12 +63,8 @@ extension PureXML.Canonical.Canonicalizer {
/// The prefix a namespace declaration binds (empty for the default
/// namespace), or nil when the attribute is not a namespace declaration.
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
if name.prefix == nil, name.localName == "xmlns" {
return ""
}
if name.prefix == "xmlns" {
return name.localName
}
if name.prefix == nil, name.localName == "xmlns" { return "" }
if name.prefix == "xmlns" { return name.localName }
return nil
}
}

View File

@ -208,12 +208,8 @@ extension PureXML.Canonical {
}
private static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
if name.prefix == nil, name.localName == "xmlns" {
return ""
}
if name.prefix == "xmlns" {
return name.localName
}
if name.prefix == nil, name.localName == "xmlns" { return "" }
if name.prefix == "xmlns" { return name.localName }
return nil
}
}

View File

@ -5,9 +5,7 @@ extension PureXML.Canonical.Canonicalizer {
static func inherited11XMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, present: Set<String>) -> [PureXML.Model.Attribute] {
var result: [PureXML.Model.Attribute] = []
for local in ["lang", "space"] where !present.contains("xml:\(local)") {
if let attribute = nearestXMLAttribute(local, above: node) {
result.append(attribute)
}
if let attribute = nearestXMLAttribute(local, above: node) { result.append(attribute) }
}
if let base = mergedBase(above: node, apex: apex) {
result.append(PureXML.Model.Attribute("xml:base", base))
@ -96,35 +94,27 @@ extension PureXML.Canonical.Canonicalizer {
var input = Substring(path)
var output = ""
while !input.isEmpty {
if input.hasPrefix("../") {
input = input.dropFirst(3)
if input.hasPrefix("../") { input = input.dropFirst(3)
continue
}
if input.hasPrefix("./") {
input = input.dropFirst(2)
if input.hasPrefix("./") { input = input.dropFirst(2)
continue
}
if input.hasPrefix("/./") {
input = "/" + input.dropFirst(3)
if input.hasPrefix("/./") { input = "/" + input.dropFirst(3)
continue
}
if input == "/." {
input = "/"
if input == "/." { input = "/"
continue
}
if input.hasPrefix("/../") {
input = "/" + input.dropFirst(4)
if input.hasPrefix("/../") { input = "/" + input.dropFirst(4)
removeLastSegment(&output)
continue
}
if input == "/.." {
input = "/"
if input == "/.." { input = "/"
removeLastSegment(&output)
continue
}
if input == "." || input == ".." {
break
}
if input == "." || input == ".." { break }
let start = input.hasPrefix("/") ? input.index(after: input.startIndex) : input.startIndex
let next = input[start...].firstIndex(of: "/") ?? input.endIndex
output += input[..<next]
@ -202,19 +192,11 @@ private struct URIParts {
/// Recomposes the components into a URI string (RFC 3986 section 5.3).
var recomposed: String {
var result = ""
if let scheme {
result += scheme + ":"
}
if let authority {
result += "//" + authority
}
if let scheme { result += scheme + ":" }
if let authority { result += "//" + authority }
result += path
if let query {
result += "?" + query
}
if let fragment {
result += "#" + fragment
}
if let query { result += "?" + query }
if let fragment { result += "#" + fragment }
return result
}
}

View File

@ -68,9 +68,7 @@ public extension PureXML.Canonical {
for child in children {
var rendering = ""
emit(child, inScope: inScope, rendered: rendered, output: &rendering)
if !rendering.isEmpty {
renderings.append(rendering)
}
if !rendering.isEmpty { renderings.append(rendering) }
}
output += renderings.joined(separator: "\n")
case let .element(element):
@ -79,9 +77,7 @@ public extension PureXML.Canonical {
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
output += Self.escapeText(text)
case let .comment(value):
if options.includeComments {
output += "<!--\(value)-->"
}
if options.includeComments { output += "<!--\(value)-->" }
case let .processingInstruction(target, data):
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
}
@ -153,9 +149,7 @@ public extension PureXML.Canonical {
let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value
output += Self.escapeText(text)
case let .comment(value):
if options.includeComments {
output += "<!--\(value)-->"
}
if options.includeComments { output += "<!--\(value)-->" }
case let .processingInstruction(target, data):
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
case .element, .document:
@ -204,9 +198,7 @@ public extension PureXML.Canonical {
for prefix in utilized {
let uri = inScope[prefix] ?? ""
if prefix.isEmpty {
if uri != (rendered[""] ?? "") {
result.append(("", uri))
}
if uri != (rendered[""] ?? "") { result.append(("", uri)) }
} else if !uri.isEmpty, rendered[prefix] != uri {
result.append((prefix, uri))
}
@ -223,21 +215,15 @@ public extension PureXML.Canonical {
private static func attributeOrder(_ lhs: PureXML.Model.Attribute, _ rhs: PureXML.Model.Attribute) -> Bool {
let leftURI = lhs.name.namespaceURI ?? ""
let rightURI = rhs.name.namespaceURI ?? ""
if leftURI != rightURI {
return leftURI < rightURI
}
if leftURI != rightURI { return leftURI < rightURI }
return lhs.name.localName < rhs.name.localName
}
private static func namespaceDeclarations(_ element: PureXML.Model.Element) -> [(String, String)] {
element.attributes.compactMap { attribute in
let name = attribute.name
if name.prefix == nil, name.localName == "xmlns" {
return ("", attribute.value)
}
if name.prefix == "xmlns" {
return (name.localName, attribute.value)
}
if name.prefix == nil, name.localName == "xmlns" { return ("", attribute.value) }
if name.prefix == "xmlns" { return (name.localName, attribute.value) }
return nil
}
}
@ -341,9 +327,7 @@ public extension PureXML.Canonical.Canonicalizer {
case .text, .cdata:
output += Self.escapeText(options.trimTextNodes ? node.value.trimmingXMLWhitespace() : node.value)
case .comment:
if options.includeComments {
output += "<!--\(node.value)-->"
}
if options.includeComments { output += "<!--\(node.value)-->" }
case .processingInstruction:
output += node.value.isEmpty ? "<?\(node.name?.description ?? "")?>" : "<?\(node.name?.description ?? "") \(node.value)?>"
case .element, .document, .doctype, .entityReference, .namespace:

View File

@ -27,9 +27,7 @@ private struct CatalogBuilder {
insert(node, key: "systemId", base: base, into: &systemMap)
case "public":
insert(node, key: "publicId", base: base, into: &publicMap)
if let id = attribute(node, "publicId") {
publicPrefer[id] = prefer
}
if let id = attribute(node, "publicId") { publicPrefer[id] = prefer }
case "uri":
insert(node, key: "name", base: base, into: &uriMap)
case "rewriteSystem":
@ -64,9 +62,7 @@ private struct CatalogBuilder {
case "delegateURI":
appendDelegate(node, start: "uriStartString", base: base, into: &delegateURI)
case "nextCatalog":
if let catalog = attribute(node, "catalog") {
nextCatalogs.append(resolve(catalog, base))
}
if let catalog = attribute(node, "catalog") { nextCatalogs.append(resolve(catalog, base)) }
default:
break
}

View File

@ -163,22 +163,16 @@ public extension PureXML.Catalog {
_ loadingCatalog: (String) -> String?,
_ visited: inout Set<String>,
) -> String? {
if let local = pick(self) {
return local
}
if let local = pick(self) { return local }
let matching = delegates
.filter { identifier.hasPrefix($0.startString) }
.sorted { $0.startString.count > $1.startString.count }
for rule in matching {
if let sub = load(rule.catalog, loadingCatalog, &visited), let resolved = pick(sub) {
return resolved
}
if let sub = load(rule.catalog, loadingCatalog, &visited), let resolved = pick(sub) { return resolved }
}
for next in nextCatalogs {
guard let sub = load(next, loadingCatalog, &visited) else { continue }
if let resolved = sub.chained(identifier, delegates: [], pick: pick, loadingCatalog, &visited) {
return resolved
}
if let resolved = sub.chained(identifier, delegates: [], pick: pick, loadingCatalog, &visited) { return resolved }
}
return nil
}

View File

@ -27,22 +27,16 @@ private struct SGMLBuilder {
mutating func apply(_ keyword: String, _ scanner: inout SGMLScanner) {
let upper = keyword.uppercased()
if applyPair(upper, &scanner) {
return
}
if applyPair(upper, &scanner) { return }
applySingle(upper, &scanner)
}
private mutating func applyPair(_ keyword: String, _ scanner: inout SGMLScanner) -> Bool {
switch keyword {
case "PUBLIC":
if let id = scanner.next(), let uri = scanner.next() {
publicMap[id] = resolve(uri)
}
if let id = scanner.next(), let uri = scanner.next() { publicMap[id] = resolve(uri) }
case "SYSTEM":
if let id = scanner.next(), let uri = scanner.next() {
systemMap[id] = resolve(uri)
}
if let id = scanner.next(), let uri = scanner.next() { systemMap[id] = resolve(uri) }
case "DELEGATE":
if let prefix = scanner.next(), let catalog = scanner.next() {
delegatePublic.append(PureXML.Catalog.DelegateRule(startString: prefix, catalog: resolve(catalog)))
@ -56,17 +50,11 @@ private struct SGMLBuilder {
private mutating func applySingle(_ keyword: String, _ scanner: inout SGMLScanner) {
switch keyword {
case "CATALOG":
if let catalog = scanner.next() {
nextCatalogs.append(resolve(catalog))
}
if let catalog = scanner.next() { nextCatalogs.append(resolve(catalog)) }
case "BASE":
if let declared = scanner.next() {
base = resolve(declared)
}
if let declared = scanner.next() { base = resolve(declared) }
case "OVERRIDE":
if let value = scanner.next() {
preferPublic = value.uppercased() == "YES"
}
if let value = scanner.next() { preferPublic = value.uppercased() == "YES" }
default:
break
}

View File

@ -20,16 +20,12 @@ extension PureXML.Emitting {
while let marker = value.utf8[runStart...].firstIndex(where: {
textEscapableByte($0, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn)
}) {
if runStart < marker {
result += value[runStart ..< marker]
}
if runStart < marker { result += value[runStart ..< marker] }
let character = value[marker]
result += textEscape(character, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn) ?? String(character)
runStart = value.index(after: marker)
}
if runStart < value.endIndex {
result += value[runStart...]
}
if runStart < value.endIndex { result += value[runStart...] }
return result
}
@ -59,42 +55,8 @@ extension PureXML.Emitting {
/// Whether ``text`` would change `value`: it escapes `&`, `<`, `>` always,
/// a carriage return when asked, and (in ASCII-only mode) any non-ASCII
/// byte. Scanned at the byte level, since every trigger is ASCII or, for
/// ASCII-only mode, a non-ASCII lead byte. Run on every text node in a
/// document, so it scans a raw contiguous buffer when the string
/// provides one: `UTF8View`'s default `Collection` iteration measured
/// markedly slower than a raw-buffer scan for this shape (~2x on a
/// dedicated micro-benchmark), and every string this library builds
/// (parsed from its own byte-backed reader) is natively contiguous, so
/// the fallback below only exists for a foreign/bridged string.
/// ASCII-only mode, a non-ASCII lead byte.
private static func textNeedsEscaping(_ value: String, asciiOnly: Bool, escapeCarriageReturn: Bool) -> Bool {
textNeedsEscapingContiguous(value, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn)
?? textNeedsEscapingCollection(value, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn)
}
/// The raw contiguous-buffer scan, split into its own function (rather
/// than inlined alongside the ``textNeedsEscapingCollection`` fallback in
/// one body) purely to keep each function's cyclomatic complexity low;
/// the switch itself must stay duplicated, not shared through a per-byte
/// helper call, since that measured erasing almost the entire win.
private static func textNeedsEscapingContiguous(_ value: String, asciiOnly: Bool, escapeCarriageReturn: Bool) -> Bool? {
value.utf8.withContiguousStorageIfAvailable { buffer -> Bool in
for byte in buffer {
switch byte {
case 0x26, 0x3C, 0x3E: return true
case 0x0D where escapeCarriageReturn: return true
case 0x80... where asciiOnly: return true
default: continue
}
}
return false
}
}
/// `Collection`-based fallback for a string with no contiguous storage
/// (a foreign/bridged string); every string this library builds is
/// natively contiguous, so this path is believed dead in practice but
/// kept for correctness.
private static func textNeedsEscapingCollection(_ value: String, asciiOnly: Bool, escapeCarriageReturn: Bool) -> Bool {
for byte in value.utf8 {
switch byte {
case 0x26, 0x3C, 0x3E: return true
@ -136,16 +98,12 @@ extension PureXML.Emitting {
while let marker = value.utf8[runStart...].firstIndex(where: {
attributeEscapableByte($0, quoteByte: quoteByte, asciiOnly: asciiOnly)
}) {
if runStart < marker {
result += value[runStart ..< marker]
}
if runStart < marker { result += value[runStart ..< marker] }
let character = value[marker]
result += attributeEscape(character, quote: quote, asciiOnly: asciiOnly) ?? String(character)
runStart = value.index(after: marker)
}
if runStart < value.endIndex {
result += value[runStart...]
}
if runStart < value.endIndex { result += value[runStart...] }
return result
}
@ -179,33 +137,8 @@ extension PureXML.Emitting {
/// Whether ``attribute`` would change `value`: it escapes the quote, the
/// markup characters, the whitespace characters, and (in ASCII-only mode)
/// any non-ASCII byte. Every trigger is ASCII bar the non-ASCII lead. Run
/// on every attribute value in a document, so it prefers the same raw
/// contiguous-buffer scan as ``textNeedsEscaping``, for the same measured
/// reason.
/// any non-ASCII byte. Every trigger is ASCII bar the non-ASCII lead.
private static func attributeNeedsEscaping(_ value: String, quoteByte: UInt8, asciiOnly: Bool) -> Bool {
attributeNeedsEscapingContiguous(value, quoteByte: quoteByte, asciiOnly: asciiOnly)
?? attributeNeedsEscapingCollection(value, quoteByte: quoteByte, asciiOnly: asciiOnly)
}
/// The raw contiguous-buffer scan; see ``textNeedsEscapingContiguous``
/// for why this is a separate function rather than sharing the switch
/// with the fallback through a per-byte helper call.
private static func attributeNeedsEscapingContiguous(_ value: String, quoteByte: UInt8, asciiOnly: Bool) -> Bool? {
value.utf8.withContiguousStorageIfAvailable { buffer -> Bool in
for byte in buffer {
switch byte {
case quoteByte, 0x26, 0x3C, 0x3E, 0x09, 0x0A, 0x0D: return true
case 0x80... where asciiOnly: return true
default: continue
}
}
return false
}
}
/// `Collection`-based fallback; see ``textNeedsEscapingCollection``.
private static func attributeNeedsEscapingCollection(_ value: String, quoteByte: UInt8, asciiOnly: Bool) -> Bool {
for byte in value.utf8 {
switch byte {
case quoteByte, 0x26, 0x3C, 0x3E, 0x09, 0x0A, 0x0D: return true
@ -222,44 +155,15 @@ extension PureXML.Emitting {
static func comment(_ value: String) -> String {
// Fast path: a comment with no hyphen has no `--` and no trailing `-`,
// so it is returned unchanged rather than rebuilt character by character.
// A raw contiguous-buffer scan for the same measured reason as
// textNeedsEscaping/attributeNeedsEscaping above.
let hasHyphen = value.utf8.withContiguousStorageIfAvailable { buffer -> Bool in
for byte in buffer where byte == 0x2D {
return true
}
return false
} ?? value.utf8.contains(0x2D)
if !hasHyphen {
return value
}
if !value.utf8.contains(0x2D) { return value }
var result = ""
var previousHyphen = false
var runStart = value.startIndex
// Find each candidate hyphen byte, copy the verbatim run before it in
// one append, and resolve it to its full grapheme (a byte match inside
// a combining-mark cluster is not a bare "-" and is copied like any
// other character); only a genuine "-" can extend or start a run.
while let marker = value.utf8[runStart...].firstIndex(where: { $0 == 0x2D }) {
if runStart < marker {
result += value[runStart ..< marker]
previousHyphen = false
}
let character = value[marker]
if character == "-", previousHyphen {
result += " "
}
for character in value {
if character == "-", previousHyphen { result.append(" ") }
result.append(character)
previousHyphen = character == "-"
runStart = value.index(after: marker)
}
if runStart < value.endIndex {
result += value[runStart...]
previousHyphen = false
}
if previousHyphen {
result += " "
}
if previousHyphen { result.append(" ") }
return result
}
@ -268,39 +172,14 @@ extension PureXML.Emitting {
/// (the XSLT 1.0 7.3 recovery, so `a?>b` becomes `a? >b`).
static func processingInstruction(_ data: String) -> String {
// Fast path: data with no '?' cannot contain `?>`, so it is returned
// unchanged rather than rebuilt character by character. A raw
// contiguous-buffer scan for the same measured reason as
// textNeedsEscaping/attributeNeedsEscaping above.
let hasQuestion = data.utf8.withContiguousStorageIfAvailable { buffer -> Bool in
for byte in buffer where byte == 0x3F {
return true
}
return false
} ?? data.utf8.contains(0x3F)
if !hasQuestion {
return data
}
// unchanged rather than rebuilt character by character.
if !data.utf8.contains(0x3F) { return data }
var result = ""
var previousQuestion = false
var runStart = data.startIndex
// Candidates are '?' (may start the offending pair) and '>' (may close
// it); every other character, including one that merely shares a byte
// with a combining-mark cluster, is bulk-copied and resets the flag.
while let marker = data.utf8[runStart...].firstIndex(where: { $0 == 0x3F || $0 == 0x3E }) {
if runStart < marker {
result += data[runStart ..< marker]
previousQuestion = false
}
let character = data[marker]
if character == ">", previousQuestion {
result += " "
}
for character in data {
if character == ">", previousQuestion { result.append(" ") }
result.append(character)
previousQuestion = character == "?"
runStart = data.index(after: marker)
}
if runStart < data.endIndex {
result += data[runStart...]
}
return result
}

View File

@ -103,10 +103,9 @@ public extension PureXML.Emitting {
}
output += "<" + element.name.description
let quote = options.attributeQuote.character
let quoteString = String(quote)
for attribute in element.attributes {
let value = Escaping.attribute(attribute.value, quote: quote, asciiOnly: options.asciiOnly)
output += " \(attribute.name.description)=\(quoteString)\(value)\(quoteString)"
output += " \(attribute.name.description)=\(quote)\(value)\(quote)"
}
if element.children.isEmpty, options.selfCloseEmptyElements {

View File

@ -37,12 +37,8 @@ extension HTMLDocument {
/// scope boundary.
private func hasInScope(_ target: PureXML.Model.TreeNode) -> Bool {
for node in openBody.reversed() {
if node === target {
return true
}
if Self.scopeBoundaries.contains(tagName(node)) {
return false
}
if node === target { return true }
if Self.scopeBoundaries.contains(tagName(node)) { return false }
}
return false
}
@ -52,9 +48,7 @@ extension HTMLDocument {
/// re-wrapped (the `<b><i></b>X</i>` -> `<b><i></i></b><i>X</i>` behavior).
func reconstructActiveFormatting() {
guard var index = activeFormatting.indices.last else { return }
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) {
return
}
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { return }
while index > 0 {
index -= 1
if activeFormatting[index] == nil || isOpen(activeFormatting[index]) {
@ -97,9 +91,7 @@ extension HTMLDocument {
activeFormatting.remove(at: formattingIndexInList)
return
}
if !hasInScope(formattingElement) {
return
}
if !hasInScope(formattingElement) { return }
guard let furthestBlockIndex = openBody[(formattingOpenIndex + 1)...].firstIndex(where: { Self.special.contains(tagName($0)) }) else {
openBody.removeLast(openBody.count - formattingOpenIndex)
activeFormatting.remove(at: formattingIndexInList)
@ -112,9 +104,7 @@ extension HTMLDocument {
private func lastFormatting(tag: String) -> Int? {
for index in activeFormatting.indices.reversed() {
guard let entry = activeFormatting[index] else { return nil } // a marker bounds the search
if tagName(entry) == tag {
return index
}
if tagName(entry) == tag { return index }
}
return nil
}
@ -134,9 +124,7 @@ extension HTMLDocument {
inner += 1
nodeIndex -= 1
let node = openBody[nodeIndex]
if node === formattingElement {
break
}
if node === formattingElement { break }
guard let activeIndex = formattingIndex(node) else {
openBody.remove(at: nodeIndex)
continue
@ -144,9 +132,7 @@ extension HTMLDocument {
let clone = node.shallowCopy()
activeFormatting[activeIndex] = clone
openBody[nodeIndex] = clone
if lastNode === furthestBlock {
bookmark = activeIndex + 1
}
if lastNode === furthestBlock { bookmark = activeIndex + 1 }
clone.append(lastNode)
lastNode = clone
}
@ -158,19 +144,13 @@ extension HTMLDocument {
furthestBlock.append(formattingClone)
if let removeIndex = formattingIndex(formattingElement) {
activeFormatting.remove(at: removeIndex)
if removeIndex < bookmark {
bookmark -= 1
}
if removeIndex < bookmark { bookmark -= 1 }
}
activeFormatting.insert(formattingClone, at: min(bookmark, activeFormatting.count))
if let removeOpen = openIndex(formattingElement) {
openBody.remove(at: removeOpen)
}
if let removeOpen = openIndex(formattingElement) { openBody.remove(at: removeOpen) }
// The clone becomes a child of the furthest block, so it sits immediately
// above it on the open-elements stack; inserting below would re-find the
// same furthest block and loop.
if let blockIndex = openIndex(furthestBlock) {
openBody.insert(formattingClone, at: blockIndex + 1)
}
if let blockIndex = openIndex(furthestBlock) { openBody.insert(formattingClone, at: blockIndex + 1) }
}
}

View File

@ -1,3 +1,9 @@
/// The foreign-content namespaces HTML5 switches into. File-scope and private.
private enum ForeignNamespace {
static let svg = "http://www.w3.org/2000/svg"
static let mathml = "http://www.w3.org/1998/Math/MathML"
}
/// The HTML5 tree-construction insertion modes this builder implements: the
/// document-structure subset that establishes the implied `html`, `head`, and
/// `body`. File-scope and private.
@ -78,12 +84,8 @@ final class HTMLDocument {
}
private func processInitial(_ token: Token) {
if case .doctype = token {
return
}
if isIgnorableWhitespace(token) {
return
}
if case .doctype = token { return }
if isIgnorableWhitespace(token) { return }
mode = .beforeHtml
process(token)
}
@ -94,17 +96,13 @@ final class HTMLDocument {
mode = .beforeHead
return
}
if isIgnorableWhitespace(token) {
return
}
if isIgnorableWhitespace(token) { return }
mode = .beforeHead
process(token)
}
private func processBeforeHead(_ token: Token) {
if isIgnorableWhitespace(token) {
return
}
if isIgnorableWhitespace(token) { return }
if case let .startTag(name, attributes, _) = token, name == "head" {
headAttributes = modelAttributes(attributes)
mode = .inHead
@ -142,9 +140,7 @@ final class HTMLDocument {
}
let element = PureXML.Model.TreeNode.element(name, attributes: modelAttributes(attributes))
openHead.last?.append(element)
if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) {
openHead.append(element)
}
if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) { openHead.append(element) }
}
private func headClose(_ name: String) {
@ -172,9 +168,7 @@ final class HTMLDocument {
process(token)
return
}
if isIgnorableWhitespace(token) {
return
}
if isIgnorableWhitespace(token) { return }
mode = .inBody
process(token)
}
@ -216,11 +210,7 @@ final class HTMLDocument {
case let .startTag(name, attributes, _) where name == "noframes":
framesetStack.last?.append(PureXML.Model.TreeNode.element("noframes", attributes: modelAttributes(attributes)))
case let .endTag(name) where name == "frameset":
if framesetStack.count > 1 {
framesetStack.removeLast()
} else {
mode = .afterFrameset
}
if framesetStack.count > 1 { framesetStack.removeLast() } else { mode = .afterFrameset }
case let .comment(value):
framesetStack.last?.append(.comment(value))
default:
@ -233,12 +223,27 @@ final class HTMLDocument {
node.name?.localName.lowercased() ?? ""
}
/// Tags that close an open `<select>` (the "in select" rule): a nested select,
/// or an input/keygen/textarea.
private static let closesSelect: Set<String> = ["select", "input", "keygen", "textarea"]
/// Table-structural tags that close a select which is inside a table (the "in
/// select in table" rule).
private static let closesSelectInTable: Set<String> = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
/// The open-stack index of a `<select>` that an opening table tag should close
/// (a select inside a table), or nil when there is none.
private func selectInTableToClose(for name: String) -> Int? {
guard Self.closesSelectInTable.contains(name),
let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1,
openBody[..<selectIndex].contains(where: { tagName($0) == "table" }) else { return nil }
return selectIndex
}
private func bodyOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) {
if Self.closesSelect.contains(name), let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1 {
openBody.removeLast(openBody.count - selectIndex)
if name == "select" {
return
} // a nested select just closes the open one
if name == "select" { return } // a nested select just closes the open one
}
if let selectIndex = selectInTableToClose(for: name) {
openBody.removeLast(openBody.count - selectIndex)
@ -265,6 +270,51 @@ final class HTMLDocument {
private func bodyInsert(_ node: PureXML.Model.TreeNode) {
openBody.last?.append(node)
}
private func bodyEnsureTableContext(for name: String) {
let structural: Set = ["table", "tbody", "thead", "tfoot", "tr"]
guard let context = openBody.reversed().first(where: { structural.contains(tagName($0)) }).map(tagName) else { return }
switch name {
case "tr" where context == "table":
bodyPushImplied("tbody")
case "td", "th":
if context == "table" { bodyPushImplied("tbody") }
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) { bodyPushImplied("tr") }
default:
break
}
}
private func bodyPushImplied(_ name: String) {
let element = PureXML.Model.TreeNode.element(name)
openBody.last?.append(element)
openBody.append(element)
}
/// Restores SVG attribute names to their canonical camel case.
private func adjustedAttributes(_ attributes: [Attribute], namespace: String?) -> [Attribute] {
guard namespace == ForeignNamespace.svg else { return attributes }
return attributes.map { attribute in
guard let adjusted = PureXML.HTML.ForeignNames.svgAttributes[attribute.name.localName.lowercased()] else { return attribute }
return Attribute(adjusted, attribute.value)
}
}
/// The foreign-content namespace for a body element: SVG/MathML on entry, the
/// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
private func bodyForeignNamespace(for name: String) -> String? {
if name == "svg" { return ForeignNamespace.svg }
if name == "math" { return ForeignNamespace.mathml }
for node in openBody.reversed() {
guard let namespace = node.name?.namespaceURI else { continue }
if Self.svgIntegrationPoints.contains(tagName(node)) { return nil }
return namespace
}
return nil
}
/// The SVG HTML integration points: their content is parsed as HTML.
private static let svgIntegrationPoints: Set<String> = ["foreignobject", "desc", "title"]
}
extension HTMLDocument {
@ -280,9 +330,7 @@ extension HTMLDocument {
}
private func isIgnorableWhitespace(_ token: Token) -> Bool {
if case let .text(value) = token {
return value.trimmingXMLWhitespace().isEmpty
}
if case let .text(value) = token { return value.trimmingXMLWhitespace().isEmpty }
return false
}
}

View File

@ -1,75 +0,0 @@
/// Table-context and foreign-content (SVG/MathML) helpers, split out so
/// HTMLDocument's own body stays within the type-body length budget.
extension HTMLDocument {
/// Tags that close an open `<select>` (the "in select" rule): a nested select,
/// or an input/keygen/textarea.
static let closesSelect: Set<String> = ["select", "input", "keygen", "textarea"]
/// Table-structural tags that close a select which is inside a table (the "in
/// select in table" rule).
private static let closesSelectInTable: Set<String> = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
/// The SVG HTML integration points: their content is parsed as HTML.
private static let svgIntegrationPoints: Set<String> = ["foreignobject", "desc", "title"]
/// The open-stack index of a `<select>` that an opening table tag should close
/// (a select inside a table), or nil when there is none.
func selectInTableToClose(for name: String) -> Int? {
guard Self.closesSelectInTable.contains(name),
let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1,
openBody[..<selectIndex].contains(where: { tagName($0) == "table" }) else { return nil }
return selectIndex
}
func bodyEnsureTableContext(for name: String) {
let structural: Set = ["table", "tbody", "thead", "tfoot", "tr"]
guard let context = openBody.reversed().first(where: { structural.contains(tagName($0)) }).map(tagName) else { return }
switch name {
case "tr" where context == "table":
bodyPushImplied("tbody")
case "td", "th":
if context == "table" {
bodyPushImplied("tbody")
}
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) {
bodyPushImplied("tr")
}
default:
break
}
}
private func bodyPushImplied(_ name: String) {
let element = PureXML.Model.TreeNode.element(name)
openBody.last?.append(element)
openBody.append(element)
}
/// Restores SVG attribute names to their canonical camel case.
func adjustedAttributes(_ attributes: [Attribute], namespace: String?) -> [Attribute] {
guard namespace == ForeignNamespace.svg else { return attributes }
return attributes.map { attribute in
guard let adjusted = PureXML.HTML.ForeignNames.svgAttributes[attribute.name.localName.lowercased()] else { return attribute }
return Attribute(adjusted, attribute.value)
}
}
/// The foreign-content namespace for a body element: SVG/MathML on entry, the
/// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
func bodyForeignNamespace(for name: String) -> String? {
if name == "svg" {
return ForeignNamespace.svg
}
if name == "math" {
return ForeignNamespace.mathml
}
for node in openBody.reversed() {
guard let namespace = node.name?.namespaceURI else { continue }
if Self.svgIntegrationPoints.contains(tagName(node)) {
return nil
}
return namespace
}
return nil
}
}

View File

@ -59,17 +59,13 @@ extension PureXML.HTML.Tokenizer {
private static func decodeNumeric(_ characters: [Character], at start: Int) -> (String, Int)? {
var cursor = start + 2 // past "&#"
let isHex = cursor < characters.count && (characters[cursor] == "x" || characters[cursor] == "X")
if isHex {
cursor += 1
}
if isHex { cursor += 1 }
let digitsStart = cursor
while cursor < characters.count, isDigit(characters[cursor], hex: isHex) {
cursor += 1
}
guard cursor > digitsStart, let number = UInt32(String(characters[digitsStart ..< cursor]), radix: isHex ? 16 : 10) else { return nil }
if cursor < characters.count, characters[cursor] == ";" {
cursor += 1
}
if cursor < characters.count, characters[cursor] == ";" { cursor += 1 }
return (String(numericCharacter(number)), cursor - start)
}

View File

@ -1,5 +0,0 @@
/// The foreign-content namespaces HTML5 switches into.
enum ForeignNamespace {
static let svg = "http://www.w3.org/2000/svg"
static let mathml = "http://www.w3.org/1998/Math/MathML"
}

View File

@ -39,9 +39,7 @@ extension PureXML.HTML {
output += ">"
// A void element has no end tag and no content (the html model);
// anything nested is dropped, as the recursive form did.
if Elements.void.contains(name.lowercased()) {
return
}
if Elements.void.contains(name.lowercased()) { return }
stack.append(HTMLSerializeStep.close(name))
// A raw-text element (script, style) writes its own text children
// verbatim; nested elements still serialize normally by their own
@ -59,9 +57,7 @@ extension PureXML.HTML {
private static func attributeText(_ attribute: PureXML.Model.Attribute) -> String {
let name = attribute.name.description
if attribute.value.isEmpty {
return " \(name)"
}
if attribute.value.isEmpty { return " \(name)" }
// Boolean attributes minimize when the value repeats the name
// (CHECKED="CHECKED" serializes as CHECKED), the html form.
if Elements.booleanAttributes.contains(name.lowercased()), attribute.value.lowercased() == name.lowercased() {
@ -136,9 +132,7 @@ extension PureXML.HTML {
default: result += latin1Entity(scalar) ?? String(Character(scalar))
}
}
if previousWasAmpersand {
result += "&amp;"
}
if previousWasAmpersand { result += "&amp;" }
return result
}

View File

@ -27,18 +27,10 @@ extension PureXML.HTML {
return rawText(until: raw)
}
guard peek() == "<" else { return text() }
if matches("<!--") {
return comment()
}
if matches("<!") {
return doctype()
}
if matches("</") {
return endTag()
}
if peek(1)?.isLetter == true {
return startTag()
}
if matches("<!--") { return comment() }
if matches("<!") { return doctype() }
if matches("</") { return endTag() }
if peek(1)?.isLetter == true { return startTag() }
return text()
}
@ -46,9 +38,7 @@ extension PureXML.HTML {
private mutating func text() -> Token {
var value = ""
if let first = advance() {
value.append(first)
}
if let first = advance() { value.append(first) }
while let character = peek(), character != "<" {
value.append(character)
advance()
@ -63,21 +53,16 @@ extension PureXML.HTML {
var selfClosing = false
while !isAtEnd {
skipSpace()
if consume("/>") {
selfClosing = true
break
}
if consume(">") {
if consume("/>") { selfClosing = true
break
}
if consume(">") { break }
guard let attribute = readAttribute() else { advance()
continue
}
attributes.append(attribute)
}
if Elements.rawText.contains(name) {
rawTextName = name
}
if Elements.rawText.contains(name) { rawTextName = name }
return .startTag(name: name, attributes: attributes, selfClosing: selfClosing)
}
@ -148,13 +133,9 @@ extension PureXML.HTML {
}
value = Self.replacingNull(value)
// RCDATA decodes character references; raw text (script/style) does not.
if Elements.rcdata.contains(name) {
value = Self.decodeEntities(value)
}
if Elements.rcdata.contains(name) { value = Self.decodeEntities(value) }
// A single leading newline after <textarea> is stripped (the HTML rule).
if name == "textarea", value.first == "\n" {
value.removeFirst()
}
if name == "textarea", value.first == "\n" { value.removeFirst() }
return .text(value)
}

View File

@ -55,9 +55,7 @@ extension PureXML.Model.Node: Equatable, Hashable {
stack.append(pair)
}
default:
if !leavesEqual(left, right) {
return false
}
if !leavesEqual(left, right) { return false }
}
}
return true

View File

@ -194,12 +194,8 @@ public extension PureXML.Model {
internalSubset: String = "",
) -> TreeNode {
var attributes: [PureXML.Model.Attribute] = []
if let publicID {
attributes.append(PureXML.Model.Attribute("public", publicID))
}
if let systemID {
attributes.append(PureXML.Model.Attribute("system", systemID))
}
if let publicID { attributes.append(PureXML.Model.Attribute("public", publicID)) }
if let systemID { attributes.append(PureXML.Model.Attribute("system", systemID)) }
return TreeNode(kind: .doctype, name: PureXML.Model.QualifiedName(name), attributes: attributes, value: internalSubset)
}

View File

@ -189,9 +189,7 @@ public extension PureXML.Model.TreeNode {
/// rebuilding each subtree, so a result set of nested nodes is linear rather
/// than quadratic. The projection is identical to ``node``.
func node(memo: inout [ObjectIdentifier: PureXML.Model.Node]) -> PureXML.Model.Node {
if let cached = memo[ObjectIdentifier(self)] {
return cached
}
if let cached = memo[ObjectIdentifier(self)] { return cached }
let result: PureXML.Model.Node = switch kind {
case .document:
.document(Self.projectedChildren(of: self, memo: &memo))

View File

@ -45,9 +45,7 @@ public extension PureXML.Model.TreeNode {
/// comparable only when they share a root; an unrelated node returns false. An
/// ancestor precedes its descendants.
func precedes(_ other: PureXML.Model.TreeNode) -> Bool {
if self === other {
return false
}
if self === other { return false }
let mine = Self.pathFromRoot(self)
let theirs = Self.pathFromRoot(other)
guard mine.first === theirs.first else { return false }
@ -55,12 +53,8 @@ public extension PureXML.Model.TreeNode {
while depth < mine.count, depth < theirs.count, mine[depth] === theirs[depth] {
depth += 1
}
if depth == mine.count {
return true
}
if depth == theirs.count {
return false
}
if depth == mine.count { return true }
if depth == theirs.count { return false }
let parent = mine[depth - 1]
let mineIndex = parent.children.firstIndex { $0 === mine[depth] } ?? 0
let theirsIndex = parent.children.firstIndex { $0 === theirs[depth] } ?? 0

View File

@ -75,9 +75,7 @@ public extension PureXML.Model.TreeNode {
private func isDescendant(of ancestor: PureXML.Model.TreeNode) -> Bool {
var current: PureXML.Model.TreeNode? = self
while let node = current {
if node === ancestor {
return true
}
if node === ancestor { return true }
current = node.parent
}
return false

View File

@ -9,9 +9,7 @@ public extension PureXML.Model.TreeNode {
var current: PureXML.Model.TreeNode? = self
for key in path {
guard let node = current else { return nil }
if key.stringValue.hasPrefix("@") {
return node
}
if key.stringValue.hasPrefix("@") { return node }
let matches = node.children.filter { $0.kind == .element && $0.name?.description == key.stringValue }
let index = (key.intValue ?? 1) - 1
current = matches.indices.contains(index) ? matches[index] : nil

View File

@ -16,9 +16,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append(contentsOf: sequence)
} else {
scalars.append("\u{FFFD}")
if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
}
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
}
} else if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
@ -28,9 +26,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append("\u{FFFD}")
}
}
if lead != 0 {
scalars.append("\u{FFFD}")
}
if lead != 0 { scalars.append("\u{FFFD}") }
return String(scalars)
}

View File

@ -166,9 +166,7 @@ extension PureXML.Parsing {
/// BOM is still a contradiction, only utf-8 itself agrees; the generic
/// utf-16/utf-32 names agree with either byte order.
private static func sameFamily(_ declared: InputEncoding, as sniffed: InputEncoding) -> Bool {
if declared == sniffed {
return true
}
if declared == sniffed { return true }
switch (sniffed, declared) {
case (.utf16BigEndian, .utf16LittleEndian), (.utf16LittleEndian, .utf16BigEndian),
(.utf32BigEndian, .utf32LittleEndian), (.utf32LittleEndian, .utf32BigEndian):
@ -193,9 +191,7 @@ extension PureXML.Parsing {
while index < body.endIndex, body.index(after: index) < body.endIndex {
let high = sniffed == .utf16BigEndian ? body[index] : body[body.index(after: index)]
let low = sniffed == .utf16BigEndian ? body[body.index(after: index)] : body[index]
if high == 0 {
ascii.append(low)
}
if high == 0 { ascii.append(low) }
index = body.index(index, offsetBy: 2)
}
return declaredEncoding(in: ascii)

View File

@ -90,12 +90,8 @@ public extension PureXML.Parsing {
var map: [UInt32: UInt8] = [:]
for byte in UInt8.min ... UInt8.max {
let value = forward(byte).value
if value == 0xFFFD {
continue
}
if map[value] == nil {
map[value] = byte
}
if value == 0xFFFD { continue }
if map[value] == nil { map[value] = byte }
}
return map
}

View File

@ -104,9 +104,7 @@ extension PureXML.Parsing.ByteEncoder {
func record(_ bytes: [UInt8]) {
let scalars = Array(decode(bytes[...]).unicodeScalars)
guard scalars.count == 1, scalars[0].value != 0xFFFD, scalars[0].value >= 0x80 else { return }
if map[scalars[0].value] == nil {
map[scalars[0].value] = bytes
}
if map[scalars[0].value] == nil { map[scalars[0].value] = bytes }
}
for byte in 0x80 ... 0xFF {
record([UInt8(byte)])

View File

@ -25,17 +25,13 @@ extension PureXML.Parsing {
// Each definition starts with required whitespace (the body begins
// right after the element name).
guard scanner.requiredSpace() else { return nil }
if scanner.isAtEnd {
return defaults
} // trailing whitespace
if scanner.isAtEnd { return defaults } // trailing whitespace
guard scanner.name() else { return nil }
guard scanner.requiredSpace() else { return nil }
guard attType(&scanner) else { return nil }
guard scanner.requiredSpace() else { return nil }
guard let value = defaultDecl(&scanner) else { return nil }
if let value {
defaults.append(value)
}
if let value { defaults.append(value) }
}
return defaults
}
@ -45,9 +41,7 @@ extension PureXML.Parsing {
return scanner.nameGroup(nmtokens: true)
}
guard let keyword = scanner.keyword() else { return false }
if tokenizedTypes.contains(keyword) {
return true
}
if tokenizedTypes.contains(keyword) { return true }
if keyword == "NOTATION" {
guard scanner.requiredSpace() else { return false }
return scanner.nameGroup(nmtokens: false)

View File

@ -8,9 +8,7 @@ extension PureXML.Parsing {
enum DTDContentModelGrammar {
static func isValid(_ model: String) -> Bool {
let text = model.trimmingXMLWhitespace()
if text == "EMPTY" || text == "ANY" {
return true
}
if text == "EMPTY" || text == "ANY" { return true }
var scanner = ModelScanner(text)
guard scanner.peek() == "(" else { return false }
// Mixed and children both start with '('; Mixed is identified by
@ -76,9 +74,7 @@ private struct ModelScanner {
var sawAlternative = false
while true {
skipSpace()
if consume(")") {
break
}
if consume(")") { break }
guard consume("|") else { return false }
sawAlternative = true
skipSpace()
@ -108,9 +104,7 @@ private struct ModelScanner {
var connector: Character?
while !consume(")") {
guard let next = advance(), next == "|" || next == "," else { return false }
if let connector, connector != next {
return false
}
if let connector, connector != next { return false }
connector = next
skipSpace()
guard particle() else { return false }

View File

@ -83,9 +83,7 @@ struct DTDScanner {
/// a VC (deferred finding), since the unread external declarations might
/// have declared the entity (production 68).
var entityDeclaredIsWellFormedness: Bool {
if standalone {
return true
}
if standalone { return true }
return doctype.externalSubset == nil && doctype.parameterEntities.isEmpty && unresolvedParameterEntities.isEmpty
}

View File

@ -50,9 +50,7 @@ extension DTDScanner {
} else if reader.matches("]]>") {
reader.consume("]]>")
nesting -= 1
if nesting == 0 {
return body
}
if nesting == 0 { return body }
body += "]]>"
} else if let character = reader.peek() {
body.append(character)
@ -170,9 +168,7 @@ extension DTDScanner {
while index < text.endIndex {
let character = text[index]
if let open = quote {
if character == open {
quote = nil
}
if character == open { quote = nil }
index = text.index(after: index)
continue
}

View File

@ -95,9 +95,7 @@ extension DTDScanner {
if character == "&" || character == "%" {
var probe = index + 1
let isCharacterReference = character == "&" && probe < characters.count && characters[probe] == "#"
if isCharacterReference {
probe += 1
}
if isCharacterReference { probe += 1 }
let bodyStart = probe
let stops: Set<Character> = [";", "&", "%"]
while probe < characters.count, !stops.contains(characters[probe]), !characters[probe].isWhitespace {
@ -239,9 +237,7 @@ extension DTDScanner {
var systemID = ""
if reader.peek()?.isWhitespace == true {
reader.skipSpace()
if let literal = scanLiteral(&reader) {
systemID = literal
}
if let literal = scanLiteral(&reader) { systemID = literal }
}
return ExternalID(publicID: publicID, systemID: systemID, base: currentBase)
}

View File

@ -26,9 +26,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append(scalar)
} else {
scalars.append("\u{FFFD}")
if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
}
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
}
} else if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
@ -38,9 +36,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append("\u{FFFD}")
}
}
if lead != 0 {
scalars.append("\u{FFFD}")
}
if lead != 0 { scalars.append("\u{FFFD}") }
return String(scalars)
}

View File

@ -17,9 +17,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append(scalar)
} else {
scalars.append("\u{FFFD}")
if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
}
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
}
} else if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
@ -29,9 +27,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append("\u{FFFD}")
}
}
if lead != 0 {
scalars.append("\u{FFFD}")
}
if lead != 0 { scalars.append("\u{FFFD}") }
return String(scalars)
}

View File

@ -18,14 +18,7 @@ extension PureXML.Parsing {
budget: inout Int,
at mark: Mark,
) throws -> String {
// A `Character` literal here resolves to `Sequence.contains(_:)`,
// walking extended grapheme clusters to find one ASCII byte; called
// once per ampersand-bearing text node or attribute value in the
// whole document. `utf8.contains(0x26)` finds the same `&` via the
// UTF-8 byte view, since 0x26 is never a lead or continuation byte
// of a multi-byte sequence (matches the reasoning already applied
// to `NamespaceContext.checkNameShape`'s colon check).
guard raw.utf8.contains(0x26) else { return raw }
guard raw.contains("&") else { return raw }
var expander = EntityExpander(entities: entities, mark: mark, budget: budget)
try expander.expand(raw, visiting: [], counts: false)
budget = expander.budget
@ -44,9 +37,7 @@ extension PureXML.Parsing {
at mark: Mark,
undeclared: inout [String],
) throws -> String {
// See `decode(_:entities:budget:at:)`'s matching guard: byte-level
// scan, not a grapheme walk, for the same reason.
guard raw.utf8.contains(0x26) else { return raw }
guard raw.contains("&") else { return raw }
var expander = EntityExpander(entities: entities, mark: mark, budget: budget, undeclared: [])
try expander.expand(raw, visiting: [], counts: false)
budget = expander.budget
@ -126,9 +117,7 @@ extension PureXML.Parsing.EntityDecoder {
/// A tentative `false` recorded before the recursion breaks any cycle (a
/// cyclic entity is an error caught when the reference is expanded).
private static func producesMarkup(_ name: String, entities: [String: String], cache: inout [String: Bool]) -> Bool {
if let cached = cache[name] {
return cached
}
if let cached = cache[name] { return cached }
guard let replacement = entities[name] else {
cache[name] = false
return false

View File

@ -57,13 +57,9 @@ extension PureXML.Parsing {
while let character = scanner.advance() {
switch character {
case "<":
if let reason = scanner.markup(depth: &depth) {
return reason
}
if let reason = scanner.markup(depth: &depth) { return reason }
case "&":
if !scanner.completeReference() {
return "incomplete reference"
}
if !scanner.completeReference() { return "incomplete reference" }
default:
break
}
@ -104,9 +100,7 @@ private struct ReplacementScanner {
private mutating func skip(past terminator: String) -> Bool {
while index < characters.count {
if consume(terminator) {
return true
}
if consume(terminator) { return true }
index += 1
}
return false
@ -132,12 +126,8 @@ private struct ReplacementScanner {
depth -= 1
return depth < 0 ? "an end tag closes an element opened outside the entity" : nil
}
if let reason = attributes() {
return reason
}
if consume("/>") {
return nil
}
if let reason = attributes() { return reason }
if consume("/>") { return nil }
guard consume(">") else { return "malformed start tag" }
depth += 1
return nil
@ -156,16 +146,12 @@ private struct ReplacementScanner {
while true {
skipSpace()
guard let next = peek() else { return "unterminated start tag" }
if next == ">" || next == "/" {
return nil
}
if next == ">" || next == "/" { return nil }
guard name() else { return "malformed attribute name" }
skipSpace()
guard consume("=") else { return "attribute without a value" }
skipSpace()
if let reason = attributeValue() {
return reason
}
if let reason = attributeValue() { return reason }
}
}
@ -174,14 +160,10 @@ private struct ReplacementScanner {
guard let quote = peek(), quote == "\"" || quote == "'" else { return "unquoted attribute value" }
index += 1
while let character = peek(), character != quote {
if character == "<" {
return "'<' in an attribute value"
}
if character == "<" { return "'<' in an attribute value" }
if character == "&" {
index += 1
if !completeReference() {
return "incomplete reference in an attribute value"
}
if !completeReference() { return "incomplete reference in an attribute value" }
continue
}
index += 1
@ -192,9 +174,7 @@ private struct ReplacementScanner {
/// After a consumed `&`: a complete `name;` or `#digits;`.
mutating func completeReference() -> Bool {
var sawBody = false
if peek() == "#" {
index += 1
}
if peek() == "#" { index += 1 }
while let character = peek(), character != ";" {
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameChar) else { return false }
sawBody = true

View File

@ -26,13 +26,6 @@ public extension PureXML.Parsing {
var entityBudget: Int
var pending: [Event] = []
/// Scratch storage for `scanAttributes`' duplicate-name check, hoisted
/// to a stored property so one element's check reuses the previous
/// element's `Set` storage (`removeAll(keepingCapacity: true)`) instead
/// of allocating fresh native storage per element with attributes.
/// Safe to share because `scanAttributes` is not reentrant: nothing
/// inside an attribute scan can start another start-tag scan.
var attributeNamesSeen: Set<String> = []
private var sawRoot = false
private var primed = false
/// When true, ``next()`` repairs malformed input in place instead of
@ -88,16 +81,12 @@ public extension PureXML.Parsing {
guard recovering else {
return open.isEmpty ? try nextAtTopLevel() : try nextInContent()
}
if reachedEnd {
return nil
}
if reachedEnd { return nil }
let start = offset
do {
return open.isEmpty ? try nextAtTopLevel() : try nextInContent()
} catch let error as ParseError {
if let event = recoveredEvent(from: error, startingAt: start) {
return event
}
if let event = recoveredEvent(from: error, startingAt: start) { return event }
}
}
}
@ -164,24 +153,16 @@ public extension PureXML.Parsing {
// spliced entity text, or a non-ASCII lead.
if let byte = reader.peekByte() {
eventStart = reader.mark
if byte == 0x3C {
return try scanContentMarkup()
} // '<'
if let text = try scanText() {
return text
}
if byte == 0x3C { return try scanContentMarkup() } // '<'
if let text = try scanText() { return text }
continue
}
guard let lead = reader.peek() else {
throw ParseError.unexpectedEndOfInput(reader.mark)
}
eventStart = reader.mark
if lead == "<" {
return try scanContentMarkup()
}
if let text = try scanText() {
return text
}
if lead == "<" { return try scanContentMarkup() }
if let text = try scanText() { return text }
}
}
@ -211,6 +192,97 @@ public extension PureXML.Parsing {
return .startElement(name: resolved.name, attributes: resolved.attributes)
}
/// Returns the next text event, or nil after splicing: a reference to
/// an entity whose replacement (directly or transitively) contains
/// markup is validated, budgeted, and pushed back into the reader so
/// it is reparsed as content (4.4.2 Included), elements in replacement
/// text become elements in the stream, not character data.
private mutating func scanText() throws -> Event? {
let mark = reader.mark
var raw = ""
var length = 0
// Whether any '&' was seen: without one the text has no references
// and no markup-entity boundary, so the whole decode/split/findings
// pass is skipped. The scanners visit every byte already, so this
// costs nothing beyond a compare they were doing anyway.
var sawAmpersand = false
while true {
// Bulk byte runs first: plain ASCII content (the bulk of any
// document) skips the per-character machinery entirely; the
// run never spans '<', CR, ']', or invalid bytes, so the
// character path below keeps exact error marks.
if let run = reader.contentRunBytes(sawAmpersand: &sawAmpersand) {
length += run.utf8.count
try checkContent(length, mark)
raw += run
continue
}
// Detect the closing '<' on the byte path so the buffer stays
// empty: a buffered '<' would force the next element's dispatch,
// matches(), and name scan onto the slow Character path.
if reader.peekByte() == 0x3C { break }
guard let character = reader.peek(), character != "<" else { break }
length += 1
try checkContent(length, mark)
if character == ">", raw.hasSuffix("]]") {
throw ParseError.cdataCloseInContent(reader.mark)
}
guard character.unicodeScalars.allSatisfy(XMLCharacter.isChar) else {
throw ParseError.invalidCharacter(reader.mark)
}
if character == "&" { sawAmpersand = true }
raw.append(character)
reader.advance()
}
guard sawAmpersand else { return .characters(raw) }
let split = EntityDecoder.splitAtMarkupEntity(raw, entities: referencableEntities)
if let split {
let replacement = try EntityDecoder.includeForContent(
split.name,
entities: referencableEntities,
budget: &entityBudget,
at: mark,
)
reader.inject(replacement + split.remainder)
let prefix = try decodeReferences(split.prefix, at: mark)
return prefix.isEmpty ? nil : .characters(prefix)
}
let decoded = try decodeReferences(raw, at: mark)
recordReferenceContentFindings(raw: raw, decoded: decoded)
return .characters(decoded)
}
mutating func scanProcessingInstruction() throws -> (target: String, data: String) {
let mark = reader.mark
reader.consume("<?")
let target = try scanName()
// A PI target is an NCName under namespaces (no colon).
if target.description.contains(":") {
throw ParseError.namespaceConstraint(reason: "PI target '\(target.description)' is not a legal NCName", mark)
}
// Production 16: the target and the data must be separated by
// whitespace, so `<?target+++?>` is not well-formed.
if let next = reader.peek(), !next.isWhitespace, !reader.matches("?>") {
throw ParseError.unexpectedCharacter(next, reader.mark)
}
reader.skipSpace()
var data = ""
var length = 0
while !reader.matches("?>") {
guard let character = reader.advance() else {
throw ParseError.unterminatedTag(mark)
}
length += 1
try checkContent(length, mark)
guard character.unicodeScalars.allSatisfy(XMLCharacter.isChar) else {
throw ParseError.invalidCharacter(reader.mark)
}
data.append(character)
}
reader.consume("?>")
return (target.description, data)
}
mutating func scanName() throws -> PureXML.Model.QualifiedName {
let mark = reader.mark
// Fast path: an all-ASCII name scanned directly from the bytes, with

View File

@ -3,20 +3,13 @@
extension PureXML.Parsing.EventReader {
mutating func scanAttributes() throws -> [PureXML.Model.Attribute] {
var attributes: [PureXML.Model.Attribute] = []
// Reuses the stored scratch set (see its declaration) so an element
// does not allocate fresh Set storage just to check its attribute
// names; clearing keeps the capacity. `scanAttributes` cannot re-enter
// itself (nothing inside an attribute scan starts another start tag),
// so the shared storage never sees two interleaved scans.
attributeNamesSeen.removeAll(keepingCapacity: true)
var seen: Set<String> = []
while true {
reader.skipSpace()
// Byte dispatch keeps the buffer empty so scanName() reaches its
// byte fast path; the Character peek is the off-fast-path fallback.
if let byte = reader.peekByte() {
if byte == 0x3E || byte == 0x2F {
return attributes
} // '>' or '/'
if byte == 0x3E || byte == 0x2F { return attributes } // '>' or '/'
} else {
guard let next = reader.peek(), next != ">", next != "/" else {
return attributes
@ -30,7 +23,7 @@ extension PureXML.Parsing.EventReader {
}
reader.skipSpace()
let value = try scanAttributeValue()
guard attributeNamesSeen.insert(name.description).inserted else {
guard seen.insert(name.description).inserted else {
if recovering {
// Keep the element: record the duplicate and drop the later
// occurrence rather than failing the whole start tag.
@ -58,7 +51,11 @@ extension PureXML.Parsing.EventReader {
mutating func scanAttributeValue() throws -> String {
let mark = reader.mark
let (quoteByte, quoteCharacter) = try consumeAttributeValueOpenQuote()
guard let quote = reader.peek(), quote == "\"" || quote == "'" else {
throw PureXML.Parsing.ParseError.unquotedAttributeValue(reader.mark)
}
reader.advance()
let quoteByte: UInt8 = quote == "\"" ? 0x22 : 0x27
var raw = ""
var length = 0
// Whether any '&' was seen: without one the value has no references, so
@ -75,16 +72,7 @@ extension PureXML.Parsing.EventReader {
raw += run
continue
}
// Byte-level check before the Character peek: whenever
// `attributeRunBytes` stopped because the next byte is the closing
// quote (its only non-empty-input stopping reason besides '<', CR,
// or a control/non-ASCII byte the Character path must inspect),
// this avoids buffering that quote as a Character only to compare
// and discard it a moment later.
if reader.peekByte() == quoteByte {
break
}
guard let character = reader.peek(), character != quoteCharacter else { break }
guard let character = reader.peek(), character != quote else { break }
length += 1
try checkContent(length, mark)
if character == "<" {
@ -93,60 +81,18 @@ extension PureXML.Parsing.EventReader {
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else {
throw PureXML.Parsing.ParseError.invalidCharacter(reader.mark)
}
if character == "&" {
sawAmpersand = true
}
if character == "&" { sawAmpersand = true }
// 3.3.3: a literal whitespace character in an attribute value
// normalizes to a space; this happens before reference decoding,
// so whitespace that arrives via a character reference survives.
raw.append(character.isXMLWhitespace ? " " : character)
reader.advance()
}
// The loop above only exits (breaks) when the next position is nil (end
// of input) or the closing quote: there is no third case.
try consumeAttributeValueCloseQuote(byte: quoteByte, character: quoteCharacter)
guard reader.consume(String(quote)) else {
throw PureXML.Parsing.ParseError.unexpectedEndOfInput(reader.mark)
}
// No '&' means no references to decode: the normalized run is the value.
guard sawAmpersand else { return raw }
return try decodeReferences(raw, at: mark)
}
/// Detects and consumes the attribute value's opening quote, returning
/// both its byte and `Character` forms: `quoteByte` drives
/// `attributeRunBytes`'s bulk scan and the closing-quote byte check;
/// `quoteCharacter` is the fallback for wherever the byte fast path does
/// not apply (a streaming source with no owned byte storage, or a
/// lookahead buffer some earlier scan already populated). Byte fast path:
/// `peekByte()` only returns non-nil when the lookahead buffer is empty,
/// so `reader.consume(_:)` below reaches its own byte fast path
/// (`matchesBytes`) directly, with no Character buffering at all.
private mutating func consumeAttributeValueOpenQuote() throws -> (byte: UInt8, character: Character) {
if let byte = reader.peekByte(), byte == 0x22 || byte == 0x27 {
reader.consume(byte == 0x22 ? "\"" : "'")
return (byte, byte == 0x22 ? "\"" : "'")
}
guard let quote = reader.peek(), quote == "\"" || quote == "'" else {
throw PureXML.Parsing.ParseError.unquotedAttributeValue(reader.mark)
}
reader.advance()
return (quote == "\"" ? 0x22 : 0x27, quote)
}
/// Consumes the attribute value's closing quote. Called only once the
/// value-scanning loop has already confirmed (via a peek that consumed
/// nothing) the next position is either the quote or end of input, so
/// whichever check fired that break already buffered nothing new;
/// re-checking the byte first, then falling back to the already-buffered
/// Character peek, reaches the same verdict `reader.consume(String(quote))`
/// would, without that function's fresh `String` allocation and double
/// `Character` iteration on every single attribute value in the document.
private mutating func consumeAttributeValueCloseQuote(byte quoteByte: UInt8, character quoteCharacter: Character) throws {
if reader.peekByte() == quoteByte {
reader.advance()
return
}
guard reader.peek() == quoteCharacter else {
throw PureXML.Parsing.ParseError.unexpectedEndOfInput(reader.mark)
}
reader.advance()
}
}

View File

@ -31,15 +31,9 @@ extension PureXML.Parsing.EventReader {
/// streaming source or spliced entity text). Equivalent to the byte switch
/// in `scanContentMarkup`.
private mutating func scanContentMarkupViaCharacters() throws -> Event {
if reader.matches("</") {
return try scanEndTag()
}
if reader.matches("<?") {
return try scanContentProcessingInstruction()
}
if reader.matches("<!") {
return try scanContentDeclaration()
}
if reader.matches("</") { return try scanEndTag() }
if reader.matches("<?") { return try scanContentProcessingInstruction() }
if reader.matches("<!") { return try scanContentDeclaration() }
return try scanStartTag()
}
@ -52,9 +46,7 @@ extension PureXML.Parsing.EventReader {
recordEmptyElementContent("a comment")
return try scanComment()
}
if reader.matches("<![CDATA[") {
return try scanCDATA()
}
if reader.matches("<![CDATA[") { return try scanCDATA() }
if reader.matches("<!DOCTYPE") {
throw PureXML.Parsing.ParseError.unsupportedDoctype(reader.mark)
}

View File

@ -7,9 +7,7 @@ extension PureXML.Parsing.EventReader {
throw PureXML.Parsing.ParseError.malformedDeclaration(mark)
}
xmlDeclaration = declaration
if declaration.version == "1.1" {
reader.xml11 = true
}
if declaration.version == "1.1" { reader.xml11 = true }
}
/// The current source offset, used to tell whether a failing ``next()`` made
@ -33,9 +31,7 @@ extension PureXML.Parsing.EventReader {
mutating func recover(from offset: Int) -> Bool {
pending.removeAll()
guard reader.peek() != nil else { return false }
if reader.mark.offset > offset {
return true
}
if reader.mark.offset > offset { return true }
reader.advance()
while let character = reader.peek(), character != "<" {
reader.advance()
@ -106,9 +102,7 @@ extension PureXML.Parsing.EventReader {
case let .mismatchedEndTag(_, found, _), let .unexpectedEndTag(found, _):
return closeTo(found)
default:
if !recover(from: start) {
reachedEnd = true
}
if !recover(from: start) { reachedEnd = true }
return nil
}
}

View File

@ -1,98 +0,0 @@
/// Text and processing-instruction scanning, split from the reader body to
/// keep it under the length caps. Behavior is part of EventReader proper.
extension PureXML.Parsing.EventReader {
/// Returns the next text event, or nil after splicing: a reference to
/// an entity whose replacement (directly or transitively) contains
/// markup is validated, budgeted, and pushed back into the reader so
/// it is reparsed as content (4.4.2 Included), elements in replacement
/// text become elements in the stream, not character data.
mutating func scanText() throws -> Event? {
let mark = reader.mark
var raw = ""
var length = 0
// Whether any '&' was seen: without one the text has no references
// and no markup-entity boundary, so the whole decode/split/findings
// pass is skipped. The scanners visit every byte already, so this
// costs nothing beyond a compare they were doing anyway.
var sawAmpersand = false
while true {
// Bulk byte runs first: plain ASCII content (the bulk of any
// document) skips the per-character machinery entirely; the
// run never spans '<', CR, ']', or invalid bytes, so the
// character path below keeps exact error marks.
if let run = reader.contentRunBytes(sawAmpersand: &sawAmpersand) {
length += run.utf8.count
try checkContent(length, mark)
raw += run
continue
}
// Detect the closing '<' on the byte path so the buffer stays
// empty: a buffered '<' would force the next element's dispatch,
// matches(), and name scan onto the slow Character path.
if reader.peekByte() == 0x3C {
break
}
guard let character = reader.peek(), character != "<" else { break }
length += 1
try checkContent(length, mark)
if character == ">", raw.hasSuffix("]]") {
throw ParseError.cdataCloseInContent(reader.mark)
}
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else {
throw ParseError.invalidCharacter(reader.mark)
}
if character == "&" {
sawAmpersand = true
}
raw.append(character)
reader.advance()
}
guard sawAmpersand else { return .characters(raw) }
let split = EntityDecoder.splitAtMarkupEntity(raw, entities: referencableEntities)
if let split {
let replacement = try EntityDecoder.includeForContent(
split.name,
entities: referencableEntities,
budget: &entityBudget,
at: mark,
)
reader.inject(replacement + split.remainder)
let prefix = try decodeReferences(split.prefix, at: mark)
return prefix.isEmpty ? nil : .characters(prefix)
}
let decoded = try decodeReferences(raw, at: mark)
recordReferenceContentFindings(raw: raw, decoded: decoded)
return .characters(decoded)
}
mutating func scanProcessingInstruction() throws -> (target: String, data: String) {
let mark = reader.mark
reader.consume("<?")
let target = try scanName()
// A PI target is an NCName under namespaces (no colon).
if target.description.contains(":") {
throw ParseError.namespaceConstraint(reason: "PI target '\(target.description)' is not a legal NCName", mark)
}
// Production 16: the target and the data must be separated by
// whitespace, so `<?target+++?>` is not well-formed.
if let next = reader.peek(), !next.isWhitespace, !reader.matches("?>") {
throw ParseError.unexpectedCharacter(next, reader.mark)
}
reader.skipSpace()
var data = ""
var length = 0
while !reader.matches("?>") {
guard let character = reader.advance() else {
throw ParseError.unterminatedTag(mark)
}
length += 1
try checkContent(length, mark)
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else {
throw ParseError.invalidCharacter(reader.mark)
}
data.append(character)
}
reader.consume("?>")
return (target.description, data)
}
}

View File

@ -19,9 +19,7 @@ extension PureXML.Parsing.EventReader {
/// it is a WFC when the document is standalone or could not have
/// external declarations, and a validity finding otherwise.
var undeclaredEntityIsFatal: Bool {
if xmlDeclaration?.standalone == true {
return true
}
if xmlDeclaration?.standalone == true { return true }
return documentType.externalSubset == nil && documentType.parameterEntities.isEmpty
}
@ -49,9 +47,7 @@ extension PureXML.Parsing.EventReader {
/// an entity whose stored replacement does (the errata E15h shape: the
/// double-escaped reference surfaces when the replacement is reparsed).
mutating func referencesCharacter(_ raw: String) -> Bool {
if containsCharacterReference(raw) {
return true
}
if containsCharacterReference(raw) { return true }
let entities = referencableEntities
var index = raw.startIndex
while index < raw.endIndex, let amp = raw[index...].firstIndex(of: "&") {
@ -70,9 +66,7 @@ extension PureXML.Parsing.EventReader {
func containsCharacterReference(_ raw: String) -> Bool {
var sawAmp = false
for character in raw {
if sawAmp, character == "#" {
return true
}
if sawAmp, character == "#" { return true }
sawAmp = character == "&"
}
return false
@ -142,9 +136,7 @@ extension PureXML.Parsing.EventReader {
guard isBinding,
let declaration = declarations.first(where: { $0.name == attribute.name.description })
else { return attribute }
if case .cdata = declaration.type {
return attribute
}
if case .cdata = declaration.type { return attribute }
let collapsed = attribute.value.split(separator: " ").joined(separator: " ")
return PureXML.Model.Attribute(name: attribute.name, value: collapsed)
}

View File

@ -71,12 +71,8 @@ extension PureXML.Parsing.ByteDecoder {
/// The scalar for a four-byte pointer through the linear ranges, or nil for
/// the unmapped gap and the out-of-range tail.
static func rangeScalar(_ pointer: Int) -> Unicode.Scalar? {
if pointer > 39419, pointer < 189_000 {
return nil
}
if pointer > 1_237_575 {
return nil
}
if pointer > 39419, pointer < 189_000 { return nil }
if pointer > 1_237_575 { return nil }
let target = UInt32(pointer)
var low = 0
var high = gb18030RangePointers.count - 1

View File

@ -18,9 +18,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append(scalar)
} else {
scalars.append("\u{FFFD}")
if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
}
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) }
}
} else if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
@ -32,9 +30,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append("\u{FFFD}")
}
}
if lead != 0 {
scalars.append("\u{FFFD}")
}
if lead != 0 { scalars.append("\u{FFFD}") }
return String(scalars)
}

View File

@ -19,9 +19,7 @@ extension PureXML.Parsing.ByteDecoder {
}
decodeByte(byte, mode: mode, lead: &lead, into: &scalars)
}
if lead != nil {
scalars.append("\u{FFFD}")
}
if lead != nil { scalars.append("\u{FFFD}") }
return String(scalars)
}

View File

@ -51,21 +51,7 @@ extension PureXML.Parsing {
undo.append(changes)
let resolvedName = try resolveElement(name, at: mark)
// `resolveAttribute` returns its argument unchanged whenever the
// attribute has no prefix or the `xmlns` prefix (the common case:
// most attributes in most documents are unprefixed), so `.map`
// over an all-unprefixed attribute list allocates a fresh backing
// array that is element-wise identical to `attributes`, on every
// single element in the document. Checking first, with no
// allocation, and reusing `attributes` directly when nothing would
// change is the same result for far less work in that common case.
let needsAttributeResolution = attributes.contains { attribute in
let prefix = attribute.name.prefix
return prefix != nil && prefix != "xmlns"
}
let resolvedAttributes = try needsAttributeResolution
? attributes.map { try resolveAttribute($0, at: mark) }
: attributes
let resolvedAttributes = try attributes.map { try resolveAttribute($0, at: mark) }
try checkExpandedNames(resolvedAttributes, at: mark)
return (resolvedName, resolvedAttributes)
}
@ -117,16 +103,7 @@ extension PureXML.Parsing {
/// `:foo`, `a:b:c`, `xmlns:`) in its local name, where the colon
/// betrays it.
private func checkNameShape(_ name: PureXML.Model.QualifiedName, at mark: Mark) throws {
// `":"` as a `Character` literal here resolves to `Sequence.contains(_:)`,
// which walks the string's extended grapheme clusters one at a time
// to compare each against `:`; called once per element and once per
// attribute name in the whole document, that grapheme decoding cost
// is pure overhead for finding one ASCII byte. `utf8.contains(_:)`
// walks the UTF-8 view's `UInt8` elements directly: 0x3A (`:`) is
// never a lead or continuation byte of a multi-byte UTF-8 sequence,
// so a byte-level scan finds every literal colon exactly the
// grapheme scan would, without decoding any of them.
guard name.localName.utf8.contains(0x3A) else { return }
guard name.localName.contains(":") else { return }
throw ParseError.namespaceConstraint(reason: "'\(name.description)' is not a legal qualified name", mark)
}
@ -161,22 +138,6 @@ extension PureXML.Parsing {
/// Attributes must be distinct by expanded name: two prefixes bound to
/// the same URI cannot carry the same local name on one element.
private func checkExpandedNames(_ attributes: [PureXML.Model.Attribute], at mark: Mark) throws {
// The only observable effect below is throwing `duplicateAttribute`,
// which needs two attributes with an equal (URI, localName) pair; an
// unprefixed attribute never carries a URI (the default namespace
// does not apply to attributes) and neither does an `xmlns`/
// `xmlns:*` declaration, so with fewer than two URI-bearing
// attributes no collision is possible. Counting first, with no
// allocation, skips the `Set<String>` storage allocation and the
// per-attribute key string (which, for the built-in `xml`
// namespace URI, is long enough to need its own heap allocation)
// on every element that has at most one namespaced attribute, the
// overwhelming majority in most real documents.
var namespacedCount = 0
for attribute in attributes where attribute.name.namespaceURI != nil {
namespacedCount += 1
}
guard namespacedCount > 1 else { return }
var seen: Set<String> = []
for attribute in attributes where attribute.name.namespaceURI != nil {
let key = (attribute.name.namespaceURI ?? "") + "\u{0}" + attribute.name.localName

View File

@ -30,9 +30,7 @@ extension PureXML.Parsing.XMLDeclaration {
// Pseudo-attributes must be separated by whitespace.
let before = index
skipSpace()
if index == before, index < chars.count {
return nil
}
if index == before, index < chars.count { return nil }
}
return pairs
}

View File

@ -52,17 +52,13 @@ public extension PureXML.Parsing {
case let .close(name):
close(name)
case let .text(value):
if !value.isEmpty {
handler.characters?(value)
}
if !value.isEmpty { handler.characters?(value) }
case let .comment(value):
handler.comment?(value)
case let .cdata(value):
handler.cdata?(value)
case let .processingInstruction(target, data):
if target.lowercased() != "xml" {
handler.processingInstruction?(target, data)
}
if target.lowercased() != "xml" { handler.processingInstruction?(target, data) }
case .ignorable:
break
}

View File

@ -64,9 +64,7 @@ private struct TagCursor {
}
private mutating func advance() {
if index < chars.count {
index += 1
}
if index < chars.count { index += 1 }
}
private mutating func skipSpace() {
@ -133,21 +131,11 @@ extension PureXML.Parsing {
if isPrefix(buffer, ofAny: ["<!--", "<![CDATA[", "<!", "</", "<?"], decidedAt: 2), !final {
return .needMore
}
if startsWith(buffer, "<!--") {
return delimited(buffer, open: 4, close: "-->") { .comment(String($0)) }
}
if startsWith(buffer, "<![CDATA[") {
return delimited(buffer, open: 9, close: "]]>") { .cdata(String($0)) }
}
if startsWith(buffer, "<!") {
return until(buffer, terminator: ">") { _ in .ignorable }
}
if startsWith(buffer, "</") {
return scanEndTag(buffer)
}
if startsWith(buffer, "<?") {
return scanProcessingInstruction(buffer)
}
if startsWith(buffer, "<!--") { return delimited(buffer, open: 4, close: "-->") { .comment(String($0)) } }
if startsWith(buffer, "<![CDATA[") { return delimited(buffer, open: 9, close: "]]>") { .cdata(String($0)) } }
if startsWith(buffer, "<!") { return until(buffer, terminator: ">") { _ in .ignorable } }
if startsWith(buffer, "</") { return scanEndTag(buffer) }
if startsWith(buffer, "<?") { return scanProcessingInstruction(buffer) }
return scanStartTag(buffer, final: final)
}
@ -174,9 +162,7 @@ extension PureXML.Parsing {
guard let end = tagEnd(buffer) else { return final ? .token(.text("<"), consumed: 1) : .needMore }
var inner = Array(buffer[1 ..< end])
let selfClosing = inner.last == "/"
if selfClosing {
inner.removeLast()
}
if selfClosing { inner.removeLast() }
let (name, attributes) = parseTag(inner)
return .token(.open(name: name, attributes: attributes, selfClosing: selfClosing), consumed: end + 1)
}
@ -215,9 +201,7 @@ extension PureXML.Parsing {
for index in 1 ..< buffer.count {
let character = buffer[index]
if let active = quote {
if character == active {
quote = nil
}
if character == active { quote = nil }
} else if character == "\"" || character == "'" {
quote = character
} else if character == ">" {

View File

@ -67,9 +67,7 @@ extension PureXML.Parsing {
// Split multi-scalar graphemes so the buffer is scalar-level.
var queued: [Character] = []
self.pull = {
if !queued.isEmpty {
return queued.removeFirst()
}
if !queued.isEmpty { return queued.removeFirst() }
guard let next = pull() else { return nil }
let scalars = next.unicodeScalars
guard scalars.count > 1 else { return next }
@ -135,14 +133,10 @@ extension PureXML.Parsing {
private mutating func normalizedPull() -> Character? {
guard let character = rawNext() else { return nil }
// Swift coalesces a CR+LF pair into one grapheme-cluster character.
if character == "\r\n" {
return "\n"
}
if character == "\r\n" { return "\n" }
if character == "\r" {
let next = rawNext()
if next != "\n", !(xml11 && next == "\u{85}") {
pendingRaw = next
}
if next != "\n", !(xml11 && next == "\u{85}") { pendingRaw = next }
return "\n"
}
if xml11, character == "\u{85}" || character == "\u{2028}" {
@ -207,9 +201,7 @@ extension PureXML.Parsing {
/// True if the upcoming characters equal `literal`, without consuming.
mutating func matches(_ literal: String) -> Bool {
if buffer.isEmpty, pendingRaw == nil, let storage {
if let byteResult = matchesBytes(literal, storage) {
return byteResult
}
if let byteResult = matchesBytes(literal, storage) { return byteResult }
}
// No per-call Array materialization: literals are short constants
// and this runs on every scan decision (the hottest comparison in
@ -219,9 +211,7 @@ extension PureXML.Parsing {
guard available >= count else { return false }
var index = 0
for character in literal {
if buffer[head + index] != character {
return false
}
if buffer[head + index] != character { return false }
index += 1
}
return true
@ -264,18 +254,10 @@ extension PureXML.Parsing {
let limit = byteIndex + maxLength
while index < storage.count {
let byte = pointer[index]
if byte >= 0x80 {
return nil
}
if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) {
break
}
if index >= limit {
return nil
}
if colon == nil, byte == 0x3A {
colon = index - byteIndex
}
if byte >= 0x80 { return nil }
if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) { break }
if index >= limit { return nil }
if colon == nil, byte == 0x3A { colon = index - byteIndex }
index += 1
}
let length = index - byteIndex
@ -307,9 +289,7 @@ extension PureXML.Parsing {
}
mutating func skipSpace() {
if skipSpaceBytes() {
return
}
if skipSpaceBytes() { return }
while let character = peek(), character.isXMLWhitespace {
advance()
}

View File

@ -22,25 +22,19 @@ extension PureXML.Parsing.Reader {
// the slow path consumed "]]" and the run would begin with '>': leave a
// leading '>' to the slow path and its cdataCloseInContent check (W3C
// ibm14n01).
if byteIndex < count, pointer[byteIndex] == 0x3E {
return nil
}
if byteIndex < count, pointer[byteIndex] == 0x3E { return nil }
var index = byteIndex
var newlines = 0
var lastLineStart = -1
while index < count {
let byte = pointer[index]
if byte == 0x3C {
break
} // '<'
if byte == 0x3C { break } // '<'
// Valid plain ASCII content only; CR, ']' and non-ASCII go to the
// character path.
guard byte < 0x80, byte != 0x0D, byte != 0x5D,
byte >= 0x20 || byte == 0x09 || byte == 0x0A
else {
if index == byteIndex {
return nil
}
if index == byteIndex { return nil }
break
}
if byte == 0x0A {

View File

@ -19,9 +19,7 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append(scalar)
} else {
scalars.append("\u{FFFD}")
if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
} // reprocess ASCII trail
if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) } // reprocess ASCII trail
}
} else if let single = singleByteScalar(byte) {
scalars.append(single)
@ -31,21 +29,15 @@ extension PureXML.Parsing.ByteDecoder {
scalars.append("\u{FFFD}")
}
}
if lead != 0 {
scalars.append("\u{FFFD}")
}
if lead != 0 { scalars.append("\u{FFFD}") }
return String(scalars)
}
/// The scalar for a byte that is not part of a two-byte sequence: ASCII
/// (`0x00`-`0x80`) or half-width katakana (`0xA1`-`0xDF`), else nil.
static func singleByteScalar(_ byte: UInt8) -> Unicode.Scalar? {
if byte <= 0x80 {
return Unicode.Scalar(byte)
}
if (0xA1 ... 0xDF).contains(byte) {
return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1)
}
if byte <= 0x80 { return Unicode.Scalar(byte) }
if (0xA1 ... 0xDF).contains(byte) { return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) }
return nil
}
@ -61,9 +53,7 @@ extension PureXML.Parsing.ByteDecoder {
let offset = lead < 0xA0 ? 0x81 : 0xC1
let trailOffset = trail < 0x7F ? 0x40 : 0x41
let pointer = (Int(lead) - offset) * 188 + Int(trail) - trailOffset
if (8836 ... 10715).contains(pointer) {
return Unicode.Scalar(0xE000 + pointer - 8836)
} // EUDC private use
if (8836 ... 10715).contains(pointer) { return Unicode.Scalar(0xE000 + pointer - 8836) } // EUDC private use
guard pointer >= 0, pointer < jis0208.count, jis0208[pointer] != 0xFFFF else { return nil }
return Unicode.Scalar(UInt32(jis0208[pointer]))
}

View File

@ -47,6 +47,136 @@ extension PureXML.Parsing {
}
}
/// Streams one character from a multi-byte CJK encoding.
private mutating func nextCJK(_ encoding: InputEncoding) -> Character? {
switch encoding {
case .shiftJIS: nextShiftJIS()
case .eucJP: nextEUCJP()
case .eucKR: nextEUCKR()
case .gbk: nextGBK()
case .gb18030: nextGB18030()
case .big5: nextBig5()
case .iso2022jp: nextISO2022JP()
case .eucTW: nextEUCTW()
default: nextUTF8()
}
}
/// Streams one GBK character: ASCII, `0x80` (euro), or a lead+trail.
private mutating func nextGBK() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
if lead == 0x80 { return "\u{20AC}" }
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
}
/// Streams one GB18030 character: ASCII, `0x80` (euro), a four-byte
/// sequence when the lead is followed by a digit, or a two-byte GBK pair.
private mutating func nextGB18030() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
if lead == 0x80 { return "\u{20AC}" }
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let second = nextByte() else { finished = true
return Self.replacement
}
if (0x30 ... 0x39).contains(second) {
guard let third = nextByte(), let fourth = nextByte() else { finished = true
return Self.replacement
}
guard (0x81 ... 0xFE).contains(third), (0x30 ... 0x39).contains(fourth) else { return Self.replacement }
let pointer = PureXML.Parsing.ByteDecoder.GB18030.fourBytePointer(lead, second, third, fourth)
return PureXML.Parsing.ByteDecoder.GB18030.rangeScalar(pointer).map(Character.init) ?? Self.replacement
}
return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: second).map(Character.init) ?? Self.replacement
}
/// Streams one Big5 character: ASCII, or a lead+trail through the index
/// (a two-scalar combining sequence stays one grapheme, one `Character`).
private mutating func nextBig5() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let trail = nextByte() else { finished = true
return Self.replacement
}
guard let sequence = PureXML.Parsing.ByteDecoder.Big5.mapping(lead: lead, trail: trail) else {
return Self.replacement
}
return Character(String(String.UnicodeScalarView(sequence)))
}
/// Streams one EUC-KR character: ASCII, or a lead+trail through CP949.
private mutating func nextEUCKR() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.EUCKR.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
}
/// Streams one EUC-JP character: ASCII, `0x8E`+byte (half-width katakana),
/// `0x8F`+two bytes (JIS X 0212), or a lead+byte through JIS X 0208.
private mutating func nextEUCJP() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
if lead == 0x8E {
guard let byte = nextByte() else { finished = true
return Self.replacement
}
guard (0xA1 ... 0xDF).contains(byte) else { return Self.replacement }
return Character(Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) ?? "\u{FFFD}")
}
if lead == 0x8F {
guard let mid = nextByte(), let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: mid, trail: trail, useJis0212: true).map(Character.init) ?? Self.replacement
}
guard let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: lead, trail: trail, useJis0212: false).map(Character.init) ?? Self.replacement
}
/// Streams one Shift-JIS character: a single byte (ASCII or half-width
/// katakana) or a lead+trail two-byte sequence resolved through JIS X 0208.
private mutating func nextShiftJIS() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) { return Character(single) }
guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement }
guard let trail = nextByte() else {
finished = true
return Self.replacement
}
guard let scalar = PureXML.Parsing.ByteDecoder.ShiftJIS.twoByteScalar(lead: lead, trail: trail) else { return Self.replacement }
return Character(scalar)
}
private mutating func nextUTF32(bigEndian: Bool) -> Character? {
var ordered: [UInt32] = []
for _ in 0 ..< 4 {
@ -56,9 +186,7 @@ extension PureXML.Parsing {
}
ordered.append(UInt32(byte))
}
if !bigEndian {
ordered.reverse()
}
if !bigEndian { ordered.reverse() }
let value = ordered.reduce(UInt32(0)) { ($0 << 8) | $1 }
return Character(Unicode.Scalar(value) ?? "\u{FFFD}")
}
@ -101,7 +229,7 @@ extension PureXML.Parsing {
return pull()
}
mutating func nextUTF8() -> Character? {
private mutating func nextUTF8() -> Character? {
guard let first = nextByte() else {
finished = true
return nil

View File

@ -1,147 +0,0 @@
extension PureXML.Parsing.StreamingDecoder {
/// Streams one character from a multi-byte CJK encoding.
mutating func nextCJK(_ encoding: PureXML.Parsing.InputEncoding) -> Character? {
switch encoding {
case .shiftJIS: nextShiftJIS()
case .eucJP: nextEUCJP()
case .eucKR: nextEUCKR()
case .gbk: nextGBK()
case .gb18030: nextGB18030()
case .big5: nextBig5()
case .iso2022jp: nextISO2022JP()
case .eucTW: nextEUCTW()
default: nextUTF8()
}
}
/// Streams one GBK character: ASCII, `0x80` (euro), or a lead+trail.
private mutating func nextGBK() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F {
return Character(Unicode.Scalar(lead))
}
if lead == 0x80 {
return "\u{20AC}"
}
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
}
/// Streams one GB18030 character: ASCII, `0x80` (euro), a four-byte
/// sequence when the lead is followed by a digit, or a two-byte GBK pair.
private mutating func nextGB18030() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F {
return Character(Unicode.Scalar(lead))
}
if lead == 0x80 {
return "\u{20AC}"
}
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let second = nextByte() else { finished = true
return Self.replacement
}
if (0x30 ... 0x39).contains(second) {
guard let third = nextByte(), let fourth = nextByte() else { finished = true
return Self.replacement
}
guard (0x81 ... 0xFE).contains(third), (0x30 ... 0x39).contains(fourth) else { return Self.replacement }
let pointer = PureXML.Parsing.ByteDecoder.GB18030.fourBytePointer(lead, second, third, fourth)
return PureXML.Parsing.ByteDecoder.GB18030.rangeScalar(pointer).map(Character.init) ?? Self.replacement
}
return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: second).map(Character.init) ?? Self.replacement
}
/// Streams one Big5 character: ASCII, or a lead+trail through the index
/// (a two-scalar combining sequence stays one grapheme, one `Character`).
private mutating func nextBig5() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F {
return Character(Unicode.Scalar(lead))
}
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let trail = nextByte() else { finished = true
return Self.replacement
}
guard let sequence = PureXML.Parsing.ByteDecoder.Big5.mapping(lead: lead, trail: trail) else {
return Self.replacement
}
return Character(String(String.UnicodeScalarView(sequence)))
}
/// Streams one EUC-KR character: ASCII, or a lead+trail through CP949.
private mutating func nextEUCKR() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F {
return Character(Unicode.Scalar(lead))
}
guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
guard let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.EUCKR.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
}
/// Streams one EUC-JP character: ASCII, `0x8E`+byte (half-width katakana),
/// `0x8F`+two bytes (JIS X 0212), or a lead+byte through JIS X 0208.
private mutating func nextEUCJP() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if lead <= 0x7F {
return Character(Unicode.Scalar(lead))
}
if lead == 0x8E {
guard let byte = nextByte() else { finished = true
return Self.replacement
}
guard (0xA1 ... 0xDF).contains(byte) else { return Self.replacement }
return Character(Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) ?? "\u{FFFD}")
}
if lead == 0x8F {
guard let mid = nextByte(), let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: mid, trail: trail, useJis0212: true).map(Character.init) ?? Self.replacement
}
guard let trail = nextByte() else { finished = true
return Self.replacement
}
return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: lead, trail: trail, useJis0212: false).map(Character.init) ?? Self.replacement
}
/// Streams one Shift-JIS character: a single byte (ASCII or half-width
/// katakana) or a lead+trail two-byte sequence resolved through JIS X 0208.
private mutating func nextShiftJIS() -> Character? {
guard let lead = nextByte() else {
finished = true
return nil
}
if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) {
return Character(single)
}
guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement }
guard let trail = nextByte() else {
finished = true
return Self.replacement
}
guard let scalar = PureXML.Parsing.ByteDecoder.ShiftJIS.twoByteScalar(lead: lead, trail: trail) else { return Self.replacement }
return Character(scalar)
}
}

View File

@ -6,9 +6,7 @@ extension PureXML.Parsing.StreamingDecoder {
finished = true
return nil
}
if lead <= 0x7F {
return Character(Unicode.Scalar(lead))
}
if lead <= 0x7F { return Character(Unicode.Scalar(lead)) }
if lead == 0x8E {
guard let plane = nextByte(), let byte1 = nextByte(), let byte2 = nextByte() else {
finished = true

View File

@ -186,9 +186,7 @@ public extension PureXML.Parsing {
applyStartElement(name: name, attributes: attributes)
case let .endElement(name):
currentDepth -= 1
if !langStack.isEmpty {
langStack.removeLast()
}
if !langStack.isEmpty { langStack.removeLast() }
currentName = name
xmlLang = langStack.last ?? nil
set(.endElement, name: name.description, depth: currentDepth)

View File

@ -67,9 +67,7 @@ public extension PureXML.Parsing {
/// `NameChar`: a name character after the first.
public static func isNameChar(_ scalar: Unicode.Scalar) -> Bool {
if isNameStart(scalar) {
return true
}
if isNameStart(scalar) { return true }
switch scalar.value {
case 0x2D, // "-"
0x2E, // "."
@ -88,9 +86,7 @@ public extension PureXML.Parsing {
var scalars = name.unicodeScalars.makeIterator()
guard let first = scalars.next(), isNameStart(first) else { return false }
while let scalar = scalars.next() {
if !isNameChar(scalar) {
return false
}
if !isNameChar(scalar) { return false }
}
return true
}

View File

@ -22,9 +22,7 @@ extension PureXML.Pattern {
case let .prefixWildcard(prefix):
return name.prefix == prefix
case let .name(text):
if text.contains(":") {
return name.description == text
}
if text.contains(":") { return name.description == text }
return name.description == text || name.localName == text
}
}
@ -107,9 +105,7 @@ public extension PureXML.Pattern {
private func ancestorMatches(_ stepIndex: Int, below pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool {
var candidate = pathIndex - 1
while candidate >= 0 {
if matchUpTo(stepIndex, candidate, path) {
return true
}
if matchUpTo(stepIndex, candidate, path) { return true }
candidate -= 1
}
return false

View File

@ -42,12 +42,8 @@ extension PureXML.Pattern {
}
private mutating func leadingGap() -> Gap {
if consume("//") {
return .descendant
}
if consume("/") {
return .root
}
if consume("//") { return .descendant }
if consume("/") { return .root }
return .descendant
}
@ -64,17 +60,11 @@ extension PureXML.Pattern {
private mutating func parseStep(gap: Gap) throws -> RawStep {
guard let character = peek() else { throw PatternError.expectedStep }
if character == "[" {
throw PatternError.unsupported("predicate")
}
if character == "." {
throw PatternError.unsupported(".")
}
if character == "[" { throw PatternError.unsupported("predicate") }
if character == "." { throw PatternError.unsupported(".") }
let isAttribute = consume("@")
let test = try parseTest()
if peek() == "[" {
throw PatternError.unsupported("predicate")
}
if peek() == "[" { throw PatternError.unsupported("predicate") }
return RawStep(gap: gap, isAttribute: isAttribute, test: test)
}
@ -126,9 +116,7 @@ extension PureXML.Pattern {
}
private mutating func advance() {
if index < chars.count {
index += 1
}
if index < chars.count { index += 1 }
}
}
}

View File

@ -29,9 +29,7 @@ public extension PureXML.Pattern.Matcher {
stack.append(name)
collect(stack: stack, attributes: attributes, into: &results)
case .endElement:
if !stack.isEmpty {
stack.removeLast()
}
if !stack.isEmpty { stack.removeLast() }
default:
break
}

View File

@ -30,9 +30,7 @@ extension PureXML.Regex {
next.insert(transition.target)
}
}
if next.isEmpty {
return false
}
if next.isEmpty { return false }
current = closure(next)
}
return current.contains(accept)
@ -106,11 +104,7 @@ extension PureXML.Regex {
var last: Int?
for node in nodes {
let part = fragment(for: node)
if let previous = last {
states[previous].epsilon.append(part.start)
} else {
first = part.start
}
if let previous = last { states[previous].epsilon.append(part.start) } else { first = part.start }
last = part.accept
}
return (first ?? addState(), last ?? addState())
@ -133,9 +127,7 @@ extension PureXML.Regex {
let boundedMin = Swift.min(minimum, Self.repeatUnrollCap)
var unrolled = 0
for _ in 0 ..< boundedMin {
if stateBudgetExhausted {
break
}
if stateBudgetExhausted { break }
let part = fragment(for: node)
states[current].epsilon.append(part.start)
current = part.accept

View File

@ -23,9 +23,7 @@ extension PureXML.Regex {
/// a two-letter category (`Lu`, `Nd`, ...), or an `Is<Block>` block name.
enum CategoryMatcher {
static func isKnown(_ name: String) -> Bool {
if name.hasPrefix("Is") {
return blocks[String(name.dropFirst(2))] != nil
}
if name.hasPrefix("Is") { return blocks[String(name.dropFirst(2))] != nil }
return groups.contains(name) || codeValues.contains(name)
}

View File

@ -20,9 +20,7 @@ extension PureXML.Regex {
}
static func parse(_ pattern: String) throws -> Node {
if pattern.isEmpty {
return .empty
}
if pattern.isEmpty { return .empty }
var parser = RegexParser(pattern)
let node = try parser.parseAlternation()
guard parser.isAtEnd else { throw RegexError.unbalanced }
@ -43,9 +41,7 @@ extension PureXML.Regex {
while let character = peek(), character != "|", character != ")" {
try pieces.append(parsePiece())
}
if pieces.isEmpty {
return .empty
}
if pieces.isEmpty { return .empty }
return pieces.count == 1 ? pieces[0] : .concat(pieces)
}
@ -79,9 +75,7 @@ extension PureXML.Regex {
maximum = peek() == "}" ? nil : parseInteger()
}
guard peek() == "}", let minimum else { throw RegexError.badQuantifier }
if let maximum, minimum > maximum {
throw RegexError.reversedQuantifier
}
if let maximum, minimum > maximum { throw RegexError.reversedQuantifier }
advance()
return .repeated(atom, min: minimum, max: maximum)
}
@ -192,9 +186,7 @@ extension PureXML.Regex {
cls.negated = true
}
// A class must have at least one member; `[]` (or `[^]`) is invalid.
if peek() == "]" {
throw RegexError.emptyClass
}
if peek() == "]" { throw RegexError.emptyClass }
while let character = peek(), character != "]" {
if character == "-", peek(1) == "[" {
advance()
@ -244,19 +236,13 @@ extension PureXML.Regex {
// A nested `[` is legal only as a `-[...]` subtraction, routed by
// parseClass/parseClassMember before reaching here; any other `[`
// is an unescaped class bracket, invalid per Appendix F.
if character == "[" {
throw RegexError.unescapedClassBracket
}
if character == "[" { throw RegexError.unescapedClassBracket }
return .scalar(scalar(of: character))
}
guard let escaped = peek() else { throw RegexError.incompleteEscape }
advance()
if let single = Self.singleEscape(escaped) {
return .scalar(single)
}
if let member = Self.classEscape(escaped) {
return member
}
if let single = Self.singleEscape(escaped) { return .scalar(single) }
if let member = Self.classEscape(escaped) { return member }
if escaped == "p" || escaped == "P" {
return try .category(parseCategory(negated: escaped == "P"))
}
@ -288,9 +274,7 @@ extension PureXML.Regex {
}
private mutating func advance() {
if index < chars.count {
index += 1
}
if index < chars.count { index += 1 }
}
}
}

View File

@ -121,9 +121,7 @@ extension PureXML.Regex {
// removes from that result: `[^cde-[ag]]` is (not c/d/e) minus a/g,
// i.e. excludes {a,c,d,e,g}, not just {c,d,e} (XSTS RegexTest_430).
var hit = negated ? !inBase : inBase
if hit, subtraction.contains(where: { $0.matches(scalar) }) {
hit = false
}
if hit, subtraction.contains(where: { $0.matches(scalar) }) { hit = false }
return hit
}

View File

@ -47,9 +47,7 @@ extension PureXML.Regex {
]
static func generalCategoryCode(_ scalar: Unicode.Scalar) -> String {
if let override = categoryOverrides[scalar.value] {
return override
}
if let override = categoryOverrides[scalar.value] { return override }
guard assignedInUnicode32(scalar.value) else { return "Cn" }
return categoryCodes[scalar.properties.generalCategory] ?? "Cn"
}

View File

@ -92,9 +92,7 @@ public extension PureXML.Schema {
func derives(from other: BuiltinType) -> Bool {
var current: BuiltinType? = self
while let type = current {
if type == other {
return true
}
if type == other { return true }
current = type.derivationBase
}
return false
@ -110,12 +108,8 @@ public extension PureXML.Schema {
}
var primitive: Primitive {
if let primitive = Self.stringLike[self] {
return primitive
}
if Self.integerLike.contains(self) {
return .integer
}
if let primitive = Self.stringLike[self] { return primitive }
if Self.integerLike.contains(self) { return .integer }
return computedPrimitive
}

View File

@ -121,15 +121,9 @@ public extension PureXML.Schema {
return .notAbsent
case let (.notNamespace(name), .enumerated(set)), let (.enumerated(set), .notNamespace(name)):
let hasName = set.contains(name), hasAbsent = set.contains("")
if hasName, hasAbsent {
return .any
} // E1-10 5.1.1
if hasName {
return .notAbsent
} // E1-10 5.1.2
if hasAbsent {
return nil
} // NOT EXPRESSIBLE, E1-10 5.1.3
if hasName, hasAbsent { return .any } // E1-10 5.1.1
if hasName { return .notAbsent } // E1-10 5.1.2
if hasAbsent { return nil } // NOT EXPRESSIBLE, E1-10 5.1.3
return .notNamespace(name) // E1-10 5.1.4
case let (.notAbsent, .enumerated(set)), let (.enumerated(set), .notAbsent):
return set.contains("") ? .any : .notAbsent
@ -137,12 +131,8 @@ public extension PureXML.Schema {
}
private static func unionProcessContents(_ left: ProcessContents, _ right: ProcessContents) -> ProcessContents {
if left == .strict || right == .strict {
return .strict
}
if left == .lax || right == .lax {
return .lax
}
if left == .strict || right == .strict { return .strict }
if left == .lax || right == .lax { return .lax }
return .skip
}
@ -201,12 +191,8 @@ public extension PureXML.Schema {
// When the two differ the intersection is strictly not expressible; keep
// the less strict so a matched attribute is never validated more harshly
// than either source asked for (under-validation, not over-rejection).
if left == right {
return left
}
if left == .skip || right == .skip {
return .skip
}
if left == right { return left }
if left == .skip || right == .skip { return .skip }
return .lax
}
}
@ -238,12 +224,6 @@ public extension PureXML.Schema {
struct Particle: Sendable {
public var occurrenceRange: OccurrenceRange
public var term: Term
/// The declared global-element reference this particle was compiled from, when it
/// came from `<xs:element ref="..."/>`. Substitution-group expansion rewrites the
/// term (an abstract head is dropped, members join a synthetic choice), but XSD 1.0
/// section 3.9.6 defines particle restriction over the DECLARED particles, so the
/// restriction check needs the pre-expansion identity the term no longer carries.
public var declaredReference: PureXML.Model.QualifiedName?
public var minOccurs: Int {
get { occurrenceRange.minimum.clamped(to: Int.max) }
@ -255,16 +235,14 @@ public extension PureXML.Schema {
set { occurrenceRange.maximum = OccurrenceUpper(newValue) }
}
public init(minOccurs: Int = 1, maxOccurs: Int? = 1, term: Term, declaredReference: PureXML.Model.QualifiedName? = nil) {
public init(minOccurs: Int = 1, maxOccurs: Int? = 1, term: Term) {
occurrenceRange = OccurrenceRange(minimum: minOccurs, maximum: maxOccurs)
self.term = term
self.declaredReference = declaredReference
}
public init(occurrenceRange: OccurrenceRange, term: Term, declaredReference: PureXML.Model.QualifiedName? = nil) {
public init(occurrenceRange: OccurrenceRange, term: Term) {
self.occurrenceRange = occurrenceRange
self.term = term
self.declaredReference = declaredReference
}
}
@ -306,9 +284,7 @@ public extension PureXML.Schema {
/// The fixed value, when this is a `fixed` constraint.
public var fixedValue: String? {
if case let .fixed(value) = self {
return value
}
if case let .fixed(value) = self { return value }
return nil
}

View File

@ -23,9 +23,9 @@ public extension PureXML.Schema {
/// The named type table that `ElementType.typeReference` resolves against.
let types: [String: ElementType]
/// Local names of elements declared `nillable="true"`.
let nillableElements: Set<String>
private let nillableElements: Set<String>
/// The `default`/`fixed` value constraint declared on each element name.
let elementConstraints: [String: ValueConstraint]
private let elementConstraints: [String: ValueConstraint]
/// Complex types declared `abstract="true"`, keyed by namespaced identity
/// (`{ns}local`) so same-local-name types in different namespaces stay distinct.
let abstractTypes: Set<String>
@ -79,21 +79,8 @@ public extension PureXML.Schema {
at path: [PureXML.Validation.PathKey] = [],
namespaceBindings inheritedBindings: [String: String] = [:],
) -> [PureXML.Validation.ValidationError] {
validateTagged(element, against: type, at: path, namespaceBindings: inheritedBindings).map(\.error)
}
/// The tagged form of ``validate(_:against:at:namespaceBindings:)``: the same
/// computation, with each finding labeled by the decomposed rule it belongs
/// to. The public, untagged form is a thin `.map(\.error)` over this so
/// behavior and output stay identical for every existing caller.
func validateTagged(
_ element: PureXML.Model.Element,
against type: ComplexType,
at path: [PureXML.Validation.PathKey] = [],
namespaceBindings inheritedBindings: [String: String] = [:],
) -> [XSDFinding] {
let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings)
var errors: [XSDFinding] = []
var errors: [XSDFailure] = []
validateAttributes(element, type, at: path, into: &errors)
// An xsi:nil element keeps its attribute obligations but must have no
// content; its content model is not otherwise checked.
@ -103,15 +90,68 @@ public extension PureXML.Schema {
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
// A simpleContent fixed value is compared in its simple type's value space
// (cvc-elt.5.2.2.1), e.g. "05" equals an xs:int fixed "5".
let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content {
simple
} else {
nil
}
let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content { simple } else { nil }
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
return errors
}
/// The errors from an `xsi:nil="true"` element: rejecting it when the
/// element is not nillable, or when it carries content. Returns nil when
/// the element is not nilled.
func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFailure]? {
let name = element.name.localName
// cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at
// all, whatever its value (so `xsi:nil="false"` is equally forbidden, not
// only `xsi:nil="true"`).
if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) {
return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
}
guard Self.isNil(element) else { return nil }
if Self.hasContent(element) {
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
}
// cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}.
// The matched particle's constraint is authoritative; the by-local-name map
// is a fallback for paths without a particle (e.g. the resolved-type entry).
if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil {
return [XSDFailure(reason: "nil element '\(name)' must not have a fixed value constraint", at: path)]
}
return []
}
/// The text an element's simple content is validated against: its character
/// content, or, when the element is empty, its `default`/`fixed` value (an
/// empty element takes that value, and the value, not the empty string, must
/// satisfy the type). The matched particle's constraint takes precedence over
/// the by-local-name `elementConstraints` map, exactly as the tree path's
/// `validateChild` resolves it.
func effectiveSimpleText(for element: PureXML.Model.Element, particleConstraint: ValueConstraint? = nil) -> String {
let text = Self.rawTextContent(element)
let constraint = particleConstraint ?? elementConstraints[element.name.localName]
return text.isEmpty ? (constraint?.value ?? text) : text
}
/// The error from a `fixed` element value constraint: the element's text
/// must equal the fixed value in the element type's value space when known.
func elementFixedErrors(
_ element: PureXML.Model.Element,
valueType: SimpleType? = nil,
particleFixed: String? = nil,
at path: XSDPath,
) -> [XSDFailure] {
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
// A fixed element's content is its fixed character value: it may not carry
// element children, even under a mixed content type (cvc-elt.5.2.2.2.1).
if !element.children.compactMap(\.element).isEmpty {
return [XSDFailure(reason: "fixed element '\(element.name.localName)' must not have element children", at: path)]
}
let text = Self.rawTextContent(element)
// An empty element takes the fixed value; only present content must equal it.
if text.isEmpty { return [] }
if let valueType, valueType.valueMatches(text, literal: fixed) { return [] }
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
}
/// Validates `element` against any element type at `path`, resolving a
/// `typeReference` through the type table.
public func validate(
@ -119,16 +159,7 @@ public extension PureXML.Schema {
as type: ElementType,
at path: [PureXML.Validation.PathKey] = [],
) -> [PureXML.Validation.ValidationError] {
validateTagged(element, as: type, at: path).map(\.error)
}
/// The tagged form of ``validate(_:as:at:)``. See ``validateTagged(_:against:at:namespaceBindings:)``.
func validateTagged(
_ element: PureXML.Model.Element,
as type: ElementType,
at path: [PureXML.Validation.PathKey] = [],
) -> [XSDFinding] {
var errors: [XSDFinding] = []
var errors: [XSDFailure] = []
let namespaceBindings = Self.namespaceBindings(for: element)
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
return errors
@ -141,21 +172,17 @@ public extension PureXML.Schema {
_ content: ContentType,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) {
let children = element.children.compactMap(\.element)
switch content {
case .empty:
if !children.isEmpty {
errors.append(XSDFinding(.emptyContentForbidsChildren, reason: "element must be empty", at: path))
}
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
rejectText(element, at: path, into: &errors)
case let .simpleContent(type):
if !children.isEmpty {
errors.append(XSDFinding(.simpleContentForbidsChildren, reason: "element must not have children", at: path))
}
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
if let error = type.validate(Self.rawTextContent(element)) {
errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path))
errors.append(XSDFailure(reason: "content: \(error)", at: path))
}
case let .elementOnly(particle):
rejectText(element, at: path, into: &errors)
@ -165,9 +192,9 @@ public extension PureXML.Schema {
}
}
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFinding]) {
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
guard !Self.textContent(element).isEmpty else { return }
errors.append(XSDFinding(.textForbidden, reason: "element must not contain text", at: path))
errors.append(XSDFailure(reason: "element must not contain text", at: path))
}
// MARK: Particles
@ -177,7 +204,7 @@ public extension PureXML.Schema {
children: [PureXML.Model.Element],
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) {
// Locate each content-model violation at the offending child (or the
// missing one), with a recovery hint, rather than one opaque failure,
@ -205,7 +232,7 @@ public extension PureXML.Schema {
_ children: [PureXML.Model.Element],
matched: [PureXML.Schema.MatchedParticle?],
frame: XSDChildValidationFrame,
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) {
let steps = Self.childSteps(children)
for (index, child) in children.enumerated() {
@ -227,13 +254,60 @@ public extension PureXML.Schema {
}
}
func validateChild(
/// Validates a wildcard-matched child by its `processContents`: skip does
/// nothing, lax validates against a global declaration when one exists, and
/// strict requires one.
private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
// A global element declaration is always in the schema's target
// namespace, so an instance element matches one only by full qualified
// name. The namespaced key already covers a no-namespace global (its key
// is `element:{}local`); matching an unqualified instance against a
// namespaced global by bare local name would conflate namespaces, e.g.
// accepting unqualified text where a namespaced `xs:short` is declared.
types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
}
private func validateWildcardChild(
_ child: PureXML.Model.Element,
processContents: ProcessContents,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFailure],
) {
switch processContents {
case .skip:
return
case .lax:
if let declaration = globalElementDeclaration(for: child.name) {
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
case .strict:
if let declaration = globalElementDeclaration(for: child.name) {
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
} else {
errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
}
}
}
/// The type named by a wildcard-matched element's `xsi:type`, when present.
private func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> ElementType? {
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
let type = Self.resolveNamedType(reference, in: types)
else { return nil }
return type
}
private func validateChild(
_ child: PureXML.Model.Element,
against declared: ElementType,
at path: XSDPath,
namespaceBindings: [String: String],
particleConstraint: ValueConstraint? = nil,
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) {
// An instance `xsi:type` overrides the declared type, provided the named
// type exists in the schema. Resolve to the type itself rather than a
@ -241,7 +315,7 @@ public extension PureXML.Schema {
// the declared type can forbid the substitution; an abstract declared
// type requires one.
if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
errors.append(XSDFinding(.xsiTypeSubstitutionValid, xsiError))
errors.append(xsiError)
return
}
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
@ -254,30 +328,26 @@ public extension PureXML.Schema {
// violation. Checked before the nil short-circuit, since an
// xsi:nil element is equally bound by it.
for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) {
errors.append(XSDFinding(
.simpleTypeDisallowedAttribute,
reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'",
at: path,
))
errors.append(XSDFailure(reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", at: path))
}
if let nilErrors = nilErrors(child, fixedValue: particleConstraint?.fixedValue, at: path) {
errors += nilErrors
return
}
if !child.children.compactMap(\.element).isEmpty {
errors.append(XSDFinding(.simpleTypeForbidsChildren, reason: "'\(child.name.localName)' must not have children", at: path))
errors.append(XSDFailure(reason: "'\(child.name.localName)' must not have children", at: path))
}
// 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.rawTextContent(child)
if let error = simple.validate(effectiveSimpleText(for: child, particleConstraint: particleConstraint)) {
errors.append(XSDFinding(.simpleTypeValueValid, reason: "'\(child.name.localName)': \(error)", at: path))
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
}
recordIDs(simple, value: text, at: path)
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
case let .complex(complex):
errors.append(contentsOf: validateTagged(child, against: complex, at: path, namespaceBindings: namespaceBindings))
errors.append(contentsOf: validate(child, against: complex, at: path, namespaceBindings: namespaceBindings))
case .typeReference:
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
@ -290,13 +360,13 @@ public extension PureXML.Schema {
_ child: PureXML.Model.Element,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) {
switch resolveReference(declared) {
case let .unknown(name):
errors.append(XSDFinding(.typeReferenceResolves, reason: "unknown type '\(name)'", at: path))
errors.append(XSDFailure(reason: "unknown type '\(name)'", at: path))
case let .circular(name):
errors.append(XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path))
errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path))
case let .resolved(resolved):
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
}

View File

@ -3,21 +3,21 @@ extension PureXML.Schema.ComplexValidator {
_ element: PureXML.Model.Element,
_ type: PureXML.Schema.ComplexType,
at path: [PureXML.Validation.PathKey],
into errors: inout [XSDFinding],
into errors: inout [PureXML.Validation.ValidationError],
) {
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
for use in type.attributes where !use.prohibited {
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
if let match {
if let error = use.type.validate(match.value) {
errors.append(XSDFinding(.attributeTypeValid, reason: "attribute '\(use.name.localName)': \(error)", at: path))
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)': \(error)", at: path))
}
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) {
errors.append(XSDFinding(.attributeFixedMatches, reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
}
recordIDs(use.type, value: match.value, at: path)
} else if use.required {
errors.append(XSDFinding(.attributeRequiredPresent, reason: "missing required attribute '\(use.name.localName)'", at: path))
errors.append(PureXML.Validation.ValidationError(reason: "missing required attribute '\(use.name.localName)'", at: path))
} else if let constraint = use.valueConstraint {
// An absent attribute takes its `default`/`fixed` value, so a
// defaulted or fixed IDREF/IDREFS must still resolve to a matching
@ -28,8 +28,7 @@ extension PureXML.Schema.ComplexValidator {
}
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
if idTypedAttributeCount(present, type, on: element) > 1 {
errors.append(XSDFinding(
.attributeSingleID,
errors.append(PureXML.Validation.ValidationError(
reason: "an element may carry at most one attribute of type ID",
at: path,
))
@ -46,9 +45,7 @@ extension PureXML.Schema.ComplexValidator {
on element: PureXML.Model.Element,
) -> Int {
present.reduce(into: 0) { count, attribute in
if isIDTyped(attribute, type, on: element) {
count += 1
}
if isIDTyped(attribute, type, on: element) { count += 1 }
}
}
@ -76,9 +73,7 @@ extension PureXML.Schema.ComplexValidator {
use: PureXML.Schema.AttributeUse,
on elementName: PureXML.Model.QualifiedName,
) -> Bool {
if sameName(instance, declared) {
return true
}
if sameName(instance, declared) { return true }
guard use.chameleonUnprefixed,
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
@ -91,7 +86,7 @@ extension PureXML.Schema.ComplexValidator {
element: PureXML.Model.Element,
type: PureXML.Schema.ComplexType,
at path: [PureXML.Validation.PathKey],
into errors: inout [XSDFinding],
into errors: inout [PureXML.Validation.ValidationError],
) {
// A `use="prohibited"` use is excluded from the effective {attribute uses},
// so a prohibited attribute is treated as undeclared and left to the
@ -101,7 +96,7 @@ extension PureXML.Schema.ComplexValidator {
let wildcard = type.attributeWildcard
for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) {
guard let wildcard, wildcard.admits(attribute.name) else {
errors.append(XSDFinding(.attributeDeclaredOrPermitted, reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
errors.append(PureXML.Validation.ValidationError(reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
continue
}
switch wildcard.processContents {
@ -113,8 +108,7 @@ extension PureXML.Schema.ComplexValidator {
}
case .strict:
guard let use = globalAttributeUse(for: attribute.name) else {
errors.append(XSDFinding(
.wildcardAttributeUndeclared,
errors.append(PureXML.Validation.ValidationError(
reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'",
at: path,
))
@ -133,13 +127,13 @@ extension PureXML.Schema.ComplexValidator {
_ attribute: PureXML.Model.Attribute,
use: PureXML.Schema.AttributeUse,
at path: [PureXML.Validation.PathKey],
into errors: inout [XSDFinding],
into errors: inout [PureXML.Validation.ValidationError],
) {
if let error = use.type.validate(attribute.value) {
errors.append(XSDFinding(.wildcardAttributeTypeValid, reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
}
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) {
errors.append(XSDFinding(.wildcardAttributeFixedMatches, reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
}
recordIDs(use.type, value: attribute.value, at: path)
}

View File

@ -4,7 +4,7 @@
extension PureXML.Schema.ComplexValidator {
/// Walks the children through the content automaton, flagging the first child
/// the follow-set rejects, or the missing content when the sequence ends early.
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFinding]) {
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
let matcher = PureXML.Schema.ContentRTNMatcher(PureXML.Schema.ContentRTNBuilder.build(particle))
let steps = Self.childSteps(children)
// Advance one active state-set across the children rather than re-walking
@ -14,23 +14,19 @@ extension PureXML.Schema.ComplexValidator {
for (index, child) in children.enumerated() {
guard let next = matcher.step(current, over: child.name) else {
let allowed = matcher.admissible(from: current)
errors.append(XSDFinding(
.contentModelStructure,
reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))",
at: path + [steps[index]],
))
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]]))
return
}
current = next
}
if !matcher.isComplete(current) {
errors.append(XSDFinding(.contentModelStructure, reason: "content is incomplete\(Self.expectation(matcher.admissible(from: current)))", at: path))
errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(matcher.admissible(from: current)))", at: path))
}
}
/// Locates `all`-group violations: each child that is not an in-bounds member,
/// recovering past it, then each required member that never appeared.
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], groupOptional: Bool = false, at path: XSDPath, into errors: inout [XSDFinding]) {
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], groupOptional: Bool = false, at path: XSDPath, into errors: inout [XSDFailure]) {
var counts = [Int](repeating: 0, count: group.particles.count)
let steps = Self.childSteps(children)
for (index, child) in children.enumerated() {
@ -39,7 +35,7 @@ extension PureXML.Schema.ComplexValidator {
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
return room && Self.memberMatches(member.term, child.name)
}) else {
errors.append(XSDFinding(.contentModelStructure, reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
continue
}
counts[position] += 1
@ -48,12 +44,10 @@ extension PureXML.Schema.ComplexValidator {
// absent (zero occurrences), so its required members are not expected; but
// once any child appears the group is present once and every member's own
// `minOccurs` applies. A required group (`minOccurs="1"`) always enforces.
if groupOptional, children.isEmpty {
return
}
if groupOptional, children.isEmpty { return }
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
if case let .element(name, _, _, _, _, _) = member.term {
errors.append(XSDFinding(.contentModelStructure, reason: "element '\(name.localName)' is required but missing", at: path))
errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path))
}
}
}
@ -98,9 +92,7 @@ extension PureXML.Schema.ComplexValidator {
/// A "; expected a, b" hint naming the elements the automaton accepts next.
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
let names = labels.compactMap { label -> String? in
if case let .name(qualified) = label {
return "<\(qualified.localName)>"
}
if case let .name(qualified) = label { return "<\(qualified.localName)>" }
return nil
}
let unique = Set(names).sorted()

View File

@ -14,24 +14,12 @@ extension PureXML.Schema.ComplexValidator {
at path: [PureXML.Validation.PathKey],
namespaceBindings: [String: String],
) -> PureXML.Validation.ValidationError? {
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
return blocked
}
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
return notDerived
}
if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
return anyTypeSub
}
if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) {
return listUnion
}
if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) {
return abstractType
}
if case let .typeReference(name) = declared {
return abstractTypeError(named: name, child: child, at: path)
}
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked }
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived }
if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return anyTypeSub }
if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion }
if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) { return abstractType }
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
return nil
}
@ -111,9 +99,7 @@ extension PureXML.Schema.ComplexValidator {
case .simple, .complex: declared
case let .typeReference(reference): resolvedDeclaredType(reference)
}
if case let .simple(type)? = resolved {
return type.isAnySimpleType
}
if case let .simple(type)? = resolved { return type.isAnySimpleType }
return false
}
@ -171,9 +157,7 @@ extension PureXML.Schema.ComplexValidator {
/// Whether an element type resolves to a complex type.
static func isComplexType(_ type: PureXML.Schema.ElementType) -> Bool {
if case .complex = type {
return true
}
if case .complex = type { return true }
return false
}
@ -253,17 +237,13 @@ extension PureXML.Schema.ComplexValidator {
for member in members {
// The substitute IS a member: derived with no method of its own, so
// this path is unblocked and the substitution is permitted outright.
if member == substitute {
return false
}
if member == substitute { return false }
guard let toMember = PureXML.Schema.XSDParser.derivationMethods(from: substitute, to: member, typeDerivation) else {
continue
}
anyConfirmedPath = true
// An unblocked path to any member makes the whole substitution valid.
if toMember.isDisjoint(with: blocked) {
return false
}
if toMember.isDisjoint(with: blocked) { return false }
}
// Every confirmed member path is blocked (and at least one exists): bar it.
return anyConfirmedPath
@ -345,12 +325,8 @@ extension PureXML.Schema.ComplexValidator {
/// namespaced backbone and the check stays silent rather than risk a false
/// positive.
static func derivationKey(fromReference reference: String) -> String {
if reference.hasPrefix("type:") {
return String(reference.dropFirst("type:".count))
}
if reference.hasPrefix("{") || reference.contains(":") {
return reference
}
if reference.hasPrefix("type:") { return String(reference.dropFirst("type:".count)) }
if reference.hasPrefix("{") || reference.contains(":") { return reference }
return key(PureXML.Model.QualifiedName(localName: reference, namespaceURI: nil))
}
}

View File

@ -22,9 +22,7 @@ extension PureXML.Schema.ComplexValidator {
return wildcard.processContents
case let .group(group):
for member in group.particles {
if let found = wildcard(in: member.term) {
return found
}
if let found = wildcard(in: member.term) { return found }
}
return nil
case .element:
@ -41,9 +39,7 @@ extension PureXML.Schema.ComplexValidator {
fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) {
switch term {
case let .element(name, type, _, _, _, _):
if let type {
result[key(name)] = type
}
if let type { result[key(name)] = type }
case let .group(group):
for member in group.particles {
collectTypes(member.term, into: &result)
@ -66,9 +62,7 @@ extension PureXML.Schema.ComplexValidator {
fileprivate static func collectValueConstraints(_ term: XSDTerm, into result: inout [String: PureXML.Schema.ValueConstraint]) {
switch term {
case let .element(name, _, _, valueConstraint, _, _):
if let valueConstraint {
result[key(name)] = valueConstraint
}
if let valueConstraint { result[key(name)] = valueConstraint }
case let .group(group):
for member in group.particles {
collectValueConstraints(member.term, into: &result)
@ -206,13 +200,7 @@ extension PureXML.Schema.ComplexValidator {
/// Whether the element has any child element or non-whitespace text.
static func hasContent(_ element: PureXML.Model.Element) -> Bool {
!textContent(element).isEmpty || element.children.contains {
if case .element = $0 {
true
} else {
false
}
}
!textContent(element).isEmpty || element.children.contains { if case .element = $0 { true } else { false } }
}
}
@ -248,9 +236,7 @@ extension PureXML.Schema.ComplexValidator {
/// Resolves a named type reference, including built-in datatypes and ur-types.
static func resolveNamedType(_ name: String, in types: [String: PureXML.Schema.ElementType]) -> PureXML.Schema.ElementType? {
if let next = types[name] {
return next
}
if let next = types[name] { return next }
if name.hasPrefix("type:"), let (uri, local) = parseQualifiedTypeKey(name) {
if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = urOrBuiltinType(named: local) {
return builtin

View File

@ -1,62 +0,0 @@
extension PureXML.Schema.ComplexValidator {
/// The errors from an `xsi:nil="true"` element: rejecting it when the
/// element is not nillable, or when it carries content. Returns nil when
/// the element is not nilled.
func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFinding]? {
let name = element.name.localName
// cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at
// all, whatever its value (so `xsi:nil="false"` is equally forbidden, not
// only `xsi:nil="true"`).
if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) {
return [XSDFinding(.nilAttributeRequiresNillable, reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
}
guard Self.isNil(element) else { return nil }
if Self.hasContent(element) {
return [XSDFinding(.nilElementMustBeEmpty, reason: "nil element '\(name)' must be empty", at: path)]
}
// cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}.
// The matched particle's constraint is authoritative; the by-local-name map
// is a fallback for paths without a particle (e.g. the resolved-type entry).
if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil {
return [XSDFinding(.nilElementNoFixedConstraint, reason: "nil element '\(name)' must not have a fixed value constraint", at: path)]
}
return []
}
/// The text an element's simple content is validated against: its character
/// content, or, when the element is empty, its `default`/`fixed` value (an
/// empty element takes that value, and the value, not the empty string, must
/// satisfy the type). The matched particle's constraint takes precedence over
/// the by-local-name `elementConstraints` map, exactly as the tree path's
/// `validateChild` resolves it.
func effectiveSimpleText(for element: PureXML.Model.Element, particleConstraint: PureXML.Schema.ValueConstraint? = nil) -> String {
let text = Self.rawTextContent(element)
let constraint = particleConstraint ?? elementConstraints[element.name.localName]
return text.isEmpty ? (constraint?.value ?? text) : text
}
/// The error from a `fixed` element value constraint: the element's text
/// must equal the fixed value in the element type's value space when known.
func elementFixedErrors(
_ element: PureXML.Model.Element,
valueType: PureXML.Schema.SimpleType? = nil,
particleFixed: String? = nil,
at path: XSDPath,
) -> [XSDFinding] {
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
// A fixed element's content is its fixed character value: it may not carry
// element children, even under a mixed content type (cvc-elt.5.2.2.2.1).
if !element.children.compactMap(\.element).isEmpty {
return [XSDFinding(.fixedElementNoChildren, reason: "fixed element '\(element.name.localName)' must not have element children", at: path)]
}
let text = Self.rawTextContent(element)
// An empty element takes the fixed value; only present content must equal it.
if text.isEmpty {
return []
}
if let valueType, valueType.valueMatches(text, literal: fixed) {
return []
}
return text == fixed ? [] : [XSDFinding(.fixedElementValueMatches, reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
}
}

View File

@ -17,8 +17,6 @@ public extension PureXML.Schema {
}
}
extension PureXML.Schema.ComplexValidator: PureXML.Validation.Validatable {}
public extension PureXML.Schema.ComplexValidator {
// MARK: Streaming support
@ -38,19 +36,6 @@ public extension PureXML.Schema.ComplexValidator {
}
}
/// The streaming path's document-scoped ID/IDREF check as a composable
/// ``PureXML/Validation`` value, reusing ``idErrors()`` (the same accumulator
/// the tree path's `idIdrefConsistent` rule reads) rather than the streaming
/// driver calling it imperatively outside the framework. The validator is its
/// own subject: by the time this runs (after the whole document has streamed
/// through ``shallowValidity``), its `idTracker` already holds every recorded
/// ID/IDREF.
static var idConsistency: PureXML.Validation.Validation<PureXML.Schema.ComplexValidator, Void> {
.init(description: "Every streamed xs:ID is unique and every streamed xs:IDREF resolves to one") { context in
context.subject.idErrors()
}
}
/// The matched particle's `default`/`fixed` value constraint for a child of
/// `parent`'s content model, keyed by namespaced identity (so same-local-name
/// elements in different types do not collide as in the flat `elementConstraints`).
@ -124,9 +109,7 @@ public extension PureXML.Schema.ComplexValidator {
guard case let .complex(complex) = parent else { return nil }
switch complex.content {
case let .elementOnly(particle), let .mixed(particle):
if Self.elementTypes(in: particle.term)[Self.key(child)] != nil {
return nil
}
if Self.elementTypes(in: particle.term)[Self.key(child)] != nil { return nil }
guard Self.wildcardMatch(for: child, in: particle.term) == .strict,
types[PureXML.Schema.XSDParser.elementDeclarationKey(child)] == nil
else { return nil }
@ -148,9 +131,7 @@ public extension PureXML.Schema.ComplexValidator {
return nil
case let .group(group):
for member in group.particles {
if let match = wildcardMatch(for: name, in: member.term) {
return match
}
if let match = wildcardMatch(for: name, in: member.term) { return match }
}
return nil
}
@ -179,45 +160,28 @@ public extension PureXML.Schema.ComplexValidator {
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
at path: [PureXML.Validation.PathKey] = [],
) -> [PureXML.Validation.ValidationError] {
validateShallowTagged(element, as: type, valueConstraint: valueConstraint, at: path).map(\.error)
}
/// The tagged form of ``validateShallow(_:as:valueConstraint:at:)``. See
/// ``PureXML/Schema/ComplexValidator/validateTagged(_:against:at:namespaceBindings:)``.
private func validateShallowTagged(
_ element: PureXML.Model.Element,
as type: PureXML.Schema.ElementType,
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
at path: [PureXML.Validation.PathKey] = [],
) -> [XSDFinding] {
var errors: [XSDFinding] = []
var errors: [XSDFailure] = []
switch type {
case let .simple(simple):
validateSimpleElement(element, simple, valueConstraint: valueConstraint, at: path, into: &errors)
case let .complex(complex):
validateAttributes(element, complex, at: path, into: &errors)
if let nilErrors = nilErrors(element, at: path) {
return errors + nilErrors
}
if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors }
shallowContent(element, complex.content, at: path, into: &errors)
// A simpleContent fixed value is compared in its simple type's value space
// (cvc-elt.5.2.2.1), matching the tree path's validate(_:against:).
let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content {
simple
} else {
nil
}
let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content { simple } else { nil }
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
case .typeReference:
// The shared resolver also guards a circular chain, which would
// otherwise recurse here without terminating.
switch resolveReference(type) {
case let .unknown(name):
return [XSDFinding(.typeReferenceResolves, reason: "unknown type '\(name)'", at: path)]
return [XSDFailure(reason: "unknown type '\(name)'", at: path)]
case let .circular(name):
return [XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path)]
return [XSDFailure(reason: "circular type reference '\(name)'", at: path)]
case let .resolved(resolved):
return validateShallowTagged(element, as: resolved, valueConstraint: valueConstraint, at: path)
return validateShallow(element, as: resolved, valueConstraint: valueConstraint, at: path)
}
}
return errors
@ -228,21 +192,21 @@ public extension PureXML.Schema.ComplexValidator {
_ simple: PureXML.Schema.SimpleType,
valueConstraint: PureXML.Schema.ValueConstraint? = nil,
at path: XSDPath,
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) {
if let nilErrors = nilErrors(element, fixedValue: valueConstraint?.fixedValue, at: path) {
errors += nilErrors
return
}
if !element.children.compactMap(\.element).isEmpty {
errors.append(XSDFinding(.simpleTypeForbidsChildren, reason: "'\(element.name.localName)' must not have children", at: path))
errors.append(XSDFailure(reason: "'\(element.name.localName)' must not have children", at: path))
}
// An empty element takes its `default`/`fixed` value; that value (not the
// empty string) is what must be valid against the type, matching the tree
// path's validateChild.
let text = Self.rawTextContent(element)
if let error = simple.validate(effectiveSimpleText(for: element, particleConstraint: valueConstraint)) {
errors.append(XSDFinding(.simpleTypeValueValid, reason: "'\(element.name.localName)': \(error)", at: path))
errors.append(XSDFailure(reason: "'\(element.name.localName)': \(error)", at: path))
}
recordIDs(simple, value: text, at: path)
// Compare a present fixed value in the type's value space (e.g. "05" == "5"
@ -251,21 +215,17 @@ public extension PureXML.Schema.ComplexValidator {
errors += elementFixedErrors(element, valueType: simple, particleFixed: valueConstraint?.fixedValue, at: path)
}
private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFinding]) {
private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFailure]) {
let children = element.children.compactMap(\.element)
switch content {
case .empty:
if !children.isEmpty {
errors.append(XSDFinding(.emptyContentForbidsChildren, reason: "element must be empty", at: path))
}
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
rejectText(element, at: path, into: &errors)
case let .simpleContent(type):
if !children.isEmpty {
errors.append(XSDFinding(.simpleContentForbidsChildren, reason: "element must not have children", at: path))
}
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
let text = Self.rawTextContent(element)
if let error = type.validate(text) {
errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path))
errors.append(XSDFailure(reason: "content: \(error)", at: path))
}
recordIDs(type, value: text, at: path)
case let .elementOnly(particle):
@ -276,7 +236,7 @@ public extension PureXML.Schema.ComplexValidator {
}
}
private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFinding]) {
private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
if case let .group(group) = particle.term, group.compositor == .all {
allStructureErrors(group, children: children, groupOptional: particle.occurrenceRange.minimum.isZero, at: path, into: &errors)
} else {

View File

@ -1,48 +0,0 @@
extension PureXML.Schema.ComplexValidator {
/// A global element declaration is always in the schema's target
/// namespace, so an instance element matches one only by full qualified
/// name. The namespaced key already covers a no-namespace global (its key
/// is `element:{}local`); matching an unqualified instance against a
/// namespaced global by bare local name would conflate namespaces, e.g.
/// accepting unqualified text where a namespaced `xs:short` is declared.
func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
}
/// Validates a wildcard-matched child by its `processContents`: skip does
/// nothing, lax validates against a global declaration when one exists, and
/// strict requires one.
func validateWildcardChild(
_ child: PureXML.Model.Element,
processContents: PureXML.Schema.ProcessContents,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFinding],
) {
switch processContents {
case .skip:
return
case .lax:
if let declaration = globalElementDeclaration(for: child.name) {
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
case .strict:
if let declaration = globalElementDeclaration(for: child.name) {
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
} else {
errors.append(XSDFinding(.wildcardElementUndeclared, reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
}
}
}
/// The type named by a wildcard-matched element's `xsi:type`, when present.
func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> PureXML.Schema.ElementType? {
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
let type = Self.resolveNamedType(reference, in: types)
else { return nil }
return type
}
}

View File

@ -7,26 +7,20 @@ extension PureXML.Schema.ComplexValidator {
for child: PureXML.Model.Element,
namespaceBindings: [String: String] = [:],
at path: XSDPath,
into errors: inout [XSDFinding],
into errors: inout [XSDFailure],
) -> PureXML.Schema.ElementType? {
guard Self.xsiTypeAttributeValue(child) != nil else { return declared }
let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings) ?? Self.xsiTypeName(child) ?? ""
if let resolved = Self.resolveNamedType(reference, in: types) {
return resolved
}
if let resolved = Self.resolveNamedType(reference, in: types) { return resolved }
let overriding = Self.xsiTypeName(child) ?? reference
if let builtin = PureXML.Schema.BuiltinType(rawValue: overriding) {
if xsiBuiltinSubstitutes(builtin, for: declared) {
return .simple(PureXML.Schema.SimpleType(base: builtin))
}
errors.append(XSDFinding(
.xsiTypeOverrideResolves,
reason: "xsi:type '\(overriding)' is not validly derived from the declared type of '\(child.name.localName)'",
at: path,
))
errors.append(XSDFailure(reason: "xsi:type '\(overriding)' is not validly derived from the declared type of '\(child.name.localName)'", at: path))
return nil
}
errors.append(XSDFinding(.xsiTypeOverrideResolves, reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path))
errors.append(XSDFailure(reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path))
return nil
}
@ -38,9 +32,7 @@ extension PureXML.Schema.ComplexValidator {
private func xsiBuiltinSubstitutes(_ xsi: PureXML.Schema.BuiltinType, for declared: PureXML.Schema.ElementType) -> Bool {
switch declared {
case let .typeReference(name):
if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") {
return true
}
if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") { return true }
switch resolveReference(declared) {
case let .resolved(resolved): return xsiBuiltinSubstitutes(xsi, for: resolved)
case .unknown, .circular: return false

View File

@ -140,9 +140,7 @@ extension PureXML.Schema {
func admissible(from current: Set<ContentConfiguration>) -> [TermLabel] {
var labels: [TermLabel] = []
for configuration in current where states[configuration.state].target != nil {
if let label = states[configuration.state].label {
labels.append(label)
}
if let label = states[configuration.state].label { labels.append(label) }
}
return labels
}
@ -205,9 +203,7 @@ extension PureXML.Schema {
next.insert(ContentConfiguration(state: target, counters: configuration.counters))
}
}
if next.isEmpty {
return nil
}
if next.isEmpty { return nil }
return closure(next, caps: counterCaps(for: inputLength))
}

View File

@ -63,12 +63,8 @@ public extension PureXML.Schema {
/// foreign annotation content.
private static func forEachComplexType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
let local = PureXML.Schema.XSDNode.localName(node)
if local == "appinfo" || local == "documentation" {
return
}
if local == "complexType" {
visit(node)
}
if local == "appinfo" || local == "documentation" { return }
if local == "complexType" { visit(node) }
for child in PureXML.Schema.XSDNode.elementChildren(node) {
forEachComplexType(child, visit)
}

View File

@ -98,9 +98,7 @@ private struct CompWalk {
var identityConflict: String?
mutating func emit(_ set: [CompItem]) {
if set.count > 1 {
decisionSets.append(set)
}
if set.count > 1 { decisionSets.append(set) }
}
/// Dedupe a decision set (first set or follow set) by particle identity. A repeated
@ -158,17 +156,13 @@ private struct CompWalk {
}
private func labelName(_ label: PureXML.Schema.TermLabel) -> String {
if case let .name(qname) = label {
return qname.localName
}
if case let .name(qname) = label { return qname.localName }
return "any"
}
mutating func summarize(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
let (minimum, maximum) = PureXML.Schema.XSDNode.occurrence(node)
if let maximum, maximum <= 0 {
return CompSummary(nullable: true, first: [], lastFollow: [])
}
if let maximum, maximum <= 0 { return CompSummary(nullable: true, first: [], lastFollow: []) }
var body = bodySummary(node, visiting: visiting)
// A repeat edge folds the body's first into every end-item's follow. Kept in a
// separate method, not inline: `summarize` recurses on nesting depth, so any
@ -233,9 +227,7 @@ private struct CompWalk {
var suffix: [CompItem] = []
for next in parts[(index + 1)...] {
suffix.append(contentsOf: next.first)
if !next.nullable {
break
}
if !next.nullable { break }
}
suffixFirst[index] = suffix
}
@ -247,9 +239,7 @@ private struct CompWalk {
var prefixFirsts: [[CompItem]] = []
for part in parts {
prefixFirsts.append(part.first)
if !part.nullable {
break
}
if !part.nullable { break }
}
// Each trailing end-item carries its internal follow PLUS the suffix that follows
// it inside the sequence. Otherwise an end-item that is also followed internally
@ -263,9 +253,7 @@ private struct CompWalk {
for entry in parts[index].lastFollow {
trailing.append((entry.item, dedupOnly([entry.follow, suffixFirst[index]])))
}
if !parts[index].nullable {
break
}
if !parts[index].nullable { break }
}
return CompSummary(nullable: parts.allSatisfy(\.nullable), first: mergeList(prefixFirsts), lastFollow: mergeLastFollow(trailing))
}
@ -305,12 +293,8 @@ private struct CompWalk {
private mutating func group(_ node: PureXML.Model.TreeNode, visiting: Set<String>) -> CompSummary {
guard let ref = PureXML.Schema.XSDNode.attribute(node, "ref") else { return CompSummary(nullable: true, first: [], lastFollow: []) }
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
if visiting.contains(name) {
return redefineSelfReference(name, visiting: visiting)
}
if let cached = groupCache[name] {
return cached
}
if visiting.contains(name) { return redefineSelfReference(name, visiting: visiting) }
if let cached = groupCache[name] { return cached }
guard let definition = context.groups[name], let model = modelChild(definition)
else { return CompSummary(nullable: true, first: [], lastFollow: []) }
// Summarize the group once: its internal decision sets are context-independent

View File

@ -11,12 +11,8 @@ enum CompConflict {
for item in set {
switch item.label {
case let .name(qname):
if let owner = nameOwner[qname], owner != item.particle {
return qname.localName
}
if nameOwner[qname] == nil {
nameOwner[qname] = item.particle
}
if let owner = nameOwner[qname], owner != item.particle { return qname.localName }
if nameOwner[qname] == nil { nameOwner[qname] = item.particle }
case let .wildcard(card):
wildcards.append((card, item.particle))
}
@ -24,9 +20,7 @@ enum CompConflict {
for (index, wildcard) in wildcards.enumerated() {
// A wildcard conflicts with any name it admits, from a distinct particle,
// and with any other wildcard it intersects, from a distinct particle.
if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) {
return "any"
}
if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) { return "any" }
for other in wildcards[(index + 1)...] where other.particle != wildcard.particle && wildcardsOverlap(wildcard.card, other.card) {
return "any"
}

View File

@ -3,14 +3,10 @@ extension PureXML.Schema.ContentModelDeterminism {
/// complex type's content model, reached through `complexContent` and its
/// `restriction`/`extension`, or nil for simple or empty content.
static func contentModelNode(_ complexType: XSDTree) -> XSDTree? {
if let direct = modelGroupChild(complexType) {
return direct
}
if let direct = modelGroupChild(complexType) { return direct }
guard let complexContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "complexContent") else { return nil }
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
if let model = modelGroupChild(derivation) {
return model
}
if let model = modelGroupChild(derivation) { return model }
}
return nil
}

View File

@ -28,9 +28,7 @@ enum GroupDependencyOrder {
order.append(name)
continue
}
if visited.contains(name) {
continue
}
if visited.contains(name) { continue }
visited.insert(name)
onPath.insert(name)
work.append((name, true))

View File

@ -108,11 +108,7 @@ extension PureXML.Schema {
var nullable = true
for member in particles {
let part = particle(member)
if let previous = last {
addEpsilon(from: previous, to: part.start)
} else {
first = part.start
}
if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start }
last = part.accept
fragmentCounters.append(contentsOf: part.counters)
nullable = nullable && part.nullable

View File

@ -139,9 +139,7 @@ extension PureXML.Schema {
/// The shared fragment for a group definition, compiled once and memoized.
/// Its accept is marked `isReturn`.
private mutating func groupFragment(_ group: PureXML.Schema.Group, id: Int) -> ContentNFAFragment {
if let cached = fragments[id] {
return cached
}
if let cached = fragments[id] { return cached }
// `definitionID` groups are acyclic: XSD forbids a group referencing itself
// except through an element, and the parser's expansion breaks every cycle at
// an element boundary, so a fragment is never referenced during its own
@ -178,11 +176,7 @@ extension PureXML.Schema {
var nullable = true
for member in particles {
let part = particle(member)
if let previous = last {
addEpsilon(from: previous, to: part.start)
} else {
first = part.start
}
if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start }
last = part.accept
fragmentCounters.append(contentsOf: part.counters)
nullable = nullable && part.nullable

View File

@ -109,9 +109,7 @@ extension PureXML.Schema {
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
}
}
if next.isEmpty {
return nil
}
if next.isEmpty { return nil }
position += 1
return closure(next)
}
@ -123,9 +121,7 @@ extension PureXML.Schema {
func admissible(from current: Set<ContentRTNConfiguration>) -> [TermLabel] {
var labels: [TermLabel] = []
for configuration in current where rtn.states[configuration.state].target != nil {
if let label = rtn.states[configuration.state].label {
labels.append(label)
}
if let label = rtn.states[configuration.state].label { labels.append(label) }
}
return labels
}
@ -158,12 +154,8 @@ extension PureXML.Schema {
let state = configuration.state
guard let label = rtn.states[state].label, let target = rtn.states[state].target, label.matches(name) else { continue }
next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss))
if anyState.map({ state < $0 }) ?? true {
anyState = state
}
if case .name = label, namedState.map({ state < $0 }) ?? true {
namedState = state
}
if anyState.map({ state < $0 }) ?? true { anyState = state }
if case .name = label, namedState.map({ state < $0 }) ?? true { namedState = state }
}
guard let chosenState = namedState ?? anyState, !next.isEmpty else {
result.append(contentsOf: Array(repeating: nil, count: names.count - result.count))
@ -198,9 +190,7 @@ extension PureXML.Schema {
while true {
if !pending.isEmpty {
let configuration = pending.removeLast()
if seen.insert(configuration).inserted {
work.append(configuration)
}
if seen.insert(configuration).inserted { work.append(configuration) }
continue
}
guard let configuration = work.popLast() else { break }
@ -213,9 +203,7 @@ extension PureXML.Schema {
for call in state.calls {
let node = internNode(returnState: call.returnTo, savedCounters: configuration.counters, parent: configuration.gss)
let nextConfiguration = ContentRTNConfiguration(state: call.calleeStart, counters: configuration.counters, gss: node)
if seen.insert(nextConfiguration).inserted {
work.append(nextConfiguration)
}
if seen.insert(nextConfiguration).inserted { work.append(nextConfiguration) }
drainPending(into: &seen, work: &work)
}
if state.isReturn, configuration.gss != 0 {
@ -230,9 +218,7 @@ extension PureXML.Schema {
private func drainPending(into seen: inout Set<ContentRTNConfiguration>, work: inout [ContentRTNConfiguration]) {
while let configuration = pending.popLast() {
if seen.insert(configuration).inserted {
work.append(configuration)
}
if seen.insert(configuration).inserted { work.append(configuration) }
}
}

View File

@ -95,9 +95,7 @@ extension PureXML.Schema {
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2), consume(":"),
let second = readFixed(2) else { return false }
guard (0 ... 24).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return false }
if hour == 24, minute != 0 || second != 0 {
return false
}
if hour == 24, minute != 0 || second != 0 { return false }
value.hour = hour
value.minute = minute
value.second = second
@ -109,15 +107,12 @@ extension PureXML.Schema {
}
private mutating func parseTimezone(into value: inout DateTimeValue) -> Bool {
if consume("Z") {
value.offsetMinutes = 0
if consume("Z") { value.offsetMinutes = 0
return true
}
guard peek() == "+" || peek() == "-" else { return true }
let negative = consume("-")
if !negative {
advance()
}
if !negative { advance() }
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2) else { return false }
guard (0 ... 14).contains(hour), (0 ... 59).contains(minute), hour * 60 + minute <= 14 * 60 else {
return false
@ -141,9 +136,7 @@ extension PureXML.Schema {
}
private mutating func advance() {
if index < chars.count {
index += 1
}
if index < chars.count { index += 1 }
}
private mutating func consume(_ literal: String) -> Bool {

View File

@ -65,9 +65,7 @@ extension PureXML.Schema {
}
static func < (lhs: DecimalValue, rhs: DecimalValue) -> Bool {
if lhs.negative != rhs.negative {
return lhs.negative
}
if lhs.negative != rhs.negative { return lhs.negative }
let magnitude = compareMagnitude(lhs, rhs)
return lhs.negative ? magnitude > 0 : magnitude < 0
}
@ -77,9 +75,7 @@ extension PureXML.Schema {
if lhs.integerDigits.count != rhs.integerDigits.count {
return lhs.integerDigits.count < rhs.integerDigits.count ? -1 : 1
}
if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) {
return order
}
if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) { return order }
return lexicographic(lhs.fractionDigits, rhs.fractionDigits) ?? 0
}
@ -89,9 +85,7 @@ extension PureXML.Schema {
for (left, right) in zip(lhs, rhs) where left != right {
return left < right ? -1 : 1
}
if lhs.count == rhs.count {
return nil
}
if lhs.count == rhs.count { return nil }
return lhs.count < rhs.count ? -1 : 1
}
}

View File

@ -36,15 +36,9 @@ extension PureXML.Schema {
}
// P < Q only when s+P < s+Q at every reference; > likewise; equal
// when all four coincide (which holds iff the components are equal).
if sawLess, !sawGreater, !sawEqual {
return .lessThan
}
if sawGreater, !sawLess, !sawEqual {
return .greaterThan
}
if sawEqual, !sawLess, !sawGreater {
return .equal
}
if sawLess, !sawGreater, !sawEqual { return .lessThan }
if sawGreater, !sawLess, !sawEqual { return .greaterThan }
if sawEqual, !sawLess, !sawGreater { return .equal }
return .incomparable
}

View File

@ -35,25 +35,19 @@ extension PureXML.Schema {
extension PureXML.Schema.SimpleType {
/// Whether this is `xs:ID` (or an atomic restriction of it).
var isID: Bool {
if case .atomic = variety {
return base == .id
}
if case .atomic = variety { return base == .id }
return false
}
/// Whether this is `xs:IDREF` (or an atomic restriction of it).
var isIDReference: Bool {
if case .atomic = variety {
return base == .idref
}
if case .atomic = variety { return base == .idref }
return false
}
/// Whether this is `xs:IDREFS` (a list whose item type is `xs:IDREF`).
var isIDReferenceList: Bool {
if case let .list(item) = variety {
return item.base == .idref
}
if case let .list(item) = variety { return item.base == .idref }
return false
}
}

View File

@ -38,9 +38,7 @@ extension PureXML.Schema.XSDParser {
var current: XSDTree? = start
for rawStep in path.split(separator: "/", omittingEmptySubsequences: false) {
let step = String(rawStep).trimmingXMLWhitespace()
if step == "." {
continue
}
if step == "." { continue }
guard isSimpleNameTest(step), let node = current,
let child = directChildElement(named: PureXML.Schema.XSDNode.stripPrefix(step), of: node)
else { return nil }
@ -94,9 +92,7 @@ extension PureXML.Schema.XSDParser {
}
return local == "anyType"
}
if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil {
return false
}
if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil { return false }
if let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") {
return PureXML.Schema.XSDNode.firstChild(complex, named: "simpleContent") == nil
}

View File

@ -13,9 +13,7 @@ extension PureXML.Schema.IdentityValidator {
let fieldType = childElementType(named: field, in: targetComplex.content),
case let .complex(fieldComplex) = fieldType
else { return false }
if case .simpleContent = fieldComplex.content {
return false
}
if case .simpleContent = fieldComplex.content { return false }
return true
}

View File

@ -49,9 +49,7 @@ struct TupleSet {
key.reserveCapacity(tuple.count)
for field in tuple {
guard let field, !field.string.contains(":") else { return nil }
if let type = field.type, !type.comparesRawLexically {
return nil
}
if let type = field.type, !type.comparesRawLexically { return nil }
key.append(field.string)
}
return key

View File

@ -49,22 +49,11 @@ extension PureXML.Schema {
_ root: PureXML.Model.TreeNode,
at path: [PureXML.Validation.PathKey] = [],
) -> [PureXML.Validation.ValidationError] {
validateTagged(root, at: path).map(\.error)
}
/// The tagged form of ``validate(_:at:)``: the same computation, with each
/// finding labeled by the decomposed identity rule it belongs to. The
/// public, untagged form is a thin `.map(\.error)` over this so behavior and
/// output stay identical for every existing caller.
func validateTagged(
_ root: PureXML.Model.TreeNode,
at path: [PureXML.Validation.PathKey] = [],
) -> [XSDFinding] {
guard !constraints.isEmpty else { return [] }
// A selector or field XPath that does not compile is a schema-author
// error, reported once up front rather than silently disabling the
// constraint (the broken query still evaluates as no-match below).
var issues: [XSDFinding] = compileErrors(at: path)
var issues: [PureXML.Validation.ValidationError] = compileErrors(at: path)
walk(root, at: path, scopes: [], into: &issues)
return issues
}
@ -72,19 +61,15 @@ extension PureXML.Schema {
/// One located error per constraint XPath that fails to compile, in a
/// deterministic order. The failed compilations are cached, so the walk
/// afterwards treats those constraints as matching nothing.
private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [XSDFinding] {
var errors: [XSDFinding] = []
private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [PureXML.Validation.ValidationError] {
var errors: [PureXML.Validation.ValidationError] = []
for key in constraints.keys.sorted() {
for constraint in constraints[key] ?? [] {
if cache.query(constraint.selector) == nil {
errors.append(XSDFinding(
.identityConstraintXPathsValid,
reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'",
at: path,
))
errors.append(.init(reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'", at: path))
}
for field in constraint.fields where cache.query(field) == nil {
errors.append(XSDFinding(.identityConstraintXPathsValid, reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path))
errors.append(.init(reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path))
}
}
}
@ -95,7 +80,7 @@ extension PureXML.Schema {
_ node: PureXML.Model.TreeNode,
at path: [PureXML.Validation.PathKey],
scopes: [[String: [[FieldValue?]]]],
into issues: inout [XSDFinding],
into issues: inout [PureXML.Validation.ValidationError],
) {
var frame: [String: [[FieldValue?]]] = [:]
let declared = node.name.flatMap { constraints[$0.localName] } ?? []
@ -147,7 +132,7 @@ extension PureXML.Schema {
at node: PureXML.Model.TreeNode,
path: [PureXML.Validation.PathKey],
frame: inout [String: [[FieldValue?]]],
into issues: inout [XSDFinding],
into issues: inout [PureXML.Validation.ValidationError],
) {
guard isNonRef(constraint) else { return }
var tuples: [[FieldValue?]] = []
@ -160,19 +145,15 @@ extension PureXML.Schema {
}
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
if constraint.kind == .key, tuple.contains(where: { $0 == nil }) {
issues.append(XSDFinding(
.identityKeyFieldPresent,
issues.append(.init(
reason: "key '\(constraint.name)': a field is missing",
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
))
continue
}
if tuple.contains(where: { $0 == nil }) {
continue
}
if tuple.contains(where: { $0 == nil }) { continue }
if seen.contains(tuple) {
issues.append(XSDFinding(
.identityValueUnique,
issues.append(.init(
reason: "\(label(constraint)) '\(constraint.name)': duplicate value",
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
))
@ -189,7 +170,7 @@ extension PureXML.Schema {
at node: PureXML.Model.TreeNode,
path: [PureXML.Validation.PathKey],
scopes: [[String: [[FieldValue?]]]],
into issues: inout [XSDFinding],
into issues: inout [PureXML.Validation.ValidationError],
) {
guard case let .keyref(refer) = constraint.kind else { return }
let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? []
@ -204,12 +185,9 @@ extension PureXML.Schema {
continue
}
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
if tuple.contains(where: { $0 == nil }) {
continue
}
if tuple.contains(where: { $0 == nil }) { continue }
if !keys.contains(tuple) {
issues.append(XSDFinding(
.identityKeyrefResolves,
issues.append(.init(
reason: "keyref '\(constraint.name)': no matching key '\(refer)'",
at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings),
))
@ -225,14 +203,13 @@ extension PureXML.Schema {
_ constraint: IdentityConstraint,
at target: PureXML.Model.TreeNode,
path targetPath: [PureXML.Validation.PathKey],
) -> XSDFinding? {
) -> PureXML.Validation.ValidationError? {
let step = fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings)
for field in constraint.fields {
let selected = select(field, at: target, namespaces: constraint.namespaceBindings)
// cvc-identity-constraint.3: each field must evaluate to at most one node.
if selected.count > 1 {
return XSDFinding(
.identityFieldCardinality,
return .init(
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node",
at: targetPath + step,
)
@ -258,8 +235,7 @@ extension PureXML.Schema {
&& Self.xsiTypeLocalName(of: node) == nil
} ?? false
if element != nil, hasElementChildren || declaredNonSimple || fieldTargetIsComplexNonSimple(field, at: target) {
return XSDFinding(
.identityFieldMustBeSimple,
return .init(
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content",
at: targetPath + step,
)
@ -294,9 +270,7 @@ extension PureXML.Schema {
if let defaulted = defaultedValue(field, value: value, type: type, constraint: constraint, at: node) {
return defaulted
}
if let nodes = value.nodes, nodes.isEmpty {
return nil
}
if let nodes = value.nodes, nodes.isEmpty { return nil }
return FieldValue(string: value.string, type: type ?? Self.effectiveType(of: value), namespaceBindings: instanceNamespaces)
}
}
@ -345,3 +319,69 @@ extension PureXML.Schema {
}
}
}
extension PureXML.Schema.IdentityValidator {
func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? {
switch declaration {
case let .simple(simple): simple
case let .complex(complex):
if case let .simpleContent(simple) = complex.content { simple } else { nil }
case .typeReference: nil
}
}
static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? {
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
let builtin = PureXML.Schema.BuiltinType(rawValue: local)
else { return nil }
return PureXML.Schema.SimpleType(base: builtin)
}
/// The local name of a tree node's `xsi:type` attribute value, recognized by
/// the schema-instance namespace or the conventional `xsi` prefix, or nil.
static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? {
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
guard let attribute = node.attributes.first(where: {
$0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi")
}) else { return nil }
return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value
}
func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String {
constraint.kind == .key ? "key" : "unique"
}
/// Whether a constraint collects tuples (`key`/`unique`) rather than
/// referencing them (`keyref`).
func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool {
if case .keyref = constraint.kind { return false }
return true
}
/// The coding-path steps from the constraint-bearing `container` down to a
/// selected `target`, so an identity error locates the actual offending
/// element rather than the element that merely declares the constraint.
/// Returns an empty path if `target` is not a descendant of `container`.
func steps(from container: PureXML.Model.TreeNode, to target: PureXML.Model.TreeNode) -> [PureXML.Validation.PathKey] {
var chain: [PureXML.Model.TreeNode] = []
var current = target
while current !== container {
guard let parent = current.parent else { return [] }
chain.append(current)
current = parent
}
var steps: [PureXML.Validation.PathKey] = []
var parent = container
for node in chain.reversed() {
let name = node.name?.description ?? ""
if let index = stepIndex.position(of: node, under: parent, name: name) {
steps.append(.element(name, index: index))
} else {
steps.append(.element(name))
}
parent = node
}
return steps
}
}

View File

@ -23,9 +23,7 @@ extension PureXML.Schema.IdentityValidator {
/// character content; any other field is empty only on an empty node-set.
func isFieldEmpty(_ field: String, value: PureXML.XPath.Value) -> Bool {
guard let nodes = value.nodes else { return false }
if field.hasPrefix("@") {
return nodes.isEmpty
}
if field.hasPrefix("@") { return nodes.isEmpty }
if field == "." {
guard case let .tree(element)? = nodes.first, element.kind == .element else { return nodes.isEmpty }
return !element.children.contains { $0.kind == .text || $0.kind == .cdata }
@ -43,9 +41,7 @@ extension PureXML.Schema.IdentityValidator {
) -> PureXML.Schema.ValueConstraint? {
if field == ".", let local = target.name?.localName {
let key = PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local)
if let value = fieldConstraints[key] {
return value
}
if let value = fieldConstraints[key] { return value }
}
return fieldConstraints[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)]
}

View File

@ -1,71 +0,0 @@
extension PureXML.Schema.IdentityValidator {
func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? {
switch declaration {
case let .simple(simple): simple
case let .complex(complex):
if case let .simpleContent(simple) = complex.content {
simple
} else {
nil
}
case .typeReference: nil
}
}
static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? {
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
let builtin = PureXML.Schema.BuiltinType(rawValue: local)
else { return nil }
return PureXML.Schema.SimpleType(base: builtin)
}
/// The local name of a tree node's `xsi:type` attribute value, recognized by
/// the schema-instance namespace or the conventional `xsi` prefix, or nil.
static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? {
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
guard let attribute = node.attributes.first(where: {
$0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi")
}) else { return nil }
return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value
}
func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String {
constraint.kind == .key ? "key" : "unique"
}
/// Whether a constraint collects tuples (`key`/`unique`) rather than
/// referencing them (`keyref`).
func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool {
if case .keyref = constraint.kind {
return false
}
return true
}
/// The coding-path steps from the constraint-bearing `container` down to a
/// selected `target`, so an identity error locates the actual offending
/// element rather than the element that merely declares the constraint.
/// Returns an empty path if `target` is not a descendant of `container`.
func steps(from container: PureXML.Model.TreeNode, to target: PureXML.Model.TreeNode) -> [PureXML.Validation.PathKey] {
var chain: [PureXML.Model.TreeNode] = []
var current = target
while current !== container {
guard let parent = current.parent else { return [] }
chain.append(current)
current = parent
}
var steps: [PureXML.Validation.PathKey] = []
var parent = container
for node in chain.reversed() {
let name = node.name?.description ?? ""
if let index = stepIndex.position(of: node, under: parent, name: name) {
steps.append(.element(name, index: index))
} else {
steps.append(.element(name))
}
parent = node
}
return steps
}
}

Some files were not shown because too many files have changed in this diff Show More