- Eval-time prefix bindings: Query.evaluate(over:namespaces:) and
value(at:...:namespaces:) supply prefix to URI bindings, so a name test
like q:item resolves q to a URI and matches by namespace regardless of
the document's own prefix. The match is namespace-aware only when
bindings are present, so existing unprefixed/prefix-string queries are
unchanged.
- XPointer schemes: xpath1() evaluates an XPath expression (like
xpointer()), and xmlns(p=uri) binds a prefix for the expression parts
that follow it, threaded into the XPath evaluation.
The range() scheme (and so XInclude range inclusion) remains.
Part of #88
Three XInclude gaps from the audit:
- accept / accept-language / encoding are surfaced to the loader via a new
XIncludeRequest and a process(_:base:loading:) overload, so a loader can
honor content negotiation. The existing loadingURI form is kept.
- Cycle detection tracks the visited (resolved-URI) set along the current
inclusion chain and raises XIncludeError.cycle, instead of relying only
on the depth backstop; a non-cyclic diamond include is not flagged.
- parse="text" combined with an xpointer is rejected
(XIncludeError.textWithFragment), since a fragment cannot select within
unparsed text.
xpointer range() inclusion still depends on the XPointer range() scheme
(tracked in the XPointer issue).
Part of #87
Two ISO Schematron gaps, the most validator-relevant first:
- Dynamic messages: an assert/report message is now a template, so
<value-of select=> renders the string value of its XPath at the context
node and <name> renders the node's name. A finding can report actual
values ("Age 120 must be under 100") instead of static text.
- Phases: <phase id><active pattern=> scopes which patterns run.
validate(_:phase:) honors a requested phase or the schema's
defaultPhase; nil or #ALL runs every pattern.
let variables, abstract rules/extends, diagnostics, and key()/document()
remain (the last depends on XSLT-style function injection).
Part of #80
Install each builtin validation alone on a blank validator, run it
against a seeded subject, and assert the resulting errors by count,
reason, and coding path, matching the validation-rules recipe.
- Structural.uniqueAttributes (multi-error, one per duplicate).
- DTD.contentModel, DTD.attributeDeclarations (located reasons).
- DTD.undeclaredElement (the single-error Bool form: Failed to satisfy).
- DTD.identifierIntegrity (tree-wide, located at the document root).
- XSD.contentValidity (located at the document element).
Bring the validation-framework unit tests up to the OpenAPIKit idiom's
bar: each combinator is exercised as a pure function against a hand-built
context.
- The comparison operators (>, >=, <, <=, !=) lifting a KeyPath and a
literal into a predicate.
- lift running child validations against a lifted value.
- unwrap erroring on nil and otherwise running child validations.
- The erased wrapper yielding two errors when the same value of the same
type is applied twice.
- ValidationError rendering: at-path, at-root-of-document, and the
stripped trailing period.
Two OASIS catalog gaps from the audit:
- systemSuffix and uriSuffix entries: an identifier ending in the rule's
suffix maps to its URI, longest suffix winning, tried after exact and
rewrite matches.
- prefer=public|system on the catalog: a system match always wins, and
the public identifier is consulted as a fallback only when prefer is
public or no system identifier was given (the OASIS rule). The default
stays prefer=public.
xml:base / RFC 3986 relative-URI resolution of entry URIs and the legacy
SGML catalog format remain.
Part of #86
Broaden the serializer and writer options from the audit:
- attributeQuote selects single or double quotes; the active quote is
escaped in values (apostrophe as ' in single-quote mode).
- lineEnding controls the inserted newline (set to CRLF for \r\n output).
- xml:space=preserve suppresses pretty-print indentation for an element
and its subtree; xml:space=default re-enables it.
- Writer gains writeStartElementNS(prefix:localName:namespaceURI:) and
writeNamespace(prefix:uri:), so callers no longer hand-format prefixes
and xmlns attributes.
CDATA-vs-escape and entity re-escaping strategies are left as a
follow-up; the current escaping is the safe default.
Closes#91
Add the DOM/tree editing primitives from the audit that the editor use
case needs:
- Attribute helpers: attributeValue(_:), setAttribute(_:_:) (update in
place or append), removeAttribute(_:).
- shallowCopy() copies a node alone, complementing the deep copy().
- precedes(_:) compares two nodes in document order (ancestor before
descendant, earlier sibling before later; unrelated nodes compare false).
- normalize() coalesces adjacent text children, drops empty text, and
recurses into elements; CDATA is left intact.
First-class namespace/DOCTYPE/entity-reference nodes (an invasive
TreeNodeKind change) and cross-document adoption/ownerDocument (no
document-identity model) are left for a follow-up.
Closes#90
Close the two XSD-regex gaps from the audit:
- \p{...} and \P{...} now resolve Unicode general categories (one-letter
groups like \p{L} and two-letter codes like \p{Lu}/\p{Nd}) through
Swift's own scalar generalCategory, so no character tables are vendored.
A curated set of well-known \p{Is<Block>} blocks is supported; an
unknown category or block name is a compile error rather than a guess.
- Character-class subtraction [a-z-[aeiou]] removes the subtrahend from a
class, and nests ([a-z-[b-d-[c]]]) and composes with categories.
Categories and subtraction work both as standalone atoms and inside
character classes, so XSD pattern facets that use them now validate.
Closes#89
Normalize line endings in the Reader as the spec requires, which was not
done before:
- CR and CRLF fold to a single line feed (XML 1.0), so every downstream
scan sees line feeds only.
- When the declaration names version 1.1, NEL (U+0085) and LINE SEPARATOR
(U+2028), and a CR followed by NEL, also fold to a line feed; in 1.0
those characters are preserved as data.
The XML 1.1 name-character productions are identical to XML 1.0 Fifth
Edition, which the character classifier already implements, so names need
no change. The 1.1 character range (C0 controls permitted only as
references) is not yet enforced.
Closes#96
Broaden the TextReader pull cursor toward libxml2's xmlTextReader:
- Attribute nodes: moveToFirstAttribute/moveToNextAttribute/
moveToAttribute(_:)/moveToElement reposition the cursor onto each
attribute as an .attribute node and back.
- Namespace accessors: namespaceURI, localName, prefix from the current
element's or attribute's resolved name.
- xml:lang: inherited down the tree, overridable per element, reset when
it goes out of scope.
- Position: lineNumber/columnNumber report where each node begins, so a
streaming consumer can locate findings in the source. EventReader now
records each node's start mark.
DOCTYPE and entity-reference nodes are not surfaced (the DTD is exposed
through documentType, and entities are expanded inline); whitespace is
not split into significant/insignificant (needs the content model);
baseURI needs a document URI. The accurate positioning path for editor
findings remains readTree with source ranges.
Closes#98
Parse the XML declaration's pseudo-attributes instead of skipping the
<?xml?> processing instruction:
- version, encoding, and standalone are surfaced on a new XMLDeclaration,
returned from parseWithDocumentType and via PureXML.xmlDeclaration(_:).
- The pseudo-attributes are validated: fixed order (version, encoding,
standalone), known names only, and standalone must be yes or no. A
malformed declaration is rejected.
- The declared version is now readable, a prerequisite for XML 1.1.
parseWithDocumentType returns a ParsedDocument struct (node,
documentType, declaration) rather than a 2-tuple; the member names are
unchanged so existing call sites are unaffected.
Closes#95
Deepen DTD processing in the doctype scanner:
- Conditional sections are processed, not skipped: INCLUDE sections have
their declarations scanned, IGNORE sections are discarded, the keyword
may be a parameter-entity reference, and nested sections are handled.
- <!NOTATION> declarations are parsed and stored on DocumentType.
- Unparsed entities (<!ENTITY ... NDATA n>) are parsed and stored with
their notation, distinct from general entities.
- External parameter entities are loaded through the injected resolver so
their declarations expand; the default refusing resolver loads nothing,
keeping XXE closed.
- NOTATION attributes are a distinct type, validated against the declared
notations (value must be listed and the notation declared).
Tracking unparsed entities also lets ENTITY/ENTITIES attribute values be
validated against the declared NDATA entities, closing the gap left when
#73 could only check name syntax.
Closes#72
Complete the DTD attribute handling found in the audit:
- Default and #FIXED attribute values are applied to the parsed tree.
parseApplyingInternalDTDDefaults returns a tree where every element
carries the attributes its <!ATTLIST> defaults, so an editor sees the
same effective attributes a validating processor would.
- NMTOKEN, NMTOKENS, ENTITY, ENTITIES are validated: NMTOKEN(S) must be
XML name tokens, ENTITY/ENTITIES must be XML names. Previously all fell
through to unconstrained CDATA.
- DTD default values are normalized per XML 1.0: character and predefined
entity references are expanded, literal whitespace becomes a space, and
non-CDATA values are collapsed and trimmed.
ENTITY/ENTITIES check name syntax only; that the named entity is a
declared unparsed entity is not verified, since NDATA entity tracking is
not yet modeled.
Closes#73
Make XSD validation namespace-aware and enforce the form defaults and
xs:all structural constraints that were previously ignored:
- targetNamespace is tracked: a schema's global elements are in its
target namespace, and an instance's root element must be in it.
- elementFormDefault / attributeFormDefault (and per-declaration form)
qualify local element and attribute declarations. The content-model
matcher already compares namespace URIs, so qualified declarations
now match only same-namespace instance nodes; attribute matching
became namespace-aware too.
- xs:all groups are checked at compile time: every member must be an
element occurring at most once, the group may occur at most once, and
nested model groups inside an all are rejected (a SchemaError).
When a schema has no target namespace the stamping is a no-op, so
unqualified schemas validate exactly as before.
Cross-namespace type references through xs:import resolve by local name
only; full multi-namespace type tables are not modeled.
Closes#78
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
Add xs:any and xs:anyAttribute wildcards with namespace and
processContents constraints (skip/lax/strict), and resolve the
list built-in datatypes (IDREFS, ENTITIES, NMTOKENS) to their
item types so each list item validates.
WildcardNamespace models ##any/##other/enumerated URI sets;
Wildcard.admits gates undeclared attributes and wildcard-matched
children. ContentNFA.follow exposes the follow set for completions.
Closes#77
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.
Slice 4 of #94 (completes the epic). Quick-fixes are derived from the structured
schema completions, never by parsing a diagnostic message:
- TreeNode gains contentRange (the span between an element's start and end tags),
captured by the ranged reader, so an inserted attribute or child is placed
exactly.
- QuickFix and TextEdit model an editor edit (a replacement over a source range;
an insertion is a zero-width range).
- QuickFixEngine turns a Completions into fixes: a required-but-absent attribute
becomes an insertion before the start tag's >, and a still-expected required
child becomes an insertion before the end tag. Schema.Document.quickFixes(at:in:)
is the entry over a ranged tree.
Tests apply a fix and re-validate: adding the required id, or inserting the
required qty, yields XML the schema then accepts; a complete element offers none.
#94 (editor integration) is complete: ranged recovering read, unified lint,
schema completions, and quick-fixes.
Slice 3 of #94. From the compiled schema, compute exactly what is allowed at an
element, for autocomplete and "what's missing".
- ContentNFA.follow(after:) returns the precise follow-set: the element-name
labels the content automaton can accept next after the children already
present, plus whether the content may legally end there.
- Schema.Completions reports the allowed next elements, the complete flag (false
means a required child is still expected), a wildcard flag, and each declared
attribute with its required and present status.
- CompletionEngine resolves an element's declared type along a coding path (the
global declaration, then each parent's content model) and computes the
completions; Schema.Document.completions(at:in:) is the entry over a ranged
tree.
Tests pin the follow-set as the children accumulate (name then qty then the
optional note, complete flipping true), the attribute required/present status,
and resolution for a nested element by its coding path.
Slice 2 of #94. One call for an editor: PureXML.lint reads the document once into
the ranged, best-effort tree (never throwing), then merges the parse diagnostics
and the validation findings into a single source-ranged, severity-tagged list in
document order.
- LintDiagnostic carries message, severity, and the SourceRange to highlight.
- lint(_:validate:) takes a node-based validate closure that runs over the
RECOVERED tree, so validation works even when the document is not well-formed:
a truncated, schema-invalid document yields both a parse diagnostic and the
validation finding, each with a range.
- Schema.Document gains validate(_ node:) so a caller validates the same tree it
is editing; validate(_ xml:) delegates to it.
Tests cover a clean lint, well-formedness-only lint, a range-located validation
finding, and the both-truncated-and-invalid editor case.
Slice 1 of #94 (editor integration). The keystone: a located validation finding
can now be mapped back to a source range.
- TreeNode gains a sourceRange (start/end Mark); SourceRange is a half-open span.
- PureXML.readTree builds a mutable, parent-aware, best-effort tree whose every
node carries its span (an element from the < of its start tag to past its end
tag; leaves over the text they consumed). Never throws, recovers in place,
returns located diagnostics.
- TreeNode.node(at:) navigates a validation coding path to its node using the
same canonical convention the validators build paths with (a sibling index
only when a name repeats), and sourceRange(at:) returns the span. So
Document.validate's ValidationError.codingPath resolves to characters in the
document.
Tests prove the spans, the navigation, the end-to-end bridge (an XSD finding to
a source range), and that ranged reading of invalid input never crashes.
Closes#75, on the validation framework. XSD instance-level features:
- nillable elements: an xsi:nil="true" element is accepted (its content model is
not checked) only when the element is declared nillable and carries no content;
xsi:nil on a non-nillable element, or a nilled element with content, is a
located error.
- fixed value constraints: a present fixed attribute must equal its fixed value,
and a fixed element's text must match.
- default/fixed are parsed onto AttributeUse (per use) and collected per element
name (nillable set + value constraints), threaded through XSDContext to the
ComplexValidator.
All findings are located ValidationErrors. Collection and helper methods move to
same-file extensions to stay within type-body limits.
Slice 5 of #92. The last validator moves onto the framework and the legacy
Issue type is removed, so all validation now flows through one error model.
- ValidationError gains a severity (error by default, warning for advisory
findings), chosen so Schematron's assert/report distinction survives in the
unified type.
- Schematron is a Validation<Node, Void> over the document root; a failed assert
is an error finding, a matched report a warning. Each finding is located at the
context node its rule fired on (built from the element chain, with a sibling
index only when a name repeats), not the document root.
- Schematron.validate returns [ValidationError] and runs through a Validator.
- Validation.Issue and its Severity-based result are deleted; Severity now backs
ValidationError.
Every validation layer (structural, DTD, XSD content and identity, Schematron)
now produces located ValidationError values through the shared framework.
Slice 4 of #92. The XSD complex-type and identity-constraint validators move
onto the validation framework, producing located results.
- ComplexValidator threads a coding path through its (inherently recursive)
descent, so every content-model, attribute, and simple-type violation is now
a ValidationError located by path ("... at path: order/qty"). Child steps
carry a sibling index only when a name repeats.
- IdentityValidator returns ValidationError instead of Issue.
- A new XSD rules enum wraps both as Validation<Node, XSDContext> values gated to
the document root, combined by Validator<XSDContext>. XSDContext carries the
type table, the identity constraints, and the root declaration.
- Schema.Document.validate builds the context and runs the validator, returning
[ValidationError].
XSD content-model validation is recursive by nature (a child's type depends on
its position), so the rules delegate to the recursion while contributing to one
error collection. Schematron is the last validator on Issue and migrates next.
Slice 3 of #92. The DTD validator's imperative walk becomes declared Validation
values over a DTDSchema document:
- DTDSchema is now just the schema (content models + attribute declarations) and
serves as the rules' Document context; its validate/walk methods are gone.
- contentModel and attributeDeclarations are Validation<Element, DTDSchema>, so
every content-model and attribute violation is now located by coding path
("... at path: r/a"). undeclaredElement is added only in strict mode.
- identifierIntegrity is a Validation<Node, DTDSchema> gated to the root, doing
the document-wide ID-uniqueness and IDREF-resolution checks in one pass.
- DTD.validator(strict:) assembles the rule set; validateAgainstInternalDTD runs
it and returns [ValidationError] instead of [Issue].
A new test pins the located result. Schema (XSD) and Schematron validators still
use Issue and migrate in the following slices.
Closes#93. Recovery moves into a recovering mode on EventReader, so malformed
input is repaired in place rather than dropped to the next markup boundary:
- A duplicate attribute is recorded and the later occurrence dropped, keeping the
element. <a id='1' id='2'>y</a> now reads as <a id="1">y</a> with one
diagnostic (was an empty tree with three).
- A mismatched or unexpected end tag pops the open stack to the matching ancestor,
implicitly closing the elements nested inside it. <a><b><c>x</b></a> reads as
<a><b><c>x</c></b></a> with a single diagnostic.
- Pre-root junk reports "unexpected character" instead of the misleading
"content after the root element".
In recovering mode next() no longer throws: it records a Diagnostic and emits
best-effort events, popping to a match on a mismatch and stopping on truncation
so the builder closes what is open. The strict path is unchanged. readRecovering
becomes a plain non-throwing drain that takes the reader's diagnostics. Recovery
stays deterministic; new tests pin the kept-element and pop-to-match results.
Slice 2 of #92. Add PureXML.read, a deterministic recovering reader: it never
throws and never traps, returning the maximal best-effort tree plus one located
Diagnostic per problem.
- ReadResult { node, diagnostics } is the Equatable result type, so a
deterministic read can be compared whole.
- Diagnostic carries a message and a structured source Mark; a thrown ParseError
maps to one directly (ParseError.mark exposes the position each case carries).
- The reader resynchronizes after a failure (EventReader.recover(from:)):
progress-aware, so a failure that already consumed the bad construct retries
from there, while a stuck failure skips to the next markup start; it always
advances when input remains, guaranteeing termination.
- The tree builder assembles leniently: elements left open by truncation are
closed, an end tag with no open element is dropped. Well-formed input yields
exactly parse(_:)'s tree with no diagnostics.
Tests pin exact trees and diagnostics for truncation, mismatched tags, and stray
junk, prove determinism (same bytes give an identical ReadResult), and fuzz 400
seeded inputs (arbitrary plus truncated valid docs) asserting no crash.
Recovery here is resync-based; finer in-reader recovery (keep the element on a
duplicate attribute, pop to a matching open tag) is the next slice.