moxml 0.5.149 → 0.5.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +7 -2
  3. data/.github/workflows/dependent-tests.yml +4 -1
  4. data/.github/workflows/performance-release.yml +79 -0
  5. data/.github/workflows/performance.yml +59 -0
  6. data/Rakefile +23 -0
  7. data/benchmark/README.md +106 -0
  8. data/benchmark/scoreboard.json +115 -0
  9. data/benchmark/scoreboard.rb +556 -0
  10. data/bin/generate-model-coverage +21 -0
  11. data/docs/_pages/adapter-compatibility.adoc +3 -3
  12. data/docs/_pages/divergences.adoc +32 -41
  13. data/docs/_pages/ecosystem.adoc +62 -0
  14. data/docs/_pages/headed-ox-limitations.adoc +1 -1
  15. data/docs/_pages/headed-ox.adoc +2 -2
  16. data/docs/_pages/index.adoc +4 -0
  17. data/docs/_pages/model-coverage.adoc +57 -0
  18. data/docs/_pages/performance.adoc +29 -0
  19. data/docs/_pages/why-moxml.adoc +87 -0
  20. data/docs/_references/processing-model.adoc +42 -3
  21. data/lib/moxml/adapter/base.rb +73 -0
  22. data/lib/moxml/adapter/leptris.rb +183 -4
  23. data/lib/moxml/adapter/libxml.rb +34 -2
  24. data/lib/moxml/adapter/nokogiri.rb +10 -0
  25. data/lib/moxml/adapter/oga.rb +39 -7
  26. data/lib/moxml/adapter/ox.rb +8 -0
  27. data/lib/moxml/adapter/rexml.rb +17 -2
  28. data/lib/moxml/c14n/data_model.rb +8 -0
  29. data/lib/moxml/c14n/processor.rb +17 -8
  30. data/lib/moxml/context.rb +28 -0
  31. data/lib/moxml/document.rb +17 -3
  32. data/lib/moxml/node.rb +56 -5
  33. data/lib/moxml/node_set.rb +31 -1
  34. data/lib/moxml/relaxng.rb +62 -0
  35. data/lib/moxml/sax/block_handler.rb +7 -0
  36. data/lib/moxml/sax/handler.rb +20 -0
  37. data/lib/moxml/version.rb +1 -1
  38. data/lib/moxml/xpath.rb +28 -0
  39. data/lib/moxml.rb +1 -0
  40. data/spec/consistency/adapter_parity_spec.rb +22 -0
  41. data/spec/consistency/citation_hygiene_spec.rb +30 -0
  42. data/spec/consistency/model_coverage_audit_spec.rb +61 -86
  43. data/spec/consistency/published_docs_spec.rb +49 -0
  44. data/spec/consistency/xpath_idioms_spec.rb +23 -5
  45. data/spec/integration/shared_examples/edge_cases.rb +0 -13
  46. data/spec/moxml/adapter/leptris_spec.rb +23 -3
  47. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
  48. data/spec/moxml/attribute_resolver_spec.rb +33 -0
  49. data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -6
  50. data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
  51. data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
  52. data/spec/moxml/c14n/subset_spec.rb +2 -12
  53. data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
  54. data/spec/moxml/element_indent_spec.rb +34 -0
  55. data/spec/moxml/node_spec.rb +0 -3
  56. data/spec/moxml/relax_ng_spec.rb +67 -0
  57. data/spec/moxml/sax_spec.rb +45 -74
  58. data/spec/moxml/xpath_capabilities_spec.rb +11 -38
  59. data/spec/support/input_taxonomy.rb +7 -0
  60. data/spec/support/model_coverage.rb +159 -0
  61. metadata +17 -2
@@ -19,74 +19,58 @@ here. Contract:
19
19
  |===
20
20
  | ID | Adapter(s) | Divergence
21
21
 
22
- | D-01
23
- | oga
24
- | The `xmlns:` default-namespace XPath idiom is unsupported by oga's
25
- own engine (explicit bindings work). Pinned in
26
- `spec/consistency/xpath_idioms_spec.rb`.
27
-
28
22
  | D-02
29
23
  | oga
30
24
  | Raw CR preserved in text content — §2.11 EOL normalization not
31
- applied at parse. Pinned in `spec/consistency/eol_normalization_spec.rb`.
25
+ applied at parse. Decided permanent: normalization belongs at the
26
+ engine's parse layer; a facade read arm would only cover the read
27
+ verbs it touches and change content/source-position consistency.
28
+ Pinned in `spec/consistency/eol_normalization_spec.rb`.
32
29
 
