Commit Graph

183 Commits

Author SHA1 Message Date
cc44800a4a Merge: Schematron abstract patterns (#80) 2026-06-08 17:58:33 +02:00
d7a40b9a7a feat(schematron): instantiate abstract patterns with is-a/param
A <pattern abstract="true"> is a template whose rule queries carry $name
references; a <pattern is-a="…"> with <param> children instantiates it,
substituting each parameter into the template's context, test, <let>
value, and <value-of select=> before the XPath is compiled. The
substitution is a single left-to-right pass, so a substituted value is
never rescanned and a short name never captures a longer one; $name
references that are not parameters stay as written. The template itself
contributes no rules.
2026-06-08 17:58:33 +02:00
873aed7025 Merge: changelog-touched gate (verification.md) 2026-06-08 17:49:22 +02:00
e06a29ae1c chore(ci): add the changelog-touched gate verification.md requires
verification.md listed scripts/check-changelog-touched.sh in the local
gate, but the script did not exist (exit 127). Adds it: when a change set
modifies Sources/*.swift it must also update CHANGELOG.md, comparing the
branch against its base plus the working tree, no-op on a clean base, with
a CHANGELOG_SKIP escape hatch for refactor-only changes. Wires it into
check-all.sh alongside the other shell gates.
2026-06-08 17:49:21 +02:00
0171697c37 Merge: changelog and readme for #79 2026-06-08 17:45:42 +02:00
e0e498c4bb docs: record #79 RELAX NG work in changelog and refresh test count
Adds the Unreleased changelog entry for the datatypeLibrary resolution,
value-space <value> comparison, nsName subtraction, and compact-syntax
annotations/div/concatenation work, and updates the README suite count to
573 tests in 84 suites.
2026-06-08 17:45:42 +02:00
eff530bde7 Merge: RNC string concatenation (#79) 2026-06-08 17:38:59 +02:00
9845e1cd9d feat(relaxng): fold RNC ~ string concatenation
A string literal split with ~ ("a" ~ "b") is the single literal "ab",
the way long values and namespace URIs are wrapped across lines. The fold
applies wherever a string literal is read: value literals and the URIs of
namespace/datatypes/include/external declarations.
2026-06-08 17:38:58 +02:00
79f424321c feat(relaxng): compare typed <value> in the datatype value space
A <value> pattern now carries its datatype, so matching compares the
instance and the literal in that type's value space rather than purely
lexically. Numeric and date/time types compare by value (1 equals 01,
1.5 equals 1.50); booleans treat 1/true and 0/false as equal; string and
token compare by their whitespace-normalized lexical form, as before.
Untyped values default to the built-in token type, and a bare string
literal in the compact syntax is token-typed. Durations and binary types
fall under the lexical rule.
2026-06-08 17:36:16 +02:00
c1e181db50 Merge: datatypeLibrary resolution and nsName subtraction (#79) 2026-06-08 17:21:54 +02:00
147d776d21 feat(relaxng): resolve datatypeLibrary and nsName subtraction
Adds a datatypeLibrary-aware datatype resolver to the XML-syntax parser:
the in-scope library is inherited from the nearest ancestor, the default
(empty) library defines only string and token, and the W3C XML Schema
library defines the full built-in set. A type the in-scope library does
not define is an unknown datatype, so the <data>/<value> pattern matches
nothing rather than silently validating.

Adds an nsNameExcept name-class case so nsName with an <except> child
(XML syntax) and prefix:* - name subtraction (compact syntax) narrow a
namespace wildcard. The compact lexer splits prefix:* into a trailing-
colon word plus a star symbol, which the parser now recognizes.

Existing RELAX NG facet tests declared XSD types without the library and
relied on lenient resolution; they now declare the XSD datatypeLibrary,
matching what a conformant processor requires.
2026-06-08 17:21:49 +02:00
704e5e2161 Merge: RNC annotations and div grouping (#79) 2026-06-08 17:12:39 +02:00
e24004dd2a feat(relaxng): handle RNC annotations and div grouping
The compact-syntax parser now skips [ ... ] documentation/grammar
annotations (balanced over nesting) wherever they may appear, before a
define or before a pattern, and treats div { ... } as transparent
grammar grouping that contributes its defines and start directly. A
define literally named div still parses when assigned. Splits the
name-class and token helpers into a companion file to stay within the
type and file length budget.
2026-06-08 17:12:35 +02:00
434d0d8882 Merge epic/86-sgml-catalog: SGML catalog format
Part of #86
2026-06-08 17:03:07 +02:00
3a1ecb503a feat(catalog): SGML Open (TR 9401) catalog format
Resolver(sgml:) parses the legacy SGML catalog format: PUBLIC, SYSTEM,
DELEGATE, CATALOG, BASE, and OVERRIDE entries with -- comment --
delimiters. Replacement URIs resolve against BASE and the supplied
baseURI; OVERRIDE YES/NO maps to the prefer-public fallback. Unknown
keywords are skipped tolerantly. Feeds the same Resolver as the XML
catalog.

Part of #86
2026-06-08 17:03:06 +02:00
2dfa647111 Merge epic/79-rnc-nameclass-subtraction: name-class subtraction
Part of #79
2026-06-08 16:58:24 +02:00
3887b33e4e feat(relaxng): compact name-class subtraction (* - name)
The compact-syntax name class * - nameClass now parses as anyName except
the subtracted class (the model already had anyNameExcept). ns:* - name
would need an nsName-except model case and is left as a refinement.

Part of #79
2026-06-08 16:58:24 +02:00
ab4a8661f5 Merge epic/79-rnc-param-blocks: compact datatype parameter blocks
Part of #79
2026-06-08 16:55:57 +02:00
8438d94b44 feat(relaxng): compact-syntax datatype parameter blocks
The RNC datatype parameter block xsd:int { minInclusive = "5" } is now
parsed into facets (previously skipped), matching the XML-syntax <param>
support. The facet mapping is shared by both parsers through a new
RelaxNGFacets helper.

Part of #79
2026-06-08 16:55:57 +02:00
768d7b1134 Merge epic/80-schematron-scoped-let: schema- and pattern-level let
Part of #80
2026-06-08 16:52:17 +02:00
b3d535c77e feat(schematron): schema- and pattern-level let
<let> may now appear at schema and pattern level, not just in a rule.
Schema lets are evaluated once at the document root; pattern lets extend
them (also at the root); rule lets extend those at each context node. A
narrower-scope binding overrides a wider one of the same name.

Part of #80
2026-06-08 16:52:16 +02:00
7a48b6c9e6 Merge epic/79-relaxng-data-params: data param facets
Part of #79
2026-06-08 16:48:26 +02:00
28cbed0a70 feat(relaxng): <param> facets under <data>
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
2026-06-08 16:48:24 +02:00
c29bf69f07 Merge epic/101-validator-decomposition: scope decision + category isolation tests
Closes #101
2026-06-08 16:41:50 +02:00
50a6afb3fc docs(validation): document the XSD validator scope decision; isolation-test each category
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
2026-06-08 16:41:50 +02:00
e6c1c69f1c Merge epic/91-serialization-toggles: cdataAsText and asciiOnly
Closes #91
2026-06-08 16:34:58 +02:00
e2ba6a7359 feat(emitting): cdataAsText and asciiOnly save options
- 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
2026-06-08 16:34:57 +02:00
85c7bded45 Merge epic/80-schematron-diagnostics: diagnostics
Part of #80
2026-06-08 16:30:15 +02:00
a76e9b3da3 feat(schematron): diagnostics
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
2026-06-08 16:30:13 +02:00
27d8eb5e16 Merge epic/80-schematron-abstract: abstract rules and extends
Part of #80
2026-06-08 16:26:30 +02:00
7e04f6056c feat(schematron): abstract rules and extends
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
2026-06-08 16:26:30 +02:00
c21419fe7d Merge epic/80-schematron-let: rule-level let variables
Part of #80
2026-06-08 16:23:27 +02:00
d0671e40ba feat(schematron): rule-level let variable bindings
<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
2026-06-08 16:23:26 +02:00
c7802ed9d0 Merge epic/86-catalog-xmlbase: xml:base and RFC 3986 resolution
Part of #86
2026-06-08 16:19:45 +02:00
502aa26438 feat(catalog): xml:base and RFC 3986 relative-URI resolution of entry URIs
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
2026-06-08 16:19:45 +02:00
293625d41d docs(readme): chain roadmap nodes with directed edges for reliable vertical layout
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.
2026-06-08 16:12:49 +02:00
6e971ac7e6 docs(readme): rewrite stale roadmap and make mermaid diagrams vertical
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.
2026-06-08 16:10:28 +02:00
24a862e3a0 feat(relaxng): combine on define (choice/interleave merging)
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
2026-06-08 16:03:39 +02:00
8a52cdbcf2 Merge epic/88-xpath-xpointer: eval-time namespaces and XPointer schemes
Part of #88
2026-06-08 16:00:16 +02:00
2a77b56d2f feat(xpath): eval-time namespace context and XPointer xmlns()/xpath1() schemes
- 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
2026-06-08 16:00:15 +02:00
0c9aa47233 Merge epic/87-xinclude: content negotiation and cycle detection
Part of #87
2026-06-08 15:52:19 +02:00
b13cdca739 feat(xinclude): content-negotiation hints, cycle detection, text+fragment
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
2026-06-08 15:52:18 +02:00
68a8ff2887 Merge epic/80-schematron: dynamic messages and phases
Part of #80
2026-06-08 15:45:28 +02:00
68122ad887 feat(schematron): dynamic message content and phase support
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
2026-06-08 15:45:27 +02:00
5341ec3d46 test(validation): per-rule isolation tests in the OpenAPIKit style
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).
2026-06-08 15:39:18 +02:00
ef0c64c45c test(validation): unit-test the remaining combinators and error rendering
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.
2026-06-08 15:34:34 +02:00
f505a45f0f Merge epic/86-catalog: suffix entries and prefer attribute
Part of #86
2026-06-08 15:31:10 +02:00
b7da5652f3 feat(catalog): systemSuffix/uriSuffix entries and the prefer attribute
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
2026-06-08 15:31:10 +02:00
5b24530a72 Merge epic/91-serialization: serializer and writer options
Closes #91
2026-06-08 15:24:50 +02:00
cd2f30a262 feat(emitting): attribute quote style, line endings, xml:space, writer namespaces
Broaden the serializer and writer options from the audit:

- attributeQuote selects single or double quotes; the active quote is
  escaped in values (apostrophe as &apos; 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
2026-06-08 15:24:49 +02:00