moxml 0.5.149 → 0.5.151
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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-repos.json +7 -2
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.github/workflows/performance-release.yml +79 -0
- data/.github/workflows/performance.yml +59 -0
- data/Rakefile +23 -0
- data/benchmark/README.md +106 -0
- data/benchmark/scoreboard.json +115 -0
- data/benchmark/scoreboard.rb +556 -0
- data/bin/generate-model-coverage +21 -0
- data/docs/_pages/adapter-compatibility.adoc +3 -3
- data/docs/_pages/divergences.adoc +32 -41
- data/docs/_pages/ecosystem.adoc +62 -0
- data/docs/_pages/headed-ox-limitations.adoc +1 -1
- data/docs/_pages/headed-ox.adoc +2 -2
- data/docs/_pages/index.adoc +4 -0
- data/docs/_pages/model-coverage.adoc +57 -0
- data/docs/_pages/performance.adoc +29 -0
- data/docs/_pages/why-moxml.adoc +87 -0
- data/docs/_references/processing-model.adoc +42 -3
- data/lib/moxml/adapter/base.rb +80 -0
- data/lib/moxml/adapter/leptris.rb +238 -4
- data/lib/moxml/adapter/libxml.rb +34 -2
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +39 -7
- data/lib/moxml/adapter/ox.rb +8 -0
- data/lib/moxml/adapter/rexml.rb +17 -2
- data/lib/moxml/attribute_resolver.rb +4 -0
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +53 -2
- data/lib/moxml/document.rb +28 -4
- data/lib/moxml/element.rb +13 -1
- data/lib/moxml/node.rb +85 -11
- data/lib/moxml/node_set.rb +31 -1
- data/lib/moxml/relaxng.rb +62 -0
- data/lib/moxml/sax/block_handler.rb +7 -0
- data/lib/moxml/sax/handler.rb +20 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath.rb +28 -0
- data/lib/moxml.rb +1 -0
- data/spec/consistency/adapter_parity_spec.rb +22 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/model_coverage_audit_spec.rb +61 -86
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/xpath_idioms_spec.rb +23 -5
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_spec.rb +113 -3
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +33 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -6
- data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
- data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
- data/spec/moxml/c14n/subset_spec.rb +2 -12
- data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +45 -74
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/support/input_taxonomy.rb +7 -0
- data/spec/support/model_coverage.rb +159 -0
- 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.
|
|
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.
|
|
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
|
|
46
|
-
|
|
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:)`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
75
|
-
|
|
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.
|
|
82
|
-
|
|
83
|
-
|
|
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 (a transient Windows failure of one JSON-engine parity example — yeptris 0.6.34.2's undefined `MARSHAL_STRICT_DUP_KEYS` under json 3 — cleared with yeptris 0.6.34.3)
|
|
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:)`) | **Retired** (canon#281, merged): `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column; canon rides the facade.
|
|
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
|
|
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.
|
data/docs/_pages/headed-ox.adoc
CHANGED
|
@@ -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% -
|
|
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
|
|
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
|
data/docs/_pages/index.adoc
CHANGED
|
@@ -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`
|
|
155
|
-
|
|
156
|
-
|
|
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
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -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
|
|
@@ -507,6 +580,13 @@ namespace_validation_mode: :strict)
|
|
|
507
580
|
false
|
|
508
581
|
end
|
|
509
582
|
|
|
583
|
+
# Whether the mint pre-writes the consumer's contract ivars
|
|
584
|
+
# in C (create_primed_element), letting Document#create_element
|
|
585
|
+
# skip its Ruby prime pass. Base: never.
|
|
586
|
+
def mints_primed?
|
|
587
|
+
false
|
|
588
|
+
end
|
|
589
|
+
|
|
510
590
|
# Whether add_child can keep tracking the same native —
|
|
511
591
|
# adapters that may recreate the node on attach (libxml's
|
|
512
592
|
# doc.root=) override to false so the wrapper refresh path
|