Some checks failed
Style and namespacing / Style and namespacing (push) Failing after 7s
Swift (Linux) / Swift (Linux) (push) Failing after 2s
Swift (macOS) / Swift (macOS) (push) Failing after 6s
Swift (WASM) / Swift (WASM) (push) Failing after 1m4s
XSTS conformance / XSTS conformance (push) Failing after 2s
308 lines
15 KiB
Markdown
308 lines
15 KiB
Markdown
# PureXML
|
|
|
|
[](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=style.yml)
|
|
[](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-macos.yml)
|
|
[](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-linux.yml)
|
|
[](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-windows.yml)
|
|
[](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-wasm.yml)
|
|
[](https://git.aleahim.com/NodetteHQ/PureXML/actions?workflow=swift-xsts.yml)
|
|
[](LICENSE)
|
|
|
|
PureXML is a dependency-free XML package written entirely in Swift.
|
|
|
|
The goal is a Linux-, Windows-, and WebAssembly-compatible XML reader/writer that
|
|
does not pull in `libxml2`, `expat`, or Foundation's `XMLParser`. The package is
|
|
intentionally strict about portability:
|
|
|
|
- no external SwiftPM dependencies
|
|
- no bundled C sources
|
|
- no Foundation requirement in the library target
|
|
- 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
|
|
follows the same structure, rules, and verification gates.
|
|
|
|
## Roadmap
|
|
|
|
The roadmap uses the TileDown Mermaid palette: green for shipped work, yellow for
|
|
review, purple for epic grouping, and gray for open work.
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
|
|
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
|
|
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
|
|
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
|
|
LDone["In main now"]:::done
|
|
LReview["PR in review"]:::review
|
|
LEpic["Epic grouping"]:::epic
|
|
LTodo["Open issue, no PR"]:::todo
|
|
Core["Core shipped: #1 push, #19 SAX, #28 encoding, #29 entities, #31 tree, #32 writer, #33 reader, #34 save, #35 chars"]:::done
|
|
Query["Query + validation shipped: #21 #22 #23 query, #30 regex, #2 XSD/RELAX NG, #25 Schematron"]:::done
|
|
Transform["Transform + HTML shipped: #3 XSLT, #20 HTML, #24 XInclude, #26 C14N, #27 Catalog"]:::done
|
|
Parity["Parity completion shipped: #61 (XSLT keys/output, XSD union/import/identity, RELAX NG compact, HTML5 doc structure, C14N 2.0 trim)"]:::done
|
|
Validator["Validation framework + recovering reader shipped: #92 (one located severity-aware ValidationError, never-crash read)"]:::done
|
|
Editor["Editor integration shipped: #94 (source spans, unified lint, schema completions, structured quick-fixes)"]:::done
|
|
Conformance["Conformance push shipped: DTD #72 #73, XSD #75 #76 #77 #78, parsing #95 #96 #98 #100, regex #89, catalog #86, XSLT #81 #82"]:::done
|
|
LDone --> LReview
|
|
LReview --> LEpic
|
|
LEpic --> LTodo
|
|
LTodo --> Core
|
|
Core --> Query
|
|
Query --> Transform
|
|
Transform --> Parity
|
|
Parity --> Validator
|
|
Validator --> Editor
|
|
Editor --> Conformance
|
|
```
|
|
|
|
The goal is full libxml2 feature parity in pure Swift with zero dependencies, by
|
|
clean-room reimplementation (behavior reproduced from the private PureXML-research
|
|
analysis, no upstream source copied). The streaming core, the schema languages
|
|
(DTD, XSD, RELAX NG, Schematron), XPath 1.0, and the transform/canonicalization
|
|
stack are shipped; the remaining depth gaps are tracked under epic #71. Deliberate
|
|
non-goals: network
|
|
fetching (`nanohttp`/`nanoftp`) and the threading/memory infrastructure stay out,
|
|
and external resolution is opt-in through an injected resolver so XXE stays closed.
|
|
|
|
The original epics, the documented-subset parity work (#61), the validation
|
|
framework and recovering reader (#92), and the editor-integration layer (#94)
|
|
are all shipped (summarized above). The beyond-parity libxml2 conformance audit
|
|
(#71), its parser-breadth sub-epic (#74), and the parity-frontier epic (#105:
|
|
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.
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
|
|
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
|
|
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
|
|
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
|
|
Epic71["#71 beyond-parity libxml2 audit: complete"]:::epic
|
|
Epic74["#74 parser-breadth sub-epic: complete"]:::epic
|
|
Done["All audit children closed in main: DTD #72 #73, XSD #75 #76 #77 #78, parsing #95 #96 #98 #100, regex #89, catalog #86, XSLT #81 #82, RELAX NG #79, Schematron #80, DOM #90, serialization #91, C14N #85, XPath/XPointer #88, XInclude #87, validators #101, HTML5 #83 #84, encodings #97 #99 #102 #103"]:::done
|
|
Epic71 --> Done
|
|
Epic74 --> Done
|
|
Epic105["#105 parity frontier: complete (#106 #107 #108 output encodings, streaming validation, last encodings; findings #109-#119; first W3C pass #120)"]:::epic
|
|
Epic121["#121 xmlconf full archive: complete"]:::epic
|
|
Oasis["#123 OASIS/NIST: zero deviations"]:::done
|
|
Sun["#124 Sun section: complete, zero deviations"]:::done
|
|
IBM["#125 IBM section: clean to two documented classes"]:::done
|
|
Japanese["#126 japanese section: complete, zero deviations"]:::done
|
|
Eduni["#127 eduni: runner + exact baselines, four classified deviations"]:::done
|
|
StrictSubset["#128 strict internal-subset profile: complete, xmltest 185/186"]:::done
|
|
ScalarLex["#135 scalar-level lexing: complete"]:::done
|
|
NSConstraints["#136 namespace constraints: complete"]:::done
|
|
EncodingMismatch["#137 encoding-declaration mismatches: complete"]:::done
|
|
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
|
|
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
|
|
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
|
|
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
|
|
Epic195["#195 xsi:type and substitution-group derivation correctness"]:::epic
|
|
Epic105 --> Epic121
|
|
Epic105 --> Epic122
|
|
Epic121 --> Oasis
|
|
Epic121 --> Sun
|
|
Epic121 --> IBM
|
|
Epic121 --> Japanese
|
|
Epic121 --> Eduni
|
|
Epic121 --> StrictSubset
|
|
Epic121 --> ScalarLex
|
|
Epic121 --> NSConstraints
|
|
Epic121 --> EncodingMismatch
|
|
Epic121 --> BaseURI
|
|
Epic122 --> XSTS
|
|
Epic122 --> XSLTTS
|
|
Epic122 --> RNGTS
|
|
Epic122 --> C14NTS
|
|
Epic122 --> Epic167
|
|
Epic167 --> Oracle171
|
|
Epic167 --> CrossDoc161
|
|
Epic167 --> Gates145
|
|
Epic167 --> Loc169
|
|
Epic167 --> Epic195
|
|
```
|
|
|
|
## 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.
|
|
|
|
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
|
|
currently runs **1448 tests in 258 suites** (`swift test`).
|
|
|
|
### Shipped (libxml2-aligned surface)
|
|
|
|
- **Model** (`PureXML.Model`): `Node`, `Element`, `Attribute`, `QualifiedName`,
|
|
and a mutable `TreeNode` for in-place editing (insert, remove, replace, copy,
|
|
parent navigation).
|
|
- **Parsing** (`PureXML.Parsing`): iterative streaming parser over strings,
|
|
bytes, or incremental character/byte sources. Pull events via `EventReader`,
|
|
SAX2-style callbacks via `SAXHandler`, and a pull cursor via `TextReader`
|
|
(libxml2 `xmlTextReader`). Handles elements, attributes, text, predefined and
|
|
numeric entities, comments, CDATA, and processing instructions. Namespace
|
|
bindings (`xmlns`) are resolved to URIs on parse.
|
|
- **Encoding**: UTF-8/16/32 (with BOM sniffing) from raw bytes or streaming byte
|
|
input, plus the ISO-8859 single-byte family, the Windows code pages
|
|
(1250-1258), KOI8-R/U, and the CJK multi-byte encodings (Shift-JIS,
|
|
EUC-JP/KR/TW, GBK, GB18030, Big5, ISO-2022-JP).
|
|
- **DTD (opt-in)**: `<!DOCTYPE>` is **off by default** (`Limits.allowDoctype:
|
|
false`) to keep XXE and entity-expansion closed. When enabled, the internal
|
|
subset is parsed; internal general entities expand with a bounded cap; external
|
|
entities and external subsets are refused unless an injected
|
|
`EntityResolver` supplies replacement text.
|
|
- **Validation** (`PureXML.Validation`): one composable framework (the OpenAPIKit
|
|
idiom, parameterized over subject and document) backing structural checks, **DTD
|
|
validation** (content models, attribute lists/defaulting, tokenized types,
|
|
ID/IDREF), **XSD** (datatypes, complex types, derivation control, wildcards,
|
|
namespace qualification), **RELAX NG** (XML and compact syntax), and
|
|
**Schematron** (rules, phases, dynamic messages). Errors are located by coding
|
|
path for editor use.
|
|
- **Emitting** (`PureXML.Emitting`): `Serializer` (save options, quote style,
|
|
`xml:space`, line endings) and an incremental, namespace-aware `Writer` (libxml2
|
|
`xmlTextWriter`) with compact and pretty modes.
|
|
- **XPath / XPointer** (`PureXML.XPath`): XPath 1.0 (all axes, the core function
|
|
library, eval-time namespace bindings) and XPointer (`element()`, `xpointer()`,
|
|
`xpath1()`, `xmlns()`).
|
|
|
|
### Consumption modes
|
|
|
|
You choose how the document is consumed; only the tree mode materializes the whole
|
|
document. The input itself never has to be a whole string: every mode below also
|
|
accepts an incremental character or byte source (`pulling:` / `pullingBytes:`), so
|
|
the engine advances step by step with bounded memory and the bytes are pulled on
|
|
demand. The `String` overloads are a convenience for when you already hold the text.
|
|
|
|
- **Pull cursor (step by step)**: `events(_:)` / `events(pulling:)` returns an
|
|
`EventReader`; call `next()` to get one `Event` at a time. `TextReader` is the
|
|
node cursor (the libxml2 `xmlTextReader` model): `read()` advances to the next
|
|
node, you inspect it, `read()` again. Bounded state, no tree.
|
|
- **Push / feed**: `PushParser.feed(_:)`/`finish()` accepts the document in
|
|
arbitrary chunks (a socket, a generator); `events(feeding:)` exposes the same
|
|
over an `AsyncSequence`. It retains only the current incomplete token and the
|
|
open-element stack.
|
|
- **SAX**: `parse(_:sax:)` delivers SAX2-style callbacks, no tree.
|
|
- **Tree**: `parse`/`parseTree` build a `Model.Node` / `TreeNode` tree for
|
|
random access, mutation, validation, XPath, and XSLT. This mode (like libxml2's
|
|
DOM) holds the whole document, because those operations need the full structure.
|
|
|
|
### Safe defaults
|
|
|
|
By default the parser rejects `<!DOCTYPE>` and refuses every external reference
|
|
(`EntityResolver.refusing`). Enable DTD processing only when you need internal
|
|
subset validation or internal entities, and wire a resolver only for trusted,
|
|
in-memory replacements. Network fetching (`nanohttp`/`nanoftp`) and libxml2's
|
|
threading/memory infrastructure are deliberate non-goals.
|
|
|
|
### Remaining work
|
|
|
|
Feature parity with libxml2's documented surface is **complete**: the core I/O
|
|
stack, the query and transform layers, all four schema languages, the validation
|
|
framework, editor integration, and the encoding set (the ISO-8859 family, the
|
|
Windows code pages, and the CJK multi-byte encodings) are shipped. The
|
|
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).
|
|
|
|
## Usage
|
|
|
|
```swift
|
|
import PureXML
|
|
|
|
// Build a tree and emit it (works today).
|
|
let element = PureXML.Model.Element(
|
|
"book",
|
|
attributes: [.init("id", "bk101")],
|
|
children: [.element(.init("title", children: [.text("XML Developer's Guide")]))],
|
|
)
|
|
|
|
let xml = PureXML.serialize(.element(element))
|
|
try PureXML.validate(.element(element))
|
|
|
|
// Parse it back into a tree.
|
|
let node = try PureXML.parse(xml)
|
|
|
|
// Or stream events without building a tree (drives chunked input too).
|
|
var reader = PureXML.events(xml)
|
|
while let event = try reader.next() {
|
|
// handle .startElement / .characters / .endElement / ...
|
|
}
|
|
|
|
// DTD validation (opt-in: allowDoctype + optional resolver for trusted externals).
|
|
let issues = try PureXML.validateAgainstInternalDTD(
|
|
xmlWithDoctype,
|
|
limits: .init(allowDoctype: true),
|
|
)
|
|
|
|
// XPath over a parsed tree.
|
|
let titles = try PureXML.xpath("//title", over: node)
|
|
.compactMap(\.element)
|
|
```
|
|
|
|
## Attribution
|
|
|
|
PureXML is informed by the behavior of established XML parsers (`libxml2`,
|
|
`expat`, Foundation's `XMLParser`) and by the W3C XML 1.0 specification, but it
|
|
does not copy their implementation into `Sources/`. See
|
|
[ATTRIBUTION.md](ATTRIBUTION.md).
|
|
|
|
## Development Contract
|
|
|
|
PureXML must stay dependency-free and portable. Before merging changes:
|
|
|
|
- Swift tools version: 6.1
|
|
- Package products: `PureXML`
|
|
- SwiftPM dependencies: none
|
|
- Hosted CI matrix: macOS, Linux, Windows, and WASM
|
|
|
|
```sh
|
|
bash scripts/check-all.sh
|
|
```
|
|
|
|
That command expands to:
|
|
|
|
```sh
|
|
bash scripts/check-style.sh
|
|
bash scripts/check-namespacing.sh
|
|
bash scripts/check-forbidden-patterns.sh
|
|
bash scripts/check-validation-coverage.sh
|
|
bash scripts/check-validation-fields.sh
|
|
bash scripts/check-changelog-touched.sh
|
|
swiftformat . --config .swiftformat --lint
|
|
swiftlint --config .swiftlint.yml --strict
|
|
swift build
|
|
swift test
|
|
```
|
|
|
|
## License
|
|
|
|
MIT.
|