A <data type=> pattern now carries a SimpleType (base + facets) rather
than a bare built-in, and its <param name=>value</param> children
populate the facets (minLength, maxLength, length, pattern, min/max
inclusive/exclusive, total/fraction digits), reusing the XSD datatype
facet validation. So a constrained datatype like xsd:string with
minLength rejects too-short values.
datatypeLibrary resolution, typed <value> comparison, and the RNC
datatype parameter blocks remain.
Part of #79
The validation framework already conforms to the OpenAPIKit idiom: named
composable Validation values, a Validator with blank/validating/
withoutValidating, located errors, parameterized over both subject and
document. The DTD layer is already four named rules.
XSD content validation stays a single recursive Validation
(contentValidity) rather than a rule per constraint, because XSD is a
recursive type system: a child's type is resolved from its parent's
content model and position, then rewritten by xsi:type and the derivation
controls mid-walk, so the constraint categories are interdependent and
cannot be split into independent rules without changing semantics or
duplicating the recursion. That is the sanctioned multi-error form.
To keep each category verifiable on its own (the idiom's testability),
add XSDCategoryTests exercising ComplexValidator directly per category
(attributes, content, nillable, fixed); derivation, wildcards, and
namespaces are already isolation-tested through crafted schema strings.
Closes#101
- cdataAsText emits a CDATA section's content as escaped text instead of
a <![CDATA[ ... ]]> section.
- asciiOnly escapes every non-ASCII character as a numeric character
reference, so the output is pure ASCII (libxml2's character-reference
encoding).
Both apply to the Serializer and the incremental Writer through the
shared Escaping rules.
Closes#91
An assert/report may reference <diagnostic> messages by id through its
diagnostics= attribute; each referenced diagnostic is rendered (with the
same value-of/name/let substitution) and appended to the finding for
extra per-failure detail. <diagnostics> is collected schema-wide.
Schema/pattern-level let, abstract patterns, and key()/document() remain.
Part of #80
A <rule abstract="true" id="x"> is a template that does not fire on its
own; a concrete rule pulls in its let bindings and assertions with
<extends rule="x"/>, prepended so the concrete rule extends rather than
replaces them.
Part of #80
<rule> may now declare <let name= value=> bindings, evaluated at each
context node in order (so a later binding can reference an earlier one
through $name). The bindings are available to the rule's assert/report
tests and to <value-of select="$name"> in messages, using the XPath
engine's existing variable support.
Schema- and pattern-level let, abstract rules/extends, diagnostics, and
key()/document() remain.
Part of #80
Catalog replacement URIs (uri, rewritePrefix, delegate/next catalog) are
now resolved against the catalog's base URI and any in-scope xml:base,
reusing the existing RFC 3986 URIReference resolver. Resolver(_:baseURI:)
supplies the catalog's own location; xml:base on the catalog/group/entry
elements scopes nested entries. An empty base leaves relative URIs as
written, so catalogs without a base behave exactly as before.
The legacy SGML catalog format and group-level prefer override remain.
Part of #86
Invisible ~~~ links did not reliably rank nodes vertically in GitHub's
Mermaid renderer, leaving the shipped nodes spread horizontally. Chain
every node with --> so flowchart TB stacks them in a single top-to-bottom
column.
The roadmap diagrams and status text were badly out of date. Rewrite the
second mermaid block from a single unreadable node into an accurate
#71-audit roadmap, refresh the shipped block, and chain every node into a
single vertical column so flowchart TB renders top-to-bottom rather than
fanning out wide.
Also correct the flagrantly stale prose: test count (135 to 506), the
false 'XSD/RELAX NG/Schematron remain open epics' and 'XPath 1.0 subset'
claims (all shipped), and replace the obsolete open-epics table with the
current #71 depth-gap work.
A define repeated with combine="choice" or combine="interleave" now
merges with the existing definition (as a choice or interleave pattern)
instead of the last one silently replacing it. A plain redefinition with
no combine still replaces. Applied consistently in a grammar and across
include merges.
<param> facets under <data>, datatypeLibrary resolution, typed <value>
comparison, and the RNC-syntax gaps remain.
Part of #79
- 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.