moxml 0.5.148 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-repos.json +7 -2
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.github/workflows/nightly-fuzz.yml +80 -0
- 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 +88 -39
- 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 +69 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +293 -23
- data/lib/moxml/adapter/libxml.rb +179 -25
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +71 -9
- data/lib/moxml/adapter/ox.rb +34 -2
- data/lib/moxml/adapter/rexml.rb +82 -17
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +33 -1
- data/lib/moxml/document.rb +18 -4
- data/lib/moxml/node.rb +75 -8
- data/lib/moxml/node_set.rb +42 -1
- data/lib/moxml/reference/normalizer.rb +63 -0
- data/lib/moxml/reference/parser.rb +417 -0
- data/lib/moxml/reference/writer.rb +121 -0
- data/lib/moxml/reference.rb +205 -0
- 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/compiler.rb +17 -2
- data/lib/moxml/xpath/ordering.rb +97 -0
- data/lib/moxml/xpath.rb +29 -0
- data/lib/moxml.rb +2 -0
- data/spec/consistency/adapter_parity_spec.rb +24 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/differential_taxonomy_spec.rb +117 -0
- data/spec/consistency/divergence_register_spec.rb +9 -1
- data/spec/consistency/eol_normalization_spec.rb +2 -0
- data/spec/consistency/loud_degradation_spec.rb +2 -0
- data/spec/consistency/metamorphic_properties_spec.rb +195 -0
- data/spec/consistency/metamorphic_spec.rb +138 -0
- data/spec/consistency/model_coverage_audit_spec.rb +144 -61
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +34 -5
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +35 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -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/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/entity_spec.rb +2 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/plan_conformance_spec.rb +2 -0
- data/spec/moxml/processing_instruction_spec.rb +9 -0
- data/spec/moxml/reference_opal_spec.rb +29 -0
- data/spec/moxml/reference_spec.rb +162 -0
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +47 -74
- data/spec/moxml/text_spec.rb +2 -0
- data/spec/moxml/to_html_spec.rb +2 -0
- data/spec/moxml/xhtml_output_spec.rb +2 -0
- data/spec/moxml/xpath/axes_spec.rb +2 -2
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/moxml/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +219 -0
- data/spec/support/model_coverage.rb +159 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +32 -2
|
@@ -19,72 +19,121 @@ 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.
|
|
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`.
|
|
82
74
|
|
|
83
|
-
| D-
|
|
75
|
+
| D-13
|
|
84
76
|
| ox, headed_ox
|
|
85
|
-
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
77
|
+
| Inter-element whitespace-only text nodes are dropped at parse
|
|
78
|
+
(node counts and leading/trailing text differ from the other
|
|
79
|
+
adapters). Surfaced by the taxonomy differential. Pinned in
|
|
80
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
81
|
+
|
|
82
|
+
| D-14
|
|
83
|
+
| oga
|
|
84
|
+
| Processing-instruction content leaks into text reads (oga's own
|
|
85
|
+
`text` folds PIs; the model says only text nodes contribute).
|
|
86
|
+
Surfaced by the taxonomy differential. Pinned in
|
|
87
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
88
|
+
|
|
89
|
+
| D-15
|
|
90
|
+
| rexml
|
|
91
|
+
| The whitespace text node between a pre-root PI and the root is
|
|
92
|
+
retained (other adapters drop it). Surfaced by the taxonomy
|
|
93
|
+
differential. Pinned in `spec/consistency/differential_taxonomy_spec.rb`.
|
|
94
|
+
|
|
95
|
+
| D-16
|
|
96
|
+
| rexml
|
|
97
|
+
| Entity-expansion guard caps huge documents (~10k expansions) and
|
|
98
|
+
raised a bare engine error — the facade now names it a ParseError;
|
|
99
|
+
the cap itself remains (nokogiri's HUGE lane has no such cap).
|
|
100
|
+
Surfaced by the taxonomy differential. Pinned in
|
|
101
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
102
|
+
|
|
103
|
+
| D-17
|
|
104
|
+
| oga
|
|
105
|
+
| The strict parse lane does not validate nesting well-formedness
|
|
106
|
+
(oga's strict is lexer-strict, not well-formedness-strict); bad
|
|
107
|
+
nesting parses where other adapters raise. Surfaced by the taxonomy
|
|
108
|
+
differential. Pinned in
|
|
109
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
110
|
+
|
|
111
|
+
| D-18
|
|
112
|
+
| leptris, oga, ox, headed_ox
|
|
113
|
+
| Strict parse accepts invalid UTF-8 byte sequences (no encoding
|
|
114
|
+
validation at parse; the bytes flow through as binary). The
|
|
115
|
+
validating adapters (nokogiri, rexml, libxml) raise the named
|
|
116
|
+
ParseError. On leptris the facade raises the named ParseError in the
|
|
117
|
+
strict lane (default-UTF-8 rule; prolog-declared non-UTF-8 encodings
|
|
118
|
+
own their validity) pending engine-side validation —
|
|
119
|
+
leptris-ruby#514. Surfaced by the taxonomy differential. Pinned in
|
|
120
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
121
|
+
|
|
122
|
+
| D-19
|
|
123
|
+
| nokogiri, libxml
|
|
124
|
+
| Adjacent text-like leaves (CDATA next to CDATA or text) coalesce
|
|
125
|
+
into a single text node on the libxml2 lane; oga, rexml, ox,
|
|
126
|
+
headed_ox, and leptris keep them as distinct nodes. The `text`
|
|
127
|
+
answer is identical — the node count and kinds differ. The model is
|
|
128
|
+
silent on text-node coalescing (an open decision). Surfaced by the
|
|
129
|
+
generated metamorphic differential (M5). Pinned in
|
|
130
|
+
`spec/consistency/metamorphic_properties_spec.rb` through
|
|
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`).
|
|
90
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
|
|
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,15 +17,30 @@ 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
|
-
2. `spec/consistency/` — the model pinned as executable expectations
|
|
23
|
+
2. `spec/consistency/` — the model pinned as executable expectations,
|
|
24
|
+
with `lib/moxml/reference/` as the **engine-free executable
|
|
25
|
+
reference** (a pure-Ruby reader, writer, and normalizer). Adapters
|
|
26
|
+
are differentially tested against the reference
|
|
27
|
+
(`spec/consistency/reference_differential_spec.rb`), not only
|
|
28
|
+
against each other; the reference is pure Ruby and transpiles to
|
|
29
|
+
the Opal lane.
|
|
22
30
|
3. Adapter implementations and capability gates — representations.
|
|
23
31
|
|
|
32
|
+
A pin cites the model section it enforces
|
|
33
|
+
(`# Processing model §<section> (normative)`); the coverage audit
|
|
34
|
+
asserts both directions of that linkage, so removing any of the
|
|
35
|
+
document, a pin, or the reference fails CI.
|
|
36
|
+
|
|
24
37
|
A semantics change amends this document first (see
|
|
25
38
|
`TODO.model/05-model-evolution-protocol.md`).
|
|
26
39
|
|
|
27
40
|
=== Mission and invariant
|
|
28
41
|
|
|
42
|
+
Pins:: none
|
|
43
|
+
|
|
29
44
|
Moxml exists to make "which XML engine am I running?" a **deployment
|
|
30
45
|
decision instead of an API decision**. The invariant that enables it:
|
|
31
46
|
any program written against the facade produces the same observable
|
|
@@ -39,6 +54,8 @@ divergences, and performance.
|
|
|
39
54
|
|
|
40
55
|
=== Object model
|
|
41
56
|
|
|
57
|
+
Pins:: none
|
|
58
|
+
|
|
42
59
|
Node kinds:: `Document`, `Element`, `Attribute`, `Text`, `Comment`,
|
|
43
60
|
`Cdata`, `Declaration`, `Doctype`, `ProcessingInstruction`,
|
|
44
61
|
`Namespace`, `EntityReference`.
|
|
@@ -48,6 +65,12 @@ the same underlying node; node identity is a property of the
|
|
|
48
65
|
underlying tree, not of wrapper object identity. Comparisons use node
|
|
49
66
|
equality semantics, never `equal?` on wrappers.
|
|
50
67
|
|
|
68
|
+
Kind:: Every node answers `node_type` — one of `:document`,
|
|
69
|
+
`:element`, `:attribute`, `:text`, `:comment`, `:cdata`,
|
|
70
|
+
`:processing_instruction`, `:doctype`, `:declaration`,
|
|
71
|
+
`:entity_reference`, `:namespace` — the predicates (`element?`,
|
|
72
|
+
`text?`, …) are its derivatives.
|
|
73
|
+
|
|
51
74
|
Ownership:: The `Document` owns the tree's lifetime; nodes are valid
|
|
52
75
|
within it. Deterministic release is available via `Document#free`.
|
|
53
76
|
|
|
@@ -57,15 +80,23 @@ listings or serialization child walks.
|
|
|
57
80
|
|
|
58
81
|
=== Document order and traversal
|
|
59
82
|
|
|
83
|
+
Pins:: `spec/consistency/adapter_parity_spec.rb`
|
|
84
|
+
|
|
60
85
|
* `children` lists nodes in document order.
|
|
61
86
|
* Walks (`each_node`, `materialize`) visit depth-first, pre-order.
|
|
62
87
|
* `Document#children` and `#root` never include the declaration;
|
|
63
88
|
doctype exposure follows the Nokogiri contract.
|
|
89
|
+
* Document-level parts (`Declaration`, `Doctype`) are state, not
|
|
90
|
+
nodes: walks (`traverse`, `each_node`, `materialize`) never yield
|
|
91
|
+
them, and walks over a DOCTYPE-bearing document are well-defined on
|
|
92
|
+
every adapter.
|
|
64
93
|
* `NodeSet` preserves document order for axis results; unions preserve
|
|
65
94
|
first-encounter document order.
|
|
66
95
|
|
|
67
96
|
=== Names and namespaces
|
|
68
97
|
|
|
98
|
+
Pins:: `spec/consistency/xpath_idioms_spec.rb`
|
|
99
|
+
|
|
69
100
|
* Names are **expanded names**: a name test matches by namespace *URI*,
|
|
70
101
|
never by prefix spelling.
|
|
71
102
|
* Unprefixed element name tests match **no-namespace** elements only
|
|
@@ -81,12 +112,16 @@ listings or serialization child walks.
|
|
|
81
112
|
|
|
82
113
|
=== Access semantics
|
|
83
114
|
|
|
115
|
+
Pins:: `spec/moxml/attribute_resolver_spec.rb`, `spec/moxml/text_spec.rb`
|
|
116
|
+
|
|
84
117
|
`text`:: Concatenated descendant text (elements); a text node's own
|
|
85
118
|
content; entity references restored when the document is
|
|
86
119
|
entity-bearing.
|
|
87
120
|
|
|
88
121
|
`content`:: The node's content per its kind; on text-like nodes, with
|
|
89
|
-
entity-marker restoration; memoized against mutation.
|
|
122
|
+
entity-marker restoration; memoized against mutation. A processing
|
|
123
|
+
instruction's content is its data **without** the separating
|
|
124
|
+
whitespace after the target (uniform across adapters).
|
|
90
125
|
|
|
91
126
|
`raw_content`:: The backing text **without** entity restoration.
|
|
92
127
|
|
|
@@ -103,6 +138,8 @@ with an unambiguous local name.
|
|
|
103
138
|
|
|
104
139
|
=== Parsing
|
|
105
140
|
|
|
141
|
+
Pins:: `spec/consistency/eol_normalization_spec.rb`, `spec/moxml/document_parse_diagnostics_spec.rb`, `spec/moxml/entity_spec.rb`
|
|
142
|
+
|
|
106
143
|
* Modes: strict (errors raise) and recover (errors surface via
|
|
107
144
|
`parse_diagnostics` / `parse_errors`, both `[{kind:, message:}]`).
|
|
108
145
|
* HUGE mode: over-cap text (> 10 MB) parses completely.
|
|
@@ -116,6 +153,8 @@ with an unambiguous local name.
|
|
|
116
153
|
|
|
117
154
|
=== XPath evaluation
|
|
118
155
|
|
|
156
|
+
Pins:: `spec/consistency/xpath_idioms_spec.rb`
|
|
157
|
+
|
|
119
158
|
* XPath 1.0 semantics on every adapter. Engines without an evaluator
|
|
120
159
|
(Ox, HeadedOx; leptris for shapes its engine cannot evaluate) run
|
|
121
160
|
**moxml's own pure-Ruby engine** — the model's semantics do not
|
|
@@ -128,16 +167,25 @@ with an unambiguous local name.
|
|
|
128
167
|
`xmlns:` default-namespace matching; attribute terminals in every
|
|
129
168
|
position (`//a/@id`, `//@id`, `r/a/@id`); symbol- and string-keyed
|
|
130
169
|
bindings resolve identically.
|
|
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.
|
|
131
176
|
|
|
132
177
|
=== Serialization
|
|
133
178
|
|
|
179
|
+
Pins:: `spec/moxml/to_html_spec.rb`, `spec/moxml/xhtml_output_spec.rb`
|
|
180
|
+
|
|
134
181
|
[cols="1,3"]
|
|
135
182
|
|===
|
|
136
183
|
| Shape | Contract
|
|
137
184
|
|
|
138
185
|
| `to_xml`
|
|
139
186
|
| Declaration per configuration; empty elements per `expand_empty`;
|
|
140
|
-
namespace declarations preserved
|
|
187
|
+
namespace declarations preserved; a parsed DOCTYPE is emitted with
|
|
188
|
+
its external identifiers and internal subset.
|
|
141
189
|
|
|
142
190
|
| `to_html`
|
|
143
191
|
| HTML method: void elements unclosed (`<br>`), raw text
|
|
@@ -161,15 +209,25 @@ with an unambiguous local name.
|
|
|
161
209
|
|
|
162
210
|
=== Canonicalization
|
|
163
211
|
|
|
212
|
+
Pins:: `spec/moxml/c14n/adapter_delegation_spec.rb`
|
|
213
|
+
|
|
164
214
|
* Algorithms: Inclusive 1.0, Exclusive 1.0, Inclusive 1.1, plus
|
|
165
215
|
XPath-driven subset canonicalization (enveloped-signature oracle).
|
|
166
216
|
* Native delegations (leptris engine, libxml2) answer **byte-identical**
|
|
167
217
|
output to the Ruby reference path (byte-safety probed before
|
|
168
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.
|
|
169
225
|
* `Moxml::C14n.equivalent?` is the algorithm-aware comparison.
|
|
170
226
|
|
|
171
227
|
=== XSLT
|
|
172
228
|
|
|
229
|
+
Pins:: `spec/consistency/xslt_spec.rb`
|
|
230
|
+
|
|
173
231
|
XSLT 1.0 through engines that ship a processor (libxslt; leptris
|
|
174
232
|
1.0–3.0). Compilation and transform failures raise named
|
|
175
233
|
`Moxml::XsltError`s. Engines without a processor raise
|
|
@@ -177,6 +235,8 @@ XSLT 1.0 through engines that ship a processor (libxslt; leptris
|
|
|
177
235
|
|
|
178
236
|
=== SAX
|
|
179
237
|
|
|
238
|
+
Pins:: `spec/moxml/sax_spec.rb`
|
|
239
|
+
|
|
180
240
|
* The callback stream (`on_start_element`, `on_characters`, `on_error`,
|
|
181
241
|
…) is the authoritative event sequence.
|
|
182
242
|
* The record-drain fast path (`on_sax_records`) replays **exactly** the
|
|
@@ -186,6 +246,8 @@ XSLT 1.0 through engines that ship a processor (libxslt; leptris
|
|
|
186
246
|
|
|
187
247
|
=== Materialization
|
|
188
248
|
|
|
249
|
+
Pins:: `spec/moxml/plan_conformance_spec.rb`
|
|
250
|
+
|
|
189
251
|
`materialize`, `materialize_fields`, `Plan`, and `StructPlan` produce
|
|
190
252
|
**identical records across all adapters** (conformance spec). Plan name
|
|
191
253
|
matching is global; unmatched subtrees are dropped per the pinned
|
|
@@ -193,6 +255,8 @@ semantics.
|
|
|
193
255
|
|
|
194
256
|
=== Errors and loud degradation
|
|
195
257
|
|
|
258
|
+
Pins:: `spec/consistency/loud_degradation_spec.rb`
|
|
259
|
+
|
|
196
260
|
* `Moxml::Error` hierarchy; `Moxml::XPathError`, `Moxml::XsltError`,
|
|
197
261
|
`Moxml::AdapterError` (e.g. schema-handle/adapter misroute),
|
|
198
262
|
`Moxml::NotImplementedError` for unsupported capability contracts.
|
|
@@ -204,6 +268,8 @@ semantics.
|
|
|
204
268
|
|
|
205
269
|
=== Performance: a declared, non-semantic channel
|
|
206
270
|
|
|
271
|
+
Pins:: none
|
|
272
|
+
|
|
207
273
|
Acceleration (native read layers, C minting, record drains, plan
|
|
208
274
|
streams) always lands behind version + face gates and degrades to the
|
|
209
275
|
spec-pinned fallback. Cross-adapter performance variance is sanctioned
|
|
@@ -62,3 +62,7 @@ require "moxml/xpath/errors"
|
|
|
62
62
|
require "moxml/xpath/ast/node"
|
|
63
63
|
require "moxml/xpath/ruby/node"
|
|
64
64
|
require "moxml/xpath/ruby/generator"
|
|
65
|
+
require "moxml/reference"
|
|
66
|
+
require "moxml/reference/parser"
|
|
67
|
+
require "moxml/reference/writer"
|
|
68
|
+
require "moxml/reference/normalizer"
|