PureXML
Go to file
Mihaela Mihaljevic b84fdd00ef fix(xslt): no fatal traps on extreme format-number and xsl:number values
format-number of a value at or beyond 2^53 ran an unguarded
Double-to-Int conversion, a fatal trap reachable from user data; a
fraction picture beyond 15 places overflowed the scale conversion the
same way; and xsl:number value= beyond 2^53 trapped identically.
Digit extraction clamps fraction places to 15 (the 2^53 exactness
bound), pads back to the picture's minimum, renders out-of-range
integers through the exponent expander, and xsl:number falls back to
plain number rendering outside [1, 2^53). Sort-key rows always carry
one key per sort. Regression tests cover all three trap paths.
2026-06-10 15:35:36 +02:00
.claude/commands feat(parsing): streaming honors the declared encoding; streaming ISO-2022-JP 2026-06-09 17:33:52 +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 chore(ci): add the changelog-touched gate verification.md requires 2026-06-08 17:49:21 +02:00
Sources fix(xslt): no fatal traps on extreme format-number and xsl:number values 2026-06-10 15:35:36 +02:00
Tests fix(xslt): no fatal traps on extreme format-number and xsl:number values 2026-06-10 15:35:36 +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 fix(xslt): no fatal traps on extreme format-number and xsl:number values 2026-06-10 15:35:36 +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 fix(xslt): no fatal traps on extreme format-number and xsl:number values 2026-06-10 15:35:36 +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
  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 corpus-clean parity, tracked under two epics: #121 (the rest of the W3C xmlconf archive, including the DTD-validity layer) and #122 (the official spec suites for the schema and transform stack).

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 official spec suites"]:::epic
  XSTS["#129 W3C XSD test suite (XSTS)"]:::todo
  XSLTTS["#130 XSLT 1.0 suite (xalan-test): runner + baselines in, burning down"]:::review
  RNGTS["#131 RELAX NG spec suite: complete, one documented class"]:::done
  C14NTS["#132 C14N spec vectors: complete"]:::done
  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

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 966 tests in 166 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): 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 toward full libxml2 feature parity

The original parity epics (the core I/O stack, query, transform, the schema languages, the validation framework, and editor integration) are all shipped. The remaining work is the depth gaps a read-only conformance audit surfaced, tracked under epic #71 (and its parser-breadth sub-epic #74):

Open work Issues What it adds
XSLT 1.0 breadth #81 #82 Missing top-level elements and functions, html output
HTML5 #83 #84 Full tree construction, tokenizer states, entity table
C14N #85 Node-subset canonicalization, xml:* inheritance, 2.0 prefix rewrite
Legacy encodings #97 #99 #102 #103 Single-byte, CJK multi-byte, full GB18030, and Big5 to-Unicode tables
Validator decomposition #101 Inner XSD/DTD validators as composable rules
Partial follow-ups #79 #80 #86 #87 #88 #90 #91 Remaining bullets on already-advanced areas

Each open issue carries a checklist of done vs remaining bullets. The encoding tables (#97, #99) are deferred until they can be vendored from authoritative Unicode mapping files rather than transcribed.

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