33
30
  | D-03
34
31
  | ox, headed_ox
35
- | `
` character references collapse to LF. Pinned in
32
+ | `
` character references collapse to LF. Decided permanent:
33
+ the reference-vs-literal distinction is lost at parse, so no facade
34
+ read can recover it. Pinned in
36
35
  `spec/consistency/eol_normalization_spec.rb`.
37
36
 
38
- | D-04
39
- | oga
40
- | The XML declaration surfaces as a `<?xml?>` PI; PI content keeps its
41
- separator space. Pinned in `spec/moxml/c14n/adapter_delegation_spec.rb`.
42
-
43
37
  | D-05
44
38
  | ox, headed_ox
45
- | PI content dropped at parse. Pinned in
46
- `spec/moxml/c14n/adapter_delegation_spec.rb`.
39
+ | PI content is dropped on parse for element-child PIs, XPath-minted
40
+ wrappers, and quoted data (some document-level shapes retain it).
41
+ Pinned in `spec/moxml/c14n/adapter_delegation_spec.rb` and the
42
+ taxonomy differential.
47
43
 
48
44
  | D-06
49
45
  | ox, headed_ox
50
46
  | Comment content padding stripped at parse. Pinned in
51
47
  `spec/moxml/c14n/subset_spec.rb`.
52
48
 
53
- | D-07
54
- | rexml
55
- | `text()` node tests return no text nodes (adapter XPath capability
56
- gap, both axes). Pinned in `spec/moxml/c14n/subset_spec.rb`.
57
-
58
- | D-08
59
- | c14n subsystem
60
- | Document-level PI/comment rendering not yet implemented (all
61
- adapters; a subsystem TODO, not an engine divergence). Tracked in
62
- `TODO.c14n/09-comments-pis-tests.md`.
63
-
64
49
  | D-09
65
50
  | oga, ox, headed_ox, libxml, leptris
66
- | Element-level `to_xml(indent:)` silently ignored (libxml emits a
67
- partial, unindented newline form). Document-level HTML/XHTML
68
- indentation is honored on leptris/nokogiri and raises on incapable
69
- engines. Pinned in
51
+ | Element-level `to_xml(indent:)` is not supported by these engines:
52
+ the facade raises the named `Moxml::AdapterError` instead of
53
+ answering unindented output; `indent: 0` and the no-option path stay
54
+ common. The engine-level `serialize(indent:)` lane (direct
55
+ `Adapter.serialize`) still ignores or partially applies the knob.
56
+ Pinned in `spec/moxml/element_indent_spec.rb` and
70
57
  `spec/moxml/adapter/shared_examples/adapter_contract.rb`.
71
58
 
72
59
  | D-10
73
60
  | ox, headed_ox
74
- | SAX emits no separate CDATA/comment/processing-instruction events
75
- (sequences fold into text or are dropped). Pinned in
61
+ | SAX reports no separate CDATA/comment/processing-instruction
62
+ events. A handler that subscribes to one of those families gets the
63
+ named `Moxml::NotImplementedError` — loud, never a silently
64
+ incomplete stream. Text-only handlers still receive CDATA folded
65
+ into character data as the engine delivers it. Pinned in
76
66
  `spec/moxml/sax_spec.rb`.
77
67
 
78
68
  | D-11
79
69
  | oga
80
70
  | SAX parse is more lenient with malformed input than the model's
81
- error authority. Pinned in `spec/moxml/sax_spec.rb`.
82
-
83
- | D-12
84
- | ox, headed_ox
85
- | The XPath translation covers a subset: no positional predicates,
86
- attribute-value predicates, namespace/default-namespace tests,
87
- `position()`/`count()`, `text()`, boolean expressions, `child::`/
88
- `descendant::` axes, or unions. Pinned in
89
- `spec/moxml/xpath_capabilities_spec.rb` and `spec/moxml/node_spec.rb`.
71
+ error authority. Decided permanent: the engine owns error reporting,
72
+ and pre-validating with a strict parse would defeat the streaming
73
+ lane. Pinned in `spec/moxml/sax_spec.rb`.
90
74
 
91
75
  | D-13
92
76
  | ox, headed_ox
@@ -145,4 +129,11 @@ silent on text-node coalescing (an open decision). Surfaced by the
145
129
  generated metamorphic differential (M5). Pinned in
146
130
  `spec/consistency/metamorphic_properties_spec.rb` through
