Commit Graph

43 Commits

Author SHA1 Message Date
ab7d924901 Merge epic/22-pattern: streaming xmlPattern matching 2026-06-08 05:49:14 +02:00
f7fe89023c feat(pattern): streaming xmlPattern matching
Add the PureXML.Pattern namespace: a compiler for the streamable XPath
subset (names, *, prefix:*, /, //, leading absolute /, trailing @attr) into
a Matcher, with predicates and . / .. rejected. The backward-matching
algorithm tests an element's ancestor-or-self path. Pattern.matches(_:in:)
streams a document through the pull parser and returns matching node paths
in document order from the open-element stack alone, no tree built.

Closes #44
Closes #45
Closes #22
2026-06-08 05:49:13 +02:00
0b42dcaa60 Merge epic/43-xpath-context: explicit XPath evaluation context (completes #21) 2026-06-08 05:41:00 +02:00
4fe8771fb9 feat(xpath): explicit evaluation context entry point
Add Query.value(at:position:size:variables:) to evaluate an expression
against a context node already in a tree, with its proximity position, size,
and variable bindings. position(), last(), and the upward axes resolve from
that node. This completes full XPath 1.0: the per-node entry point that
downstream engines (XSLT, Schematron) drive.

Closes #43
2026-06-08 05:40:52 +02:00
cef46fe3ae Merge epic/41-xpath-functions: full XPath 1.0 function library 2026-06-08 05:38:40 +02:00
d1ae56acbd feat(xpath): full XPath 1.0 function library
Add the string functions (concat, starts-with, contains, substring-before,
substring-after, substring, string-length, normalize-space, translate), the
number functions (sum, floor, ceiling, round), and the node functions
(local-name, name, namespace-uri, id, lang), merged into the evaluation
library. All pure Swift, no Foundation: substring search is hand-rolled.
Without a DTD, id() matches an attribute named id or xml:id.

Closes #41
2026-06-08 05:38:39 +02:00
6c2d3d6948 Merge epic/42-xpath-expr: XPath expression engine and four-type model 2026-06-08 05:33:20 +02:00
d83e5a88ce feat(xpath): expression engine and the four-type model
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
2026-06-08 05:33:13 +02:00
f1d9015e80 Merge epic/40-xpath-axes: all thirteen XPath axes 2026-06-08 05:15:20 +02:00
6b7853f2d5 feat(xpath): all thirteen axes via parent-aware navigation
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
2026-06-08 05:15:14 +02:00
d2a9ba576b ci: split workflows for truthful badges and attribution gates
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.
2026-06-08 04:37:32 +02:00
93fd1f7815 Merge epic/33-textreader: xmlTextReader pull cursor 2026-06-08 03:18:01 +02:00
aafd99589e feat(parse): xmlTextReader pull cursor
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
2026-06-08 03:18:00 +02:00
dfd685fb15 Merge epic/31-tree: mutable parent-aware tree (libxml2 tree.h parity) 2026-06-08 03:14:03 +02:00
15de0c5344 feat(model): mutable parent-aware tree (libxml2 tree.h parity)
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
2026-06-08 03:13:59 +02:00
d3f457babf Merge epic/29-entities: external and parameter entities (secure by default) 2026-06-08 03:08:25 +02:00
c75c97aa9b feat(dtd): external and parameter entities, secure by default
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
2026-06-08 03:08:20 +02:00
8027010e9e Merge epic/34-save-parity: XML declaration save-option parity 2026-06-08 02:57:32 +02:00
6b12319c89 feat(emit): XML declaration save-option parity
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
2026-06-08 02:57:27 +02:00
07c4a84fac Merge epic/35-char-validity: spec-exact character classification (#35) 2026-06-08 02:52:43 +02:00
c82fef085e feat(parsing): spec-exact XML character classification (chvalid parity)
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.
2026-06-08 02:52:42 +02:00
96d240d715 Merge epic/28-encodings: full encoding support (#28) 2026-06-08 02:39:26 +02:00
85172923e8 feat(parsing): full encoding support (UTF-32, ISO-8859-1, Windows-1252)
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.
2026-06-08 02:39:25 +02:00
135e464652 Merge epic/32-xmltextwriter: incremental XML writer (#32) 2026-06-08 02:32:39 +02:00
9b55767b39 feat(emitting): add an incremental XML writer (xmlTextWriter)
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.
2026-06-08 02:32:39 +02:00
ef682808d4 feat(parsing): add SAX2 push-callback API
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.
2026-06-08 02:26:04 +02:00
a31cdd5401 docs(roadmap): track full libxml2 feature parity as epics
Reframe the goal as 100% libxml2 feature parity in pure Swift with zero
dependencies (clean-room, no upstream source copied), and track the remaining
surface as 20 epics across parsing/IO, querying, validation, and transform.
Deliberate non-goals (network fetch, threading/memory infra) and the
opt-in-only external resolution (XXE-safe) are noted. Every epic-labeled issue
has a node in the roadmap diagrams.
2026-06-08 02:23:07 +02:00
537b455ce8 feat(validation): add DTD ID/IDREF/IDREFS validation
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.
2026-06-08 02:13:11 +02:00
5cbe938826 feat(validation): add DTD attribute (ATTLIST) validation
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.
2026-06-08 02:07:01 +02:00
f02561b01d feat(validation): add DTD content-model validation
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.
2026-06-08 01:59:00 +02:00
9e7576bd7e chore(hooks): make attribution gates tool-agnostic and wire roadmap into style
Conform to the vendor-neutral github-discipline canon: the commit-msg and
pre-commit hooks and check-style.sh now reject AI attribution for every
assistant (Codex, Copilot, Gemini, ChatGPT, OpenAI, Anthropic), not just
Claude and Cursor. The no-cursor-attribution workflow is excluded from the
file-content scan since it legitimately quotes the tokens to detect them.

Also wire scripts/check-roadmap.sh into check-style.sh (and grant the CI gates
step issues:read + a token for the epic-coverage query), per the rule that the
roadmap gate runs in the style check.
2026-06-08 01:42:07 +02:00
1ae6680d94 feat(xpath): add an XPath 1.0 location-path subset
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.
2026-06-08 01:32:23 +02:00
01a35cc6e6 feat(parser): optional DTD with internal entities and billion-laughs defense
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.
2026-06-08 01:17:09 +02:00
204dd3895e feat(parser): streaming byte decode (incremental UTF-8/UTF-16)
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.
2026-06-08 01:07:24 +02:00
43f82312e6 feat(parser): resolve XML namespaces at the start-element boundary
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.
2026-06-08 01:03:29 +02:00
0f3086c8e6 docs(roadmap): add Apple-palette mermaid roadmap and the roadmap gate
Follow the mihaela-agents mermaid rule: add a README roadmap using the TileDown
Apple-color status classes (done/review/epic/todo), legend block first, then a
libxml2-parity roadmap showing shipped vs open work. Add docs/rules/mermaid.md
and scripts/check-roadmap.sh (wired into check-all.sh) so the diagrams stay
mechanically checkable.
2026-06-08 00:58:50 +02:00
e4b2e0127d feat(parser): accept raw bytes with UTF-8/UTF-16 encoding detection
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.
2026-06-08 00:52:56 +02:00
4856a3fcf4 feat(parser): add bounded-by-default limits (depth, name, content)
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.
2026-06-08 00:40:34 +02:00
209b6f1538 refactor(emitter): make the serializer iterative and fix mixed content
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).
2026-06-08 00:31:05 +02:00
5a7a52b000 fix(emitter): escape tab/newline/CR and > in attribute values
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
&#9; &#10; &#13; for the whitespace characters and &gt; for >. Behavior derived
from the libxml2 analysis (xmlsave.c / xmlIO.c escape tables), reimplemented in
Swift; no upstream code copied.
2026-06-08 00:20:55 +02:00
5c306482e5 feat(parser): add a streaming, iterative XML parser
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.
2026-06-08 00:10:50 +02:00
7683c67449 chore: extend .gitignore with editor, venv, node, and local-settings entries 2026-06-07 23:35:22 +02:00
708390242c chore: scaffold PureXML pure-Swift XML package
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
2026-06-07 23:20:20 +02:00