The validation framework + recovering reader (#92) and editor integration (#94) are complete; they fold into the shipped legend. The beyond-parity libxml2 gaps (#71) are the remaining open epic.
206 lines
9.4 KiB
Markdown
206 lines
9.4 KiB
Markdown
# PureXML
|
||
|
||
[](https://github.com/mihaelamj/PureXML/actions/workflows/style.yml)
|
||
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-macos.yml)
|
||
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-linux.yml)
|
||
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-windows.yml)
|
||
[](https://github.com/mihaelamj/PureXML/actions/workflows/swift-wasm.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://github.com/mihaelamj/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
|
||
LDone --> LReview
|
||
LReview --> LEpic
|
||
LEpic --> LTodo
|
||
```
|
||
|
||
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 plus DTD validation
|
||
(content models, attributes, ID/IDREF) and an XPath 1.0 subset is shipped; the
|
||
remaining libxml2 surface is tracked as epics. 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 remaining work is the depth gaps versus
|
||
libxml2 a read-only conformance audit surfaced (#71):
|
||
|
||
```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
|
||
Gaps["#71 Beyond-parity libxml2 gaps: DTD entities/defaulting (#72 #73), parser breadth (#74), XSD instance/derivation/wildcards/namespaces (#75 #76 #77 #78), RELAX NG + Schematron (#79 #80), XSLT elements/functions (#81 #82), HTML5 full tree (#83 #84), C14N/catalog/XInclude (#85 #86 #87), XPath/XPointer (#88), regex (#89), DOM/serialization (#90 #91)"]:::epic
|
||
```
|
||
|
||
## Status
|
||
|
||
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 **135 tests in 18 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), ISO-8859-1, and Windows-1252
|
||
from raw bytes or streaming byte input.
|
||
- **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`): structural checks (duplicate attributes,
|
||
etc.) plus **DTD validation** against declared content models, attribute lists,
|
||
and ID/IDREF rules (`DTDSchema`, `validateAgainstInternalDTD`). XSD, RELAX NG,
|
||
and Schematron remain open epics (#2, #25).
|
||
- **Emitting** (`PureXML.Emitting`): `Serializer` (save-option parity) and an
|
||
incremental `Writer` (libxml2 `xmlTextWriter`) with compact and pretty modes.
|
||
- **XPath** (`PureXML.XPath`): a **subset** of XPath 1.0 (location paths, common
|
||
axes, node tests, basic predicates). Full XPath 1.0 is epic #21.
|
||
|
||
### 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 toward full libxml2 feature parity
|
||
|
||
**12 of 20 epics are still open**; each now has filed child issues (#4–#60). The
|
||
shipped eight cover the core I/O stack; most remaining work is whole standards:
|
||
|
||
| Open epic | Child issues | What it adds |
|
||
|---|---|---|
|
||
| #1 Push / feed API | #4–#7 | Resumable scanner + push parser |
|
||
| #20 HTML parser | #36–#39 | Tag-soup HTML parse and serialize |
|
||
| #21 Full XPath 1.0 | #40–#43 | Unlocks #22, #23, #25, #3 |
|
||
| #22 xmlPattern | #44–#45 | Streaming pattern match (needs #21) |
|
||
| #23 XPointer | #46–#47 | element() / xpointer() (needs #21) |
|
||
| #24 XInclude | #48–#51 | URI, xml:base, xi:include |
|
||
| #25 Schematron | #52–#53 | Rule validation (needs #21) |
|
||
| #26 C14N | #54–#56 | Canonical XML variants |
|
||
| #27 XML Catalog | #57–#58 | OASIS catalog resolver |
|
||
| #30 → #2 | #59–#60, #8–#13 | Regex engine, then XSD / RELAX NG |
|
||
| #3 XSLT 1.0 | #14–#18 | Stylesheet transform (needs #21) |
|
||
|
||
**40 open enhancement issues** under those epics (plus the 12 epic parents). Critical
|
||
path: **#30 → #2** (schema) and **#21 → #22/#23/#25/#3** (query/transform).
|
||
|
||
## 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 subset 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
|
||
swiftformat . --config .swiftformat --lint
|
||
swiftlint --config .swiftlint.yml --strict
|
||
swift build
|
||
swift test
|
||
```
|
||
|
||
## License
|
||
|
||
MIT.
|