Commit Graph

11 Commits

Author SHA1 Message Date
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
7f1aada240 feat(dtd): conditional sections, notations, NDATA, external parameter entities
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
2026-06-08 14:39:42 +02:00
a041b8beca feat(dtd): attribute defaulting, tokenized types, value normalization
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
2026-06-08 14:31:42 +02:00
d682f85daf refactor(validation): migrate Schematron, retire Issue
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.
2026-06-08 12:49:19 +02:00
89f8ccc95a refactor(validation): migrate DTD validation onto the framework
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.
2026-06-08 12:36:56 +02:00
1fbe359ada feat(validation): composable validation framework + structural rule
Slice 1 of #92. Introduce the OpenAPIKit-idiom validation framework, replacing
the imperative if-tree structural validator:

- ValidationError (reason + coding path) renders "<reason> at path: root/row/cell";
  ValidationErrorCollection gathers every failure and is thrown once.
- Validation<Subject, Document> is the atomic, composable rule value, with the
  multi-error and single-error Bool inits (the latter auto-forms "Failed to
  satisfy: <description>"). Predicate-gated, type-directed via an erased
  AnyValidation that guards an optional from matching a non-optional rule.
- The framework is parameterized over both Subject and Document so the structural,
  DTD, XSD, and Schematron layers share one machinery (Document = Void for rules
  that need only their subject).
- Validator<Document> walks the node tree, building the coding path, with blank /
  default factories and fluent validating / withoutValidating.
- The combinator algebra (&&, ||, comparison lifts, take, lift, unwrap, all) as
  free functions returning closures.

The structural duplicate-attribute rule is reborn as Validation<Element, Void>.
Combinators and the erased wrapper are unit-tested as pure functions per the rule.

The legacy Validation.Issue stays only for the not-yet-migrated schema validators
and is retired in a later slice.
2026-06-08 12:00:34 +02:00
80fffd8127 feat(validation): Schematron rule validation
Add PureXML.Validation.Schematron: compile a Schematron schema (matching
schema/pattern/rule/assert/report by local name, so the ISO and legacy
namespaces both work) and validate a document. A rule's context XPath
selects the nodes it fires on; each assert/report test is evaluated relative
to the node via the XPath context-node API. A failed assert is an error, a
matched report a warning; within a pattern a node fires the first matching
rule. Adds XPath.Query.nodes(over:) returning matched tree nodes so one tree
can be queried repeatedly.

Closes #52
Closes #53
Closes #25
2026-06-08 05:58:55 +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
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