Compile and evaluate full XPath 1.0 expressions, not just location paths.
Add the four-type Value (node-set, boolean, number, string) with the spec's
coercion rules and canonical number formatting, a recursive-descent parser
for the operator grammar (or/and, equality, relational, additive,
multiplicative, unary minus, union) whose grammatical position disambiguates
* and the operator keywords, function calls, variable references, and filter
expressions. Predicates are now arbitrary expressions (a numeric predicate
is a position test). Comparison follows the node-set quantification rules.
Query gains value/number/string/boolean accessors and a $variable binding
map; the core functions last, position, count, not, true, false, boolean,
number, and string ship with it.
Closes#42
Rebuild the XPath engine on the parent-aware tree. Add an XPath node
abstraction (tree, attribute, namespace) with document order, and navigate
all thirteen axes: child, descendant, parent, ancestor, following-sibling,
preceding-sibling, following, preceding, attribute, namespace, self,
descendant-or-self, ancestor-or-self. Support the ./../@/// abbreviations
and processing-instruction() node tests. Reverse axes number nearest-first
for positional predicates; results return de-duplicated in document order.
The attribute axis excludes xmlns declarations; the namespace axis surfaces
in-scope namespace nodes.
Closes#40
Replace monolithic ci.yml with per-platform workflows so each README badge
tracks one job. Scan commit messages for tool-agnostic AI attribution in CI,
sync local hooks to the canon, and refresh README status plus git-discipline Rule 8.
Add PureXML.Parsing.TextReader (via PureXML.reader), a pull cursor over the
streaming core. read() advances node by node, exposing nodeKind, name,
value, depth, attributes, and isEmptyElement. A childless element is
reported once as an empty element, normalizing <a/> and <a></a>. The
documentType accessor surfaces the DTD read so far as the validation hook.
Closes#33
Add PureXML.Model.TreeNode, a reference-type document tree that knows its
parent and siblings. Navigation goes up and sideways (parent, nextSibling,
previousSibling, ancestors, root, elementChildren, stringValue) and editing
is in place (append, insert before/after, removeFromParent, replace, copy).
Children are held strongly and the parent weakly; attaching a node detaches
it from any prior parent and a node can never become its own ancestor.
parseTree builds one from XML; TreeNode.node converts back for serialization.
Closes#31
Add internal parameter entities (parsed, stored, and expanded within the
DTD, including bare %name; references that inject markup declarations,
bounded by depth and the expansion budget). Record external general
entities and the external subset identifier, and load them only through an
injected EntityResolver (a struct of closures). The default refusing
resolver fetches nothing, so XXE stays closed: an external entity fails as
undefined and the external subset is left unread. Internal declarations win
over the external subset.
Closes#29
Add includeXMLDeclaration, xmlVersion, encodingName, and standalone to
Emitting.Options, with a single shared xmlDeclaration computation consumed
by both the tree serializer and the incremental writer. Writer gains
writeStartDocument(). Defaults stay off so fragment output is unchanged.
Closes#34
Add PureXML.Parsing.XMLCharacter as the single source of truth for character
classes, implementing the exact XML 1.0 (Fifth Edition) Char, NameStartChar, and
NameChar productions as public scalar predicates, plus isValidName. The scanner's
name recognition now delegates to these exact ranges (replacing a loose isLetter
check) and handles precomposed and combining-mark names. Closes#35.
Reimplemented from the XML grammar; no upstream code copied. 4 tests; 98 total.
Decode UTF-32 (both byte orders, BOM or sniffed) and the single-byte encodings
ISO-8859-1 and Windows-1252, selected by the XML declaration's encoding name,
in addition to UTF-8 and UTF-16. The streaming byte path also handles UTF-32.
Standard library only, no Foundation. Closes#28. Reimplemented from the
documented encoding algorithms; no upstream code copied. 4 tests; 94 total.
Add PureXML.Emitting.Writer, the libxml2 xmlTextWriter model: emit a document
with writeStartElement/writeAttribute/writeString/writeEndElement and friends,
never building a tree. Escaping is extracted into a shared Emitting.Escaping
helper used by both the writer and the serializer, so compact writer output is
byte-identical to serializing the equivalent tree. Pretty mode indents element
children (forward-only, so prefer compact for mixed content). Closes#32.
Reimplemented from the libxml2 model; no upstream code copied. 6 tests; 90 total.
Add PureXML.Parsing.SAXHandler (a struct of optional callbacks) and
PureXML.parse(_:sax:), which drives the handler from the streaming event core,
delivering start/end document, start/end element, characters, CDATA, comment,
and processing-instruction callbacks. Element names are namespace-resolved.
Closes the SAX2 epic (#19). Reimplemented from the libxml2 SAX2 model; no
upstream code copied. 5 tests; 84 total.
Complete the structural DTD validation layer. AttributeType now distinguishes
the ID family; the validator does a second pass after the structural walk
(since an IDREF may point forward to an ID later in the document):
- ID-typed attribute values must be unique across the document.
- IDREF values must each resolve to a declared ID.
- IDREFS values are split on whitespace and each token must resolve.
Reimplemented from the DTD ID/IDREF spec; no upstream code copied. 4 tests; 79
total.
Surface <!ATTLIST> declarations from the parser and validate attributes against
them. Reimplemented from the DTD attribute-list spec; no upstream code copied.
- DocumentType gains attributeLists (raw ATTLIST bodies per element); the
DoctypeScanner captures them and the EventReader plumbs them through.
- AttributeListParser parses a body into AttributeDeclaration values
(AttributeType: cdata or enumeration; AttributeDefault: required/implied/
fixed/value).
- DTDSchema now checks #REQUIRED presence, #FIXED value match (when present),
and enumerated-value membership, in addition to content models.
- Tokenized types (ID/IDREF/NMTOKEN) parse but are not yet value-enforced.
- 5 attribute tests; 75 total.
Surface the parsed <!DOCTYPE> internal subset's <!ELEMENT> declarations from the
parser and validate a tree against them. Reimplemented from the DTD content-model
spec; no upstream code copied.
- DoctypeScanner captures <!ELEMENT name model> declarations; the parser surfaces
them as Parsing.DocumentType (via parseWithDocumentType).
- Validation.ContentModel + ContentModelParser parse a model string (EMPTY, ANY,
(#PCDATA), mixed, and element content with sequence/choice and ?/*/+).
- ContentModelMatcher matches the child element-name sequence against a model as
a regular language (NFA-position matching).
- Validation.DTDSchema.validate reports issues for EMPTY/PCDATA/mixed violations,
element content with stray text, and child sequences that do not match.
- PureXML.validateAgainstInternalDTD(_:) parses and validates in one call.
- ATTLIST validation is out of scope. 9 tests; 70 total.
Add PureXML.XPath: a compiled location-path query engine reimplemented from the
XPath 1.0 specification; no upstream code copied.
- Compiler parses a path into steps; Evaluator walks the tree returning a
Selection node-set (node or attribute) in document order.
- Forward axes only (child, descendant //, self ., attribute @), since the value
model has no parent pointers. Node tests: name, *, text(), node(), comment().
Predicates: [n], [@a], [@a='v'], [child], [child='v'].
- Public surface: XPath.Query (init compiles, evaluate/elements/strings), the
Selection enum (element / stringValue), QueryError, and the one-shot
PureXML.xpath(_:over:). Upward/sibling axes and the full expression language
are explicitly out of scope.
- 12 XPath tests; 61 total.
Add opt-in DTD processing behind Limits(allowDoctype:), off by default. When on,
DoctypeScanner reads the <!DOCTYPE> internal subset and honors internal
<!ENTITY name "value"> general entities, which EntityDecoder expands recursively
in text and attribute values.
Safety is mandatory and reimplemented from the Expat amplification-accounting
model (no upstream code copied):
- only entity-expanded characters count against a shared budget
(Limits.maxEntityExpansion); overflow raises amplificationLimitExceeded, which
defeats billion-laughs.
- a visiting set detects recursive entities (recursiveEntity).
- undeclared references raise undefinedEntity.
- external entities are never stored or loaded, so XXE stays closed.
The default still rejects <!DOCTYPE>. 8 DTD tests (incl. billion-laughs and the
external-entity refusal); 49 total.
Add PureXML.parse(pullingBytes:) and events(pullingBytes:) over an incremental
byte source (() -> UInt8?). StreamingDecoder detects the encoding from the
leading bytes, strips any BOM, and yields one scalar at a time as a Character,
so a byte stream is never fully buffered. UTF-8 (1 to 4 bytes) and UTF-16 (with
surrogate pairs) are decoded incrementally; invalid sequences become the Unicode
replacement character. Standard library only, no Foundation. Reimplemented from
the documented UTF decoding rules; no upstream code copied.
Add namespace resolution following the XML Namespaces rules, reimplemented in
Swift from libxml2's SAX2 model; no upstream code copied.
- QualifiedName gains an optional resolved namespaceURI (nil for names built
directly or in no namespace), with a resolved(namespaceURI:) helper.
- NamespaceContext maintains a scope stack of xmlns bindings as elements open
and close, resolving element and attribute names against it.
- A default namespace applies to unprefixed element names but never to
attributes; the xml prefix is built in; xmlns declarations are preserved as
attributes so documents round-trip; an unbound prefix raises
ParseError.undefinedNamespacePrefix. End-tag matching stays lexical.
- Namespace-free documents are unchanged (URIs remain nil).
- 7 namespace tests; 39 total.
Add PureXML.parse(bytes:) and events(bytes:) for raw [UInt8] input. The encoding
is detected following the XML sniff order (BOM first, then the <? byte pattern,
defaulting to UTF-8) and decoded with the Swift standard library only, so the
package stays Foundation-free and portable. UTF-8 and UTF-16 (both byte orders,
with or without a BOM) are supported, the encoding set the XML spec requires.
Malformed input (odd-length UTF-16) raises ParseError.malformedEncoding.
PureXML.Parsing.InputEncoding.detect(_:) exposes the detector. The lint rule
optional_data_string_conversion is disabled because its suggested fix is a
Foundation API this package cannot use. Reimplemented from the documented XML
byte-detection algorithm; no upstream code copied.
Add PureXML.Parsing.Limits: configurable caps on nesting depth (default 256,
matching libxml2), name length, and content length, enforced during scanning in
the event reader. This protects against pathological input (deeply nested or
oversized documents) and keeps the recursive Model.Node value safe to build and
tear down (extreme depth otherwise overflows on ARC release). Threaded through
Parser.parse and the top-level parse/events API. Reimplemented from libxml2's
bounded-by-default posture; no upstream code copied.
Reimplement the serializer as an explicit work-stack walk instead of recursion,
mirroring libxml2's xmlNodeDumpOutputInternal, so it is no longer the depth
bottleneck. Reimplemented in Swift; no upstream code copied.
Also fix a mixed-content correctness bug: formatting is now suppressed for any
element with text or CDATA children (previously only when ALL children were
text), so pretty-printing never injects whitespace around significant text.
Note surfaced while testing: the remaining depth ceiling is the recursive
Model.Node indirect enum (recursive ARC teardown overflows at extreme depth),
which is why the parser must cap nesting depth (next).
The serializer escaped only & < " in attribute values, so an attribute holding
tab, newline, or carriage return would not survive a spec-compliant parser's
attribute-value normalization. Match the libxml2 attribute escape table: emit
	 for the whitespace characters and > for >. Behavior derived
from the libxml2 analysis (xmlsave.c / xmlIO.c escape tables), reimplemented in
Swift; no upstream code copied.
Replace the notImplemented stub with a real parser built on a pull-based,
streaming event core that never requires the whole document in memory.
- Parsing.EventReader: pulls characters through a source closure with a bounded
lookahead buffer and emits one Parsing.Event at a time, holding only an
open-element stack. Drives chunked/streamed input.
- Parsing.Parser: builds a Model.Node tree iteratively over the event core
(explicit stack, no recursion).
- Reader: streaming character cursor (closure-backed, bounded buffer) replacing
the previous whole-string Array.
- EntityDecoder: the five predefined entities and decimal/hex char references.
- Handles elements, attributes, text, comments, CDATA, processing instructions,
and an XML declaration in the prolog. Rejects <!DOCTYPE> by default (no
DTD/XXE/entity-expansion exposure).
- Top-level API: parse(_:), parse(pulling:), events(_:), events(pulling:).
- 21 tests including event-stream and chunked-source coverage.
Root Swift package mirroring PureYAML's structure, rules, and verification
gates. Includes the XML node model (Node, Element, Attribute, QualifiedName),
a working emitter (Serializer) with escaping and pretty/compact options, and
structural validation. The parser surface is stable; parse() raises
notImplemented until the scanner lands, pinned by a test.
- dependency-free, namespaced under PureXML
- portability and style gates carried over (style, namespacing, forbidden
patterns, swiftformat, swiftlint, linux, wasm, windows CI)
- 11 tests passing