147
131
  `InputGenerator::FEATURE_EXEMPT`.
132
+
133
+ | D-20
134
+ | rexml
135
+ | Trailing whitespace in processing-instruction data is dropped at
136
+ parse (the engine's instruction content is trimmed); leading
137
+ separator runs collapse per the model. Pinned in the taxonomy
138
+ differential (`pi trailing whitespace`, face `pi_content`).
148
139
  |===
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Ecosystem
3
+ nav_order: 12
4
+ ---
5
+
6
+ == Ecosystem closure
7
+
8
+ Moxml's consumer test: **no consumer needs to know which engine is
9
+ running, and every consumer suite runs against moxml main in this
10
+ repository's CI.** The dependent matrix
11
+ (`.github/workflows/dependent-tests.yml`, repo list in
12
+ `dependent-repos.json`) binds each consumer to the checkout at the
13
+ workflow's ref and runs its own suite — the consumer's suite is the
14
+ validation target, never a stand-in.
15
+
16
+ === Dependent matrix
17
+
18
+ [cols="2,1,3",options="header"]
19
+ |===
20
+ | Repository | Tools | Suite (validated 2026-10-21)
21
+
22
+ | `lutaml/lutaml-model` | — | 6213+ examples green on the released pair
23
+
24
+ | `lutaml/canon` | — | 2573 examples, 0 failures locally; in CI the Windows leg fails one JSON-engine parity example (`yaml_engine_parity_spec.rb:123`, the resolved json gem's duplicate-key verdict) — unrelated to XML processing, owner-held in canon
25
+
26
+ | `metanorma/metanorma-standoc` | ghostscript | 627 examples green in CI on main (the boilerplate-fixture failure seen during the 04/05 validation was resolved by the 06 adapter fixes; a few local-only failures still need generated assets)
27
+
28
+ | `metanorma/metanorma-ietf` | xml2rfc | 405 examples, 0 failures
29
+
30
+ | `lutaml/expressir` | — | 2026 examples, 0 failures
31
+
32
+ | `metanorma/svg_conform` | — | 350 examples, 0 failures
33
+ |===
34
+
35
+ === Seam audit
36
+
37
+ An *engine-direct seam* is consumer code that reaches past the facade
38
+ into the engine. Each is retired with a facade capability, or kept as
39
+ a declared, decided lane.
40
+
41
+ [cols="1,2,3",options="header"]
42
+ |===
43
+ | Consumer | Use | Disposition
44
+
45
+ | canon | HTML5 parse-lint (`parse_html(collect_errors:)`) | **Facade capability shipped**: `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column. canon adopts it in canon#281, which is owner-held (moxml does not merge canon PRs).
46
+
47
+ | canon | c14n native delegation (`doc.native.c14n`) | **Decided lane**: canon owns the c14n reference implementation and byte-probes the engine against its Ruby path; it is the backend behind moxml's own native delegation.
48
+
49
+ | metanorma-standoc | RELAX NG validation (`Leptris::XML::RelaxNG`) | **Facade capability added**: `Moxml::RelaxNG` (`Context#relaxng` / `#relaxng_file`, `valid?` / `validate` / `validate_errors`), loud `NotImplementedError` on engines without an RNG validator. Consumer migration tracked in standoc.
50
+
51
+ | svg_conform | engine parsing and SAX (`Leptris::XML.parse`, `Leptris::XML::SAX`) | **Consumer work**: moxml's `Context#parse` and `Moxml::SAX` cover both; tracked for the migration.
52
+
53
+ | standoc / svg_conform | hard adapter selection (`Moxml.new(:leptris)`) | **Supported**: adapter selection is the deployment decision the facade exists for.
54
+ |===
55
+
56
+ === Cap policy
57
+
58
+ A consumer pin (for example `moxml ~> 0.5.84`) is a dependency
59
+ constraint, not a knowledge of the engine. Caps move only after the
60
+ consumer's own suite runs green against the released pair — the
61
+ relaton-only lesson: evidence from one dependent suite does not
62
+ transfer to another.
@@ -571,4 +571,4 @@ HeadedOx v1.2 successfully delivers on its core promise: **fast XML parsing with
571
571
  - Heavy DOM modifications required
572
572
  - 100% feature parity is critical
573
573
 
574
- The documented limitations are transparent, well-understood, and unlikely to affect most XML processing workflows. HeadedOx fills an important niche in the Ruby XML ecosystem as the "fast XPath" option.
574
+ The known limitations are transparent, well-understood, and unlikely to affect most XML processing workflows. HeadedOx fills an important niche in the Ruby XML ecosystem as the "fast XPath" option.
@@ -658,7 +658,7 @@ following = items[current_index + 1..-1]
658
658
  ----
659
659
  Total Tests: 2,008
660
660
  Passing: 1,992 (99.20%)
661
- Skipped: 16 (0.80% - documented Ox limitations)
661
+ Skipped: 16 (0.80% - known Ox limitations; see link:divergences.html[the register])
662
662
  Failures: 0
663
663
 
664
664
  Test Categories:
@@ -686,7 +686,7 @@ Production Use: Ready
686
686
 
687
687
  === Known limitations with test status
688
688
 
689
- All 16 limitations are comprehensively documented:
689
+ All 16 limitations are catalogued below:
690
690
 
691
691
  . **Attribute wildcards** (3 tests) - Ox `locate()` doesn't support `@*`
692
692
  . **Namespace introspection** (4 tests) - Ox doesn't expose namespace data
@@ -8,6 +8,10 @@ nav_order: 1
8
8
  This section covers the fundamental concepts and essential information for
9
9
  working with Moxml.
10
10
 
11
+ Start with link:why-moxml[Why Moxml] for the invariant and the evidence
12
+ apparatus, then link:model-coverage[Model coverage] for the
13
+ adapter-by-adapter proof.
14
+
11
15
  === Topics covered
12
16
 
13
17
  link:installation[Installation]::
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Model coverage
3
+ nav_order: 5
4
+ ---
5
+
6
+ == Model coverage
7
+
8
+ Every face family of the
9
+ link:../_references/processing-model[processing model] is governed on
10
+ every adapter: **equivalent** by contract, **loud** (a named raise), or
11
+ a **registered divergence**. The table below is generated from the
12
+ coverage matrix that the consistency audit enforces, so the published
13
+ proof cannot drift from the tested proof.
14
+
15
+ The evidence chain is checkable at every link: the model section names
16
+ the specs that enforce it (`Pins::`), each enforcing spec cites the
17
+ model section, the matrix maps every face family to its section and
18
+ pin, and the divergence IDs resolve in
19
+ link:divergences.html[the register].
20
+
21
+ // MODEL-COVERAGE:BEGIN
22
+ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the divergence register. Do not edit by hand: `spec/consistency/published_docs_spec.rb` fails when this block is stale.
23
+
24
+ [cols="4,1,1,1,1,1,1,1",options="header"]
25
+ |===
26
+ | Face family | nokogiri | oga | rexml | ox | headed ox | libxml | leptris
27
+ | document order and children listing | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
28
+ | name resolution | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
29
+ | namespace resolution and bindings normalization | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
30
+ | attribute access | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
31
+ | text/content/raw_content | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
32
+ | parsing modes and diagnostics | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
33
+ | EOL normalization | equivalent | D-02 | equivalent | D-03 | equivalent | equivalent | equivalent
34
+ | entity modes | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
35
+ | XPath evaluation and declared idioms | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
36
+ | serialization shapes (xml/html/xhtml) | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
37
+ | indentation knobs | equivalent | D-09 | equivalent | D-09 | D-09 | D-09 | D-09
38
+ | canonicalization | equivalent | equivalent | equivalent | D-05 | D-05 | equivalent | equivalent
39
+ | XSLT | equivalent | loud (NotImplementedError) | loud (NotImplementedError) | loud (NotImplementedError) | loud (NotImplementedError) | loud (NotImplementedError) | equivalent
40
+ | SAX | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
41
+ | materialization | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
42
+ | loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
43
+ |===
44
+
45
+ *equivalent* — all adapters answer the same observable result. *loud (...)* — the adapter raises the named error for that face. *D-NN* — a registered divergence in link:divergences.html[the register], with its rationale. Every cell cites a pin; see the processing model's `Pins::` line for the enforcing specs.
46
+ // MODEL-COVERAGE:END
47
+
48
+ == Reproducing
49
+
50
+ `bundle exec rake docs:coverage` regenerates the block above from
51
+ `spec/support/model_coverage.rb`. `spec/consistency/published_docs_spec.rb`
52
+ fails when the published block is stale; the consistency suite runs it
53
+ on every CI build.
54
+
55
+ The performance commitment has its own published page:
56
+ link:performance.html[Performance], regenerated from the scoreboard
57
+ harness at release time.
@@ -5,6 +5,35 @@ nav_order: 11
5
5
 
6
6
  == Performance considerations
7
7
 
8
+ === Scoreboard (generated)
9
+
10
+ `SCORE_PUBLISH=1 bundle exec ruby benchmark/scoreboard.rb` rewrites
11
+ the marked block below from a live measurement; see
12
+ `benchmark/README.md` for the protocol and row definitions.
13
+
14
+ // SCOREBOARD:BEGIN
15
+ Generated by `SCORE_PUBLISH=1 bundle exec ruby benchmark/scoreboard.rb`. Protocol: CPU-time medians of an interleaved A/B run (raw nokogiri vs moxml on leptris); the ratio is baseline / candidate, so > 1.0 means the facade is faster. Warmups discarded, GC between samples.
16
+
17
+ Measurement: Ruby 3.4.8, leptris 1.9.374.0, nokogiri 1.19.4, 2026-10-20T14:53:54Z, 1-minute load 34.01, 9 repetitions.
18
+
19
+ [cols="2,1,1,1",options="header"]
20
+ |===
21
+ | Row | Ratio | Kind | Status
22
+ | `parse` | 2.59x | parity | met
23
+ | `serialize` | 1.35x | parity | met
24
+ | `xpath //field (count)` | 1.20x | parity | met
25
+ | `xpath //field (each)` | 0.34x | structural | tracked (below parity)
26
+ | `attr pairs listing` | 8.16x | parity | met
27
+ | `build add_element` | 0.60x | structural | tracked (below parity)
28
+ | `walk+read (warm)` | 0.77x | structural | tracked (below parity)
29
+ | `walk+read (cold)` | 0.22x | structural | tracked (below parity)
30
+ | `children recursion (cold)` | 0.27x | structural | tracked (below parity)
31
+ | `plan records (typed)` | 0.79x | data | informational
32
+ |===
33
+
34
+ *Parity rows* must stay at or above the 0.95 measurement floor. *Structural rows* are measured and trend-locked (10% regression tolerance) while their gaps remain open: per-node wrapper minting costs (#312 endgame) and engine construction. *Data API* rows are informational.
35
+ // SCOREBOARD:END
36
+
8
37
  === Memory management
9
38
 
10
39
  Moxml maintains a node registry to ensure consistent object mapping:
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: Why Moxml
3
+ nav_order: 2
4
+ ---
5
+
6
+ == Why Moxml
7
+
8
+ Moxml exists to make **"which XML engine am I running?" a deployment
9
+ decision instead of an API decision**.
10
+
11
+ Ruby's XML engines — Nokogiri, Oga, REXML, Ox, LibXML, Leptris — each
12
+ carry their own object model, namespace resolution, XPath coverage,
13
+ entity policy, serialization shapes, and error surfaces. An application
14
+ that starts on one cannot move to another without a rewrite; a library
15
+ that supports several accumulates adapter-specific branches and tests.
16
+ Moxml replaces that with a single processing model, specified once and
17
+ enforced on every adapter.
18
+
19
+ == The invariant
20
+
21
+ **Engine-choice invariance:** any program written against the Moxml
22
+ facade produces the same observable outcome on every adapter.
23
+ Performance may vary across engines — semantics may not.
24
+
25
+ Three clauses govern every change:
26
+
27
+ . **Equivalence is to the model, not merely adapter-to-adapter.** Two
28
+ engines can agree and both be wrong; the normative document is the
29
+ reference.
30
+ . **Loud degradation, never silent divergence.** An unsupported
31
+ capability raises a named error. A different answer is a bug.
32
+ . **A divergence is fixed, promoted to specification, or registered.**
33
+ The register names it, explains it, and pins the tests that observe
34
+ it. An undocumented divergence is a bug.
35
+
36
+ == How the claim is proved
37
+
38
+ The proof apparatus is itself part of the project, and every link is
39
+ checked in CI:
40
+
41
+ [cols="1,3"]
42
+ |===
43
+ | Link | What it establishes
44
+
45
+ | link:../_references/processing-model[Processing model]
46
+ | The single normative statement of every semantic decision, with a
47
+ `Pins::` line naming the specs that enforce each section.
48
+
49
+ | link:model-coverage.html[Model coverage]
50
+ | The face-family × adapter matrix: equivalent, loud, or a registered
51
+ divergence. Generated from the same data the consistency audit
52
+ enforces.
53
+
54
+ | link:divergences.html[Divergence register]
55
+ | Every known difference, with its engine cause, its scope, and its
56
+ pin. Entries are fixed, loud, or decided permanent trades.
57
+
58
+ | link:performance.html[Performance]
59
+ | The published scoreboard: parity rows are commitments; structural
60
+ gaps are measured and trend-locked, never hidden.
61
+
62
+ |===
63
+
64
+ Behind the pages, the consistency suite runs an engine-free
65
+ **executable reference** (a pure-Ruby reader, writer, and normalizer)
66
+ against every adapter; a seeded **generator** checks metamorphic
67
+ properties over generated documents; and a whole-class **taxonomy
68
+ differential** compares every adapter on every input class the model
69
+ touches. The reference is itself transpiled to JavaScript and runs on
70
+ the Opal lane.
71
+
72
+ == What Moxml is not
73
+
74
+ Moxml is not a new XML parser and does not replace the engines — it
75
+ makes them interchangeable. It is not a performance claims contest
76
+ either: performance is a declared, non-semantic channel, and the
77
+ scoreboard reports below-parity rows as plainly as parity rows.
78
+
79
+ == The consumer commitment
80
+
81
+ - Upgrading an engine version never changes your program's semantics.
82
+ - Capabilities that an engine lacks raise a named error at the call
83
+ site, not a surprising result.
84
+ - Every difference you can observe has a register ID and a test.
85
+ - The same program runs on Nokogiri, Oga, REXML, Ox, LibXML, and
86
+ Leptris — and on a future engine that adopts the model with zero
87
+ code changes.
@@ -17,6 +17,8 @@ skip with their entry ID — an undocumented divergence is a bug.
17
17
 
18
18
  === Status and precedence
19
19
 
20
+ Pins:: none
21
+
20
22
  1. This document — the model.
21
23
  2. `spec/consistency/` — the model pinned as executable expectations,
22
24
  with `lib/moxml/reference/` as the **engine-free executable
@@ -37,6 +39,8 @@ A semantics change amends this document first (see
37
39
 
38
40
  === Mission and invariant
39
41
 
42
+ Pins:: none
43
+
40
44
  Moxml exists to make "which XML engine am I running?" a **deployment
41
45
  decision instead of an API decision**. The invariant that enables it:
42
46
  any program written against the facade produces the same observable
@@ -50,6 +54,8 @@ divergences, and performance.
50
54
 
51
55
  === Object model
52
56
 
57
+ Pins:: none
58
+
53
59
  Node kinds:: `Document`, `Element`, `Attribute`, `Text`, `Comment`,
54
60
  `Cdata`, `Declaration`, `Doctype`, `ProcessingInstruction`,
55
61
  `Namespace`, `EntityReference`.
@@ -74,6 +80,8 @@ listings or serialization child walks.
74
80
 
75
81
  === Document order and traversal
76
82
 
83
+ Pins:: `spec/consistency/adapter_parity_spec.rb`
84
+
77
85
  * `children` lists nodes in document order.
78
86
  * Walks (`each_node`, `materialize`) visit depth-first, pre-order.
79
87
  * `Document#children` and `#root` never include the declaration;
@@ -87,6 +95,8 @@ listings or serialization child walks.
87
95
 
88
96
  === Names and namespaces
89
97
 
98
+ Pins:: `spec/consistency/xpath_idioms_spec.rb`
99
+
90
100
  * Names are **expanded names**: a name test matches by namespace *URI*,
91
101
  never by prefix spelling.
92
102
  * Unprefixed element name tests match **no-namespace** elements only
@@ -102,6 +112,8 @@ listings or serialization child walks.
102
112
 
103
113
  === Access semantics
104
114
 
115
+ Pins:: `spec/moxml/attribute_resolver_spec.rb`, `spec/moxml/text_spec.rb`
116
+
105
117
  `text`:: Concatenated descendant text (elements); a text node's own
106
118
  content; entity references restored when the document is
107
119
  entity-bearing.
@@ -126,6 +138,8 @@ with an unambiguous local name.
126
138
 
127
139
  === Parsing
128
140
 
141
+ Pins:: `spec/consistency/eol_normalization_spec.rb`, `spec/moxml/document_parse_diagnostics_spec.rb`, `spec/moxml/entity_spec.rb`
142
+
129
143
  * Modes: strict (errors raise) and recover (errors surface via
130
144
  `parse_diagnostics` / `parse_errors`, both `[{kind:, message:}]`).
131
145
  * HUGE mode: over-cap text (> 10 MB) parses completely.
@@ -139,6 +153,8 @@ with an unambiguous local name.
139
153
 
140
154
  === XPath evaluation
141
155
 
156
+ Pins:: `spec/consistency/xpath_idioms_spec.rb`
157
+
142
158
  * XPath 1.0 semantics on every adapter. Engines without an evaluator
143
159
  (Ox, HeadedOx; leptris for shapes its engine cannot evaluate) run
144
160
  **moxml's own pure-Ruby engine** — the model's semantics do not
@@ -151,12 +167,17 @@ with an unambiguous local name.
151
167
  `xmlns:` default-namespace matching; attribute terminals in every
152
168
  position (`//a/@id`, `//@id`, `r/a/@id`); symbol- and string-keyed
153
169
  bindings resolve identically.
154
- * `at_xpath` is sugar over `xpath`: it answers the first node of a
155
- node-set and passes scalar results (`count()`, `string()`,
156
- `boolean()`) through unwrapped, uniformly across adapters.
170
+ * `at_xpath` answers the first node of a node-set and passes scalar
171
+ results (`count()`, `string()`, `boolean()`) through unwrapped,
172
+ uniformly across adapters. Adapters whose at_xpath lane falls back
173
+ to the reference engine when the native answer is empty (leptris)
174
+ use it; the rest evaluate `xpath` and take the first node. Both
175
+ lanes answer the same model.
157
176
 
158
177
  === Serialization
159
178
 
179
+ Pins:: `spec/moxml/to_html_spec.rb`, `spec/moxml/xhtml_output_spec.rb`
180
+
160
181
  [cols="1,3"]
161
182
  |===
162
183
  | Shape | Contract
@@ -188,15 +209,25 @@ with an unambiguous local name.
188
209
 
189
210
  === Canonicalization
190
211
 
212
+ Pins:: `spec/moxml/c14n/adapter_delegation_spec.rb`
213
+
191
214
  * Algorithms: Inclusive 1.0, Exclusive 1.0, Inclusive 1.1, plus
192
215
  XPath-driven subset canonicalization (enveloped-signature oracle).
193
216
  * Native delegations (leptris engine, libxml2) answer **byte-identical**
194
217
  output to the Ruby reference path (byte-safety probed before
195
218
  engagement).
219
+ * Document-level comments and PIs render in document order, separated
220
+ from adjacent nodes by exactly one line feed; the form starts and
221
+ ends with its first/last node (no leading or trailing line feed).
222
+ The XML declaration is document state and never part of the form.
223
+ * PI data excludes the separating whitespace after the target; the
224
+ canonical form joins target and non-empty data with a single space.
196
225
  * `Moxml::C14n.equivalent?` is the algorithm-aware comparison.
197
226
 
198
227
  === XSLT
199
228
 
229
+ Pins:: `spec/consistency/xslt_spec.rb`
230
+
200
231
  XSLT 1.0 through engines that ship a processor (libxslt; leptris
201
232
  1.0–3.0). Compilation and transform failures raise named
202
233
  `Moxml::XsltError`s. Engines without a processor raise
@@ -204,6 +235,8 @@ XSLT 1.0 through engines that ship a processor (libxslt; leptris
204
235
 
205
236
  === SAX
206
237
 
238
+ Pins:: `spec/moxml/sax_spec.rb`
239
+
207
240
  * The callback stream (`on_start_element`, `on_characters`, `on_error`,
208
241
  …) is the authoritative event sequence.
209
242
  * The record-drain fast path (`on_sax_records`) replays **exactly** the
@@ -213,6 +246,8 @@ XSLT 1.0 through engines that ship a processor (libxslt; leptris
213
246
 
214
247
  === Materialization
215
248
 
249
+ Pins:: `spec/moxml/plan_conformance_spec.rb`
250
+
216
251
  `materialize`, `materialize_fields`, `Plan`, and `StructPlan` produce
217
252
  **identical records across all adapters** (conformance spec). Plan name
218
253
  matching is global; unmatched subtrees are dropped per the pinned
@@ -220,6 +255,8 @@ semantics.
220
255
 
221
256
  === Errors and loud degradation
222
257
 
258
+ Pins:: `spec/consistency/loud_degradation_spec.rb`
259
+
223
260
  * `Moxml::Error` hierarchy; `Moxml::XPathError`, `Moxml::XsltError`,
224
261
  `Moxml::AdapterError` (e.g. schema-handle/adapter misroute),
225
262
  `Moxml::NotImplementedError` for unsupported capability contracts.
@@ -231,6 +268,8 @@ semantics.
231
268
 
232
269
  === Performance: a declared, non-semantic channel
233
270
 
271
+ Pins:: none
272
+
234
273
  Acceleration (native read layers, C minting, record drains, plan
235
274
  streams) always lands behind version + face gates and degrades to the
236
275
  spec-pinned fallback. Cross-adapter performance variance is sanctioned
@@ -178,6 +178,18 @@ module Moxml
178
178
  method(:sax_parse).owner != Moxml::Adapter::Base.singleton_class
179
179
  end
180
180
 
181
+ # Whether the SAX engine reports an event family as distinct
182
+ # events. Engines that cannot must raise the named error when
183
+ # the handler subscribes (model, SAX: the callback stream is
184
+ # authoritative; absence is loud, never a silent fold).
185
+ #
186
+ # @param family [Symbol] :cdata, :comment, or
187
+ # :processing_instruction
188
+ # @return [Boolean]
189
+ def sax_event_family_supported?(_family)
190
+ true
191
+ end
192
+
181
193
  def create_document(_native_doc = nil)
182
194
  raise Moxml::NotImplementedError.new(
183
195
  "create_document not implemented",
@@ -312,6 +324,30 @@ module Moxml
312
324
  # validator raise; leptris gates on >= 1.9.321.0.
313
325
  XSD_SUPPORTED = false
314
326
 
327
+ # RELAX NG validation (moxml RelaxNG contract): the same
328
+ # loud-capability rule.
329
+ RELAXNG_SUPPORTED = false
330
+
331
+ def relaxng_compile(_schema_source)
332
+ raise Moxml::NotImplementedError,
333
+ "RELAX NG validation requires an adapter with an RNG engine"
334
+ end
335
+
336
+ def relaxng_compile_file(_path)
337
+ raise Moxml::NotImplementedError,
338
+ "RELAX NG validation requires an adapter with an RNG engine"
339
+ end
340
+
341
+ def relaxng_validate_errors(_handle, _native_document)
342
+ raise Moxml::NotImplementedError,
343
+ "RELAX NG validation requires an adapter with an RNG engine"
344
+ end
345
+
346
+ def relaxng_valid?(_handle, _native_document)
347
+ raise Moxml::NotImplementedError,
348
+ "RELAX NG validation requires an adapter with an RNG engine"
349
+ end
350
+
315
351
  # Issue #384: schema handles are adapter-native objects — a
316
352
  # handle compiled through one adapter must not dispatch
317
353
  # through another. Adapters with an XSD engine answer their
@@ -499,6 +535,43 @@ namespace_validation_mode: :strict)
499
535
  node
500
536
  end
501
537
 
538
+ # Whether add_next_sibling/add_previous_sibling answer the
539
+ # native actually installed (libxml imports cross-document
540
+ # fragments and answers the copy) — the facade then refreshes
541
+ # the caller's wrapper to it.
542
+ def add_sibling_returns_inserted?
543
+ false
544
+ end
545
+
546
+ # Whether element-level to_xml(indent:) is honored. The
547
+ # facade raises a named AdapterError on incapable engines
548
+ # instead of returning unindented output silently (D-09).
549
+ def element_indent_supported?
550
+ false
551
+ end
552
+
553
+ # Whether adapter.at_xpath is safe as the primary first-node
554
+ # lane. leptris: its at_xpath falls back to the model's Ruby
555
+ # engine when the native lane answers nothing (an engine index
556
+ # left stale by sibling insertion), so it reflects the live
557
+ # tree where the node-set lane does not.
558
+ def native_at_xpath?
559
+ false
560
+ end
561
+
562
+ # libxml2 (nokogiri and libxml-ruby alike) rejects an empty
563
+ # namespace URI in an XPath binding context, but the model's
564
+ # `xmlns:` idiom must be able to address the no-namespace
565
+ # default. Adapters route such expressions through the
566
+ # reference engine.
567
+ def empty_default_namespace_binding?(namespaces)
568
+ return false unless namespaces
569
+
570
+ namespaces.any? do |prefix, uri|
571
+ prefix.to_s == "xmlns" && uri.to_s.empty?
572
+ end
573
+ end
574
+
502
575
  # Whether children() results need per-child patch_node
503
576
  # rewriting. Adapters whose natives arrive pre-wrapped (all but
504
577
  # ox and libxml) answer false so the wrapper layer can skip