PureXML
Go to file
Mihaela Mihaljevic 4fad612281 feat(schema): XSD derivation control (abstract, block, final, redefine)
Read and enforce the XSD 1.0 derivation controls that were previously
ignored:

- abstract elements may not appear in an instance directly, only through
  a substitution member; an element of an abstract complex type must
  supply an xsi:type naming a concrete derived type.
- block on a type forbids the disallowed xsi:type substitutions; block on
  an element drops substitution-group members that derive by a blocked
  method.
- final on a type rejects, at compile time, any type that derives from it
  by a final method.
- xs:redefine requires the redefined type to derive from itself.

Track each named complex type's base and derivation method as the
backbone these checks walk. Also compose complexContent extension and
restriction content models, which were previously dropped: an extension
now inherits its base's attributes and content (base first, then the
extension, in sequence).

Complex-type restriction is not verified to be a structurally valid
subset of its base (Particle Valid (Restriction)); that limit is
documented at its sites.

Closes #76
2026-06-08 14:14:23 +02:00
.githooks ci: split workflows for truthful badges and attribution gates 2026-06-08 04:37:32 +02:00
.github ci: split workflows for truthful badges and attribution gates 2026-06-08 04:37:32 +02:00
docs/rules ci: split workflows for truthful badges and attribution gates 2026-06-08 04:37:32 +02:00
scripts ci: split workflows for truthful badges and attribution gates 2026-06-08 04:37:32 +02:00
Sources feat(schema): XSD derivation control (abstract, block, final, redefine) 2026-06-08 14:14:23 +02:00
Tests feat(schema): XSD derivation control (abstract, block, final, redefine) 2026-06-08 14:14:23 +02:00
.gitattributes chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
.gitignore chore: extend .gitignore with editor, venv, node, and local-settings entries 2026-06-07 23:35:22 +02:00
.swiftformat feat(xpath): expression engine and the four-type model 2026-06-08 05:33:13 +02:00
.swiftlint.yml feat(parser): accept raw bytes with UTF-8/UTF-16 encoding detection 2026-06-08 00:52:56 +02:00
AGENTS.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
ATTRIBUTION.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
CHANGELOG.md feat(xslt): template parameters and modes 2026-06-08 07:48:05 +02:00
CLAUDE.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
CODE_OF_CONDUCT.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
CONTRIBUTING.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
LICENSE chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
Package.swift chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
README.md docs(roadmap): collapse #92 and #94 to shipped, #71 the open epic 2026-06-08 13:37:21 +02:00
SECURITY.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00
SUPPORT.md chore: scaffold PureXML pure-Swift XML package 2026-06-07 23:20:20 +02:00

PureXML

Style and namespacing Swift macOS Swift Linux Swift Windows Swift WASM License: MIT

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 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.

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):

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

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.

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
bash scripts/check-all.sh

That command expands to:

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.