moxml 0.5.145 → 0.5.147
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/rake.yml +35 -0
- data/README.adoc +19 -1
- data/docs/_pages/adapter-compatibility.adoc +7 -0
- data/docs/_pages/divergences.adoc +90 -0
- data/docs/_references/index.adoc +9 -0
- data/docs/_references/processing-model.adoc +211 -0
- data/docs/index.adoc +9 -0
- data/lib/moxml/adapter/leptris.rb +129 -2
- data/lib/moxml/adapter/libxml.rb +5 -0
- data/lib/moxml/adapter/oga.rb +10 -0
- data/lib/moxml/adapter/rexml.rb +5 -0
- data/lib/moxml/document.rb +9 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +14 -1
- data/spec/consistency/divergence_register_spec.rb +109 -0
- data/spec/consistency/document_text_spec.rb +30 -0
- data/spec/consistency/eol_normalization_spec.rb +8 -8
- data/spec/consistency/loud_degradation_spec.rb +44 -0
- data/spec/consistency/model_coverage_audit_spec.rb +113 -0
- data/spec/consistency/spec_constant_uniqueness_spec.rb +29 -0
- data/spec/consistency/xpath_idioms_spec.rb +32 -1
- data/spec/moxml/adapter/leptris_contract_reads_spec.rb +133 -0
- data/spec/moxml/adapter/leptris_detached_text_spec.rb +34 -0
- data/spec/moxml/adapter/leptris_parse_lint_spec.rb +35 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +3 -3
- data/spec/moxml/c14n/adapter_delegation_spec.rb +3 -3
- data/spec/moxml/c14n/comments_pis_spec.rb +2 -2
- data/spec/moxml/c14n/subset_spec.rb +4 -4
- data/spec/moxml/c14n/w3c_examples_spec.rb +3 -1
- data/spec/moxml/declaration_preservation_spec.rb +2 -2
- data/spec/moxml/element_add_element_spec.rb +3 -3
- data/spec/moxml/element_attribute_pairs_spec.rb +2 -2
- data/spec/moxml/node_spec.rb +1 -1
- data/spec/moxml/plan_conformance_spec.rb +3 -3
- data/spec/moxml/sax_entity_parity_spec.rb +3 -3
- data/spec/moxml/sax_spec.rb +7 -7
- data/spec/moxml/struct_plan_spec.rb +2 -2
- data/spec/moxml/xpath_capabilities_spec.rb +13 -13
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e5797a825682b992d84b5ab9155d74f81fdeca24293b40ad7b9e58860c407f0
|
|
4
|
+
data.tar.gz: 11accb2da9da7cf656d28ab7e128da698f1702120332d94d06b4a20ea39ad112
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b8980eb872815020ed2b6d582172ffd62c08a426c2fa2104bd891cd5e2f1e69392fd82d9573215383399087cb686852c47fb10e1d93e812c1641f8eb5e4eeef
|
|
7
|
+
data.tar.gz: 924b76844411af051e0a9b02ea55788dbc4f5d0e6ec0be31ade4ded2c17c6c6e58356f687ef42e2d7934a4a8f684720f77b0d7688e108437aa64c68a41b68d06
|
data/.github/workflows/rake.yml
CHANGED
|
@@ -71,3 +71,38 @@ jobs:
|
|
|
71
71
|
env:
|
|
72
72
|
LEPTRIS_BENCH_LOCK: "1"
|
|
73
73
|
run: bundle exec rake benchmark:plan
|
|
74
|
+
|
|
75
|
+
# Order-flake tripwire (TODO.model/06 checker integrity): the same
|
|
76
|
+
# example order every run - a red here reproduces locally with
|
|
77
|
+
# `rspec --bisect --order rand:424242`. The per-PR matrix legs keep
|
|
78
|
+
# their random seeds for breadth; this one is the deterministic twin.
|
|
79
|
+
battery-fixed-seed:
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/checkout@v6
|
|
83
|
+
with:
|
|
84
|
+
submodules: "recursive"
|
|
85
|
+
|
|
86
|
+
- name: Set up Ruby
|
|
87
|
+
uses: ruby/setup-ruby@v1
|
|
88
|
+
with:
|
|
89
|
+
ruby-version: "3.4"
|
|
90
|
+
bundler-cache: false
|
|
91
|
+
|
|
92
|
+
- name: Install ragel
|
|
93
|
+
run: sudo apt-get update && sudo apt-get install -y ragel
|
|
94
|
+
|
|
95
|
+
- name: Install ruby-ll (for rake vendor:prepare)
|
|
96
|
+
run: gem install ruby-ll --no-document
|
|
97
|
+
|
|
98
|
+
- name: Generate ragel / ruby-ll outputs in vendored forks
|
|
99
|
+
run: rake vendor:prepare
|
|
100
|
+
|
|
101
|
+
- name: Install dependencies
|
|
102
|
+
run: bundle install
|
|
103
|
+
|
|
104
|
+
- name: Compile liboga / libll native extensions
|
|
105
|
+
run: rake vendor:compile
|
|
106
|
+
|
|
107
|
+
- name: Full battery at the fixed seed
|
|
108
|
+
run: bundle exec rspec --order rand:424242
|
data/README.adoc
CHANGED
|
@@ -15,10 +15,28 @@ It offers a consistent API that abstracts away the underlying XML implementation
|
|
|
15
15
|
details while maintaining high performance through efficient node mapping and
|
|
16
16
|
native XPath querying.
|
|
17
17
|
|
|
18
|
+
Moxml's mission: **make "which XML engine am I running?" a deployment decision
|
|
19
|
+
instead of an API decision.** Libraries write once against a single XML
|
|
20
|
+
processing model — one object model, one name/namespace resolution model, one
|
|
21
|
+
XPath, one serialization and canonicalization contract — and the engine
|
|
22
|
+
underneath (Nokogiri, Leptris, REXML, Oga, Ox, LibXML) is a configuration
|
|
23
|
+
choice.
|
|
24
|
+
|
|
25
|
+
The invariant that makes this work: any program written against the facade
|
|
26
|
+
produces the same observable outcome on every adapter. Performance may vary
|
|
27
|
+
across engines — semantics may not. Where an engine cannot deliver a
|
|
28
|
+
capability, it fails loudly (a named error); where an engine genuinely
|
|
29
|
+
diverges, the divergence is registered and pinned — never silent. The model is
|
|
30
|
+
specified normatively in the
|
|
31
|
+
https://lutaml.github.io/moxml/_references/processing-model[Processing Model]
|
|
32
|
+
document, pinned as executable expectations in the consistency suite, and its
|
|
33
|
+
known engine differences are tracked in the
|
|
34
|
+
https://lutaml.github.io/moxml/_pages/divergences[divergence register].
|
|
35
|
+
|
|
18
36
|
Key features:
|
|
19
37
|
|
|
20
38
|
* Intuitive, Ruby-idiomatic API for XML manipulation
|
|
21
|
-
*
|
|
39
|
+
* One processing model, consistently implemented across different XML libraries
|
|
22
40
|
* Efficient node mapping for XPath queries
|
|
23
41
|
* Support for all XML node types and features
|
|
24
42
|
* Easy switching between XML processing engines
|
|
@@ -8,6 +8,13 @@ toc::[]
|
|
|
8
8
|
|
|
9
9
|
This document provides detailed compatibility information for all supported XML adapters in Moxml, explaining which features are fully supported, partially supported, or not supported by each adapter.
|
|
10
10
|
|
|
11
|
+
The authoritative sources for cross-adapter semantics are the
|
|
12
|
+
link:../_references/processing-model[Processing Model] (the normative
|
|
13
|
+
single model every adapter implements) and the
|
|
14
|
+
link:divergences[divergence register] (every known, pinned engine
|
|
15
|
+
difference, D-01..D-12). A capability an adapter cannot deliver raises
|
|
16
|
+
a named error — it never answers a divergent result silently.
|
|
17
|
+
|
|
11
18
|
== Test suite results
|
|
12
19
|
|
|
13
20
|
**Overall:** 2,182 examples, 0 failures, 97 pending
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Divergences
|
|
3
|
+
nav_order: 4
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
== Divergence register
|
|
7
|
+
|
|
8
|
+
Every known cross-adapter divergence from the
|
|
9
|
+
link:../_references/processing-model[processing model] is registered
|
|
10
|
+
here. Contract:
|
|
11
|
+
|
|
12
|
+
* A divergence is either **fixed**, **loud** (a named raise), or
|
|
13
|
+
**SPEC'D** (documented here and pinned with a skip in the
|
|
14
|
+
consistency suite, citing the entry ID).
|
|
15
|
+
* Consistency-suite exemptions must cite a register ID. An
|
|
16
|
+
undocumented divergence is a bug — report it against this register.
|
|
17
|
+
|
|
18
|
+
[cols="2,4,6"]
|
|
19
|
+
|===
|
|
20
|
+
| ID | Adapter(s) | Divergence
|
|
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
|
+
| D-02
|
|
29
|
+
| oga
|
|
30
|
+
| Raw CR preserved in text content — §2.11 EOL normalization not
|
|
31
|
+
applied at parse. Pinned in `spec/consistency/eol_normalization_spec.rb`.
|
|
32
|
+
|
|
33
|
+
| D-03
|
|
34
|
+
| ox, headed_ox
|
|
35
|
+
| ` ` character references collapse to LF. Pinned in
|
|
36
|
+
`spec/consistency/eol_normalization_spec.rb`.
|
|
37
|
+
|
|
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
|
+
| D-05
|
|
44
|
+
| ox, headed_ox
|
|
45
|
+
| PI content dropped at parse. Pinned in
|
|
46
|
+
`spec/moxml/c14n/adapter_delegation_spec.rb`.
|
|
47
|
+
|
|
48
|
+
| D-06
|
|
49
|
+
| ox, headed_ox
|
|
50
|
+
| Comment content padding stripped at parse. Pinned in
|
|
51
|
+
`spec/moxml/c14n/subset_spec.rb`.
|
|
52
|
+
|
|
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
|
+
| D-09
|
|
65
|
+
| 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
|
|
70
|
+
`spec/moxml/adapter/shared_examples/adapter_contract.rb`.
|
|
71
|
+
|
|
72
|
+
| D-10
|
|
73
|
+
| ox, headed_ox
|
|
74
|
+
| SAX emits no separate CDATA/comment/processing-instruction events
|
|
75
|
+
(sequences fold into text or are dropped). Pinned in
|
|
76
|
+
`spec/moxml/sax_spec.rb`.
|
|
77
|
+
|
|
78
|
+
| D-11
|
|
79
|
+
| oga
|
|
80
|
+
| 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`.
|
|
90
|
+
|===
|
data/docs/_references/index.adoc
CHANGED
|
@@ -7,6 +7,15 @@ nav_order: 1
|
|
|
7
7
|
|
|
8
8
|
Detailed technical specifications and API documentation for Moxml.
|
|
9
9
|
|
|
10
|
+
=== Processing model
|
|
11
|
+
|
|
12
|
+
link:processing-model[Processing Model]::
|
|
13
|
+
The normative statement of Moxml's single XML processing model — the
|
|
14
|
+
semantics every adapter implements.
|
|
15
|
+
|
|
16
|
+
link:../_pages/divergences[Divergences]::
|
|
17
|
+
The register of known, pinned cross-adapter divergences.
|
|
18
|
+
|
|
10
19
|
=== API reference
|
|
11
20
|
|
|
12
21
|
link:document-api[Document API]::
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Processing Model
|
|
3
|
+
nav_order: 3
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
== The processing model (normative)
|
|
7
|
+
|
|
8
|
+
This document is the **single normative statement of Moxml's XML
|
|
9
|
+
processing model**. Every adapter implements this model; no adapter's
|
|
10
|
+
native semantics leak through it. When implementations disagree, the
|
|
11
|
+
model wins — engines converge to it, never the reverse.
|
|
12
|
+
|
|
13
|
+
The model's executable form is the consistency suite
|
|
14
|
+
(`spec/consistency/`). Known, unfixable engine differences are
|
|
15
|
+
registered in link:../_pages/divergences[the divergence register] and
|
|
16
|
+
skip with their entry ID — an undocumented divergence is a bug.
|
|
17
|
+
|
|
18
|
+
=== Status and precedence
|
|
19
|
+
|
|
20
|
+
1. This document — the model.
|
|
21
|
+
2. `spec/consistency/` — the model pinned as executable expectations.
|
|
22
|
+
3. Adapter implementations and capability gates — representations.
|
|
23
|
+
|
|
24
|
+
A semantics change amends this document first (see
|
|
25
|
+
`TODO.model/05-model-evolution-protocol.md`).
|
|
26
|
+
|
|
27
|
+
=== Mission and invariant
|
|
28
|
+
|
|
29
|
+
Moxml exists to make "which XML engine am I running?" a **deployment
|
|
30
|
+
decision instead of an API decision**. The invariant that enables it:
|
|
31
|
+
any program written against the facade produces the same observable
|
|
32
|
+
outcome on every adapter. The engine is unobservable except through
|
|
33
|
+
declared channels: capability gates (loud raises), registered
|
|
34
|
+
divergences, and performance.
|
|
35
|
+
|
|
36
|
+
* Performance may vary across engines — semantics may not.
|
|
37
|
+
* Absence of capability is legal (a named raise). Absence of
|
|
38
|
+
equivalence is not (a different answer).
|
|
39
|
+
|
|
40
|
+
=== Object model
|
|
41
|
+
|
|
42
|
+
Node kinds:: `Document`, `Element`, `Attribute`, `Text`, `Comment`,
|
|
43
|
+
`Cdata`, `Declaration`, `Doctype`, `ProcessingInstruction`,
|
|
44
|
+
`Namespace`, `EntityReference`.
|
|
45
|
+
|
|
46
|
+
Identity:: Parsing and traversal may mint distinct wrapper objects for
|
|
47
|
+
the same underlying node; node identity is a property of the
|
|
48
|
+
underlying tree, not of wrapper object identity. Comparisons use node
|
|
49
|
+
equality semantics, never `equal?` on wrappers.
|
|
50
|
+
|
|
51
|
+
Ownership:: The `Document` owns the tree's lifetime; nodes are valid
|
|
52
|
+
within it. Deterministic release is available via `Document#free`.
|
|
53
|
+
|
|
54
|
+
Declarations are state:: `Declaration` (and the doctype token) are
|
|
55
|
+
document *state*, not child nodes: they never appear in `children`
|
|
56
|
+
listings or serialization child walks.
|
|
57
|
+
|
|
58
|
+
=== Document order and traversal
|
|
59
|
+
|
|
60
|
+
* `children` lists nodes in document order.
|
|
61
|
+
* Walks (`each_node`, `materialize`) visit depth-first, pre-order.
|
|
62
|
+
* `Document#children` and `#root` never include the declaration;
|
|
63
|
+
doctype exposure follows the Nokogiri contract.
|
|
64
|
+
* `NodeSet` preserves document order for axis results; unions preserve
|
|
65
|
+
first-encounter document order.
|
|
66
|
+
|
|
67
|
+
=== Names and namespaces
|
|
68
|
+
|
|
69
|
+
* Names are **expanded names**: a name test matches by namespace *URI*,
|
|
70
|
+
never by prefix spelling.
|
|
71
|
+
* Unprefixed element name tests match **no-namespace** elements only
|
|
72
|
+
(XPath 1.0). Unprefixed attribute name tests likewise match
|
|
73
|
+
no-namespace attributes.
|
|
74
|
+
* Reserved prefixes: `xml` is bound to the XML namespace always;
|
|
75
|
+
`xmlns:` in an expression addresses the **default namespace** (the
|
|
76
|
+
libxml2 idiom, adopted model-wide).
|
|
77
|
+
* Bindings hashes accept both string and symbol keys; explicit
|
|
78
|
+
bindings **win** over in-scope document declarations.
|
|
79
|
+
* `Element#name` returns the bare tag name; non-element DOM names are
|
|
80
|
+
`"text"` / `"comment"`; a PI's name is its target.
|
|
81
|
+
|
|
82
|
+
=== Access semantics
|
|
83
|
+
|
|
84
|
+
`text`:: Concatenated descendant text (elements); a text node's own
|
|
85
|
+
content; entity references restored when the document is
|
|
86
|
+
entity-bearing.
|
|
87
|
+
|
|
88
|
+
`content`:: The node's content per its kind; on text-like nodes, with
|
|
89
|
+
entity-marker restoration; memoized against mutation.
|
|
90
|
+
|
|
91
|
+
`raw_content`:: The backing text **without** entity restoration.
|
|
92
|
+
|
|
93
|
+
`[]` / `attributes` / `attribute_pairs`:: `[]` reads an attribute by
|
|
94
|
+
bare name (no-namespace) or prefixed name (resolved by URI);
|
|
95
|
+
values are entity-restored when the document bears markers.
|
|
96
|
+
`attribute_pairs` is the read-only listing ([name, value] pairs,
|
|
97
|
+
document order, duplicates included). `remove_attribute` returns the
|
|
98
|
+
removed attribute or nil — including for namespace-prefixed targets
|
|
99
|
+
with an unambiguous local name.
|
|
100
|
+
|
|
101
|
+
`remove`/mutation returns:: Mutators return the contract object
|
|
102
|
+
(removed node, new node) — never a silent no-op self.
|
|
103
|
+
|
|
104
|
+
=== Parsing
|
|
105
|
+
|
|
106
|
+
* Modes: strict (errors raise) and recover (errors surface via
|
|
107
|
+
`parse_diagnostics` / `parse_errors`, both `[{kind:, message:}]`).
|
|
108
|
+
* HUGE mode: over-cap text (> 10 MB) parses completely.
|
|
109
|
+
* EOL normalization (XML 1.0 §2.11): CRLF → LF; bare CR → LF in text,
|
|
110
|
+
CDATA, and parsed content; attribute-value CRLF collapses to one
|
|
111
|
+
space; `
` survives as a literal CR.
|
|
112
|
+
* Entity modes: `:expand` (default — built-ins resolved at parse),
|
|
113
|
+
`:keep` (reference nodes preserved with marker machinery),
|
|
114
|
+
`:disabled`, `:custom` (provider). Restoration of marker-bearing
|
|
115
|
+
values is marker-driven and generation-stamped.
|
|
116
|
+
|
|
117
|
+
=== XPath evaluation
|
|
118
|
+
|
|
119
|
+
* XPath 1.0 semantics on every adapter. Engines without an evaluator
|
|
120
|
+
(Ox, HeadedOx; leptris for shapes its engine cannot evaluate) run
|
|
121
|
+
**moxml's own pure-Ruby engine** — the model's semantics do not
|
|
122
|
+
depend on the engine shipping one.
|
|
123
|
+
* Document-context **relative name-test paths** resolve laxly —
|
|
124
|
+
descendant-anchored at the root (`doc.at("a/b")` finds `//a/b`),
|
|
125
|
+
matching Nokogiri's `Document#at`/`#search`. Axes, node types, `.`
|
|
126
|
+
and `..` keep strict document context.
|
|
127
|
+
* Declared idioms (pinned in `spec/consistency/xpath_idioms_spec.rb`):
|
|
128
|
+
`xmlns:` default-namespace matching; attribute terminals in every
|
|
129
|
+
position (`//a/@id`, `//@id`, `r/a/@id`); symbol- and string-keyed
|
|
130
|
+
bindings resolve identically.
|
|
131
|
+
|
|
132
|
+
=== Serialization
|
|
133
|
+
|
|
134
|
+
[cols="1,3"]
|
|
135
|
+
|===
|
|
136
|
+
| Shape | Contract
|
|
137
|
+
|
|
138
|
+
| `to_xml`
|
|
139
|
+
| Declaration per configuration; empty elements per `expand_empty`;
|
|
140
|
+
namespace declarations preserved.
|
|
141
|
+
|
|
142
|
+
| `to_html`
|
|
143
|
+
| HTML method: void elements unclosed (`<br>`), raw text
|
|
144
|
+
(script/style) verbatim, no XML declaration, DOCTYPE first.
|
|
145
|
+
|
|
146
|
+
| `to_xhtml`
|
|
147
|
+
| XML method on HTML trees: void elements self-closed (`<br/>`),
|
|
148
|
+
non-void elements paired, raw text XML-escaped, no declaration
|
|
149
|
+
unless asked. Also selected by `to_html(xhtml: true)`.
|
|
150
|
+
|
|
151
|
+
| Indentation
|
|
152
|
+
| Document-level `to_html`/`to_xhtml` indentation composes with the
|
|
153
|
+
`indent:`/`indent_text:` knobs and raises `Moxml::AdapterError` on
|
|
154
|
+
incapable engines. Element-level `to_xml(indent:)` is honored on
|
|
155
|
+
nokogiri/rexml; other engines currently ignore it — registered as
|
|
156
|
+
link:../_pages/divergences[D-09], not silently blessed.
|
|
157
|
+
|
|
158
|
+
| Line endings
|
|
159
|
+
| `line_ending:` normalizes output newlines on every shape.
|
|
160
|
+
|===
|
|
161
|
+
|
|
162
|
+
=== Canonicalization
|
|
163
|
+
|
|
164
|
+
* Algorithms: Inclusive 1.0, Exclusive 1.0, Inclusive 1.1, plus
|
|
165
|
+
XPath-driven subset canonicalization (enveloped-signature oracle).
|
|
166
|
+
* Native delegations (leptris engine, libxml2) answer **byte-identical**
|
|
167
|
+
output to the Ruby reference path (byte-safety probed before
|
|
168
|
+
engagement).
|
|
169
|
+
* `Moxml::C14n.equivalent?` is the algorithm-aware comparison.
|
|
170
|
+
|
|
171
|
+
=== XSLT
|
|
172
|
+
|
|
173
|
+
XSLT 1.0 through engines that ship a processor (libxslt; leptris
|
|
174
|
+
1.0–3.0). Compilation and transform failures raise named
|
|
175
|
+
`Moxml::XsltError`s. Engines without a processor raise
|
|
176
|
+
`Moxml::NotImplementedError` — the face is never silently absent.
|
|
177
|
+
|
|
178
|
+
=== SAX
|
|
179
|
+
|
|
180
|
+
* The callback stream (`on_start_element`, `on_characters`, `on_error`,
|
|
181
|
+
…) is the authoritative event sequence.
|
|
182
|
+
* The record-drain fast path (`on_sax_records`) replays **exactly** the
|
|
183
|
+
callback stream (spec-pinned equality). Documents whose parse would
|
|
184
|
+
emit an ERROR event are outside the drainable subset — the drain
|
|
185
|
+
refuses them so `on_error` retains authority.
|
|
186
|
+
|
|
187
|
+
=== Materialization
|
|
188
|
+
|
|
189
|
+
`materialize`, `materialize_fields`, `Plan`, and `StructPlan` produce
|
|
190
|
+
**identical records across all adapters** (conformance spec). Plan name
|
|
191
|
+
matching is global; unmatched subtrees are dropped per the pinned
|
|
192
|
+
semantics.
|
|
193
|
+
|
|
194
|
+
=== Errors and loud degradation
|
|
195
|
+
|
|
196
|
+
* `Moxml::Error` hierarchy; `Moxml::XPathError`, `Moxml::XsltError`,
|
|
197
|
+
`Moxml::AdapterError` (e.g. schema-handle/adapter misroute),
|
|
198
|
+
`Moxml::NotImplementedError` for unsupported capability contracts.
|
|
199
|
+
* Every gated surface answers the contract or raises the named error —
|
|
200
|
+
never returns a divergent answer.
|
|
201
|
+
* Adapter degradation is loud: a present-but-unusable preferred adapter
|
|
202
|
+
warns at configuration and misroutes fail at the wrap, not deep in
|
|
203
|
+
a call.
|
|
204
|
+
|
|
205
|
+
=== Performance: a declared, non-semantic channel
|
|
206
|
+
|
|
207
|
+
Acceleration (native read layers, C minting, record drains, plan
|
|
208
|
+
streams) always lands behind version + face gates and degrades to the
|
|
209
|
+
spec-pinned fallback. Cross-adapter performance variance is sanctioned
|
|
210
|
+
exactly one place: the benchmark parity locks. Semantics never ride a
|
|
211
|
+
gate.
|
data/docs/index.adoc
CHANGED
|
@@ -14,6 +14,15 @@ applications. It offers a consistent API that abstracts away the underlying
|
|
|
14
14
|
XML implementation details while maintaining high performance through
|
|
15
15
|
efficient node mapping and native XPath querying.
|
|
16
16
|
|
|
17
|
+
Moxml's mission: **make "which XML engine am I running?" a deployment
|
|
18
|
+
decision instead of an API decision.** Libraries write once against a single
|
|
19
|
+
XML processing model; the engine underneath is a configuration choice. Any
|
|
20
|
+
program written against the facade produces the same observable outcome on
|
|
21
|
+
every adapter — performance may vary, semantics may not. The model is
|
|
22
|
+
specified normatively in the
|
|
23
|
+
link:_references/processing-model[Processing Model] document; known engine
|
|
24
|
+
differences are tracked in the link:_pages/divergences[divergence register].
|
|
25
|
+
|
|
17
26
|
== Key features
|
|
18
27
|
|
|
19
28
|
* **Unified interface** - Consistent API across different XML libraries
|
|
@@ -463,25 +463,72 @@ module Moxml
|
|
|
463
463
|
# instances). The hot bare reads super into the C methods
|
|
464
464
|
# (exact-name = the bare-name contract).
|
|
465
465
|
module Identity
|
|
466
|
+
# The C faces are captured before the contract modules
|
|
467
|
+
# shadow them — alias copies bind at this point, so the
|
|
468
|
+
# overrides below plain-dispatch the native reads
|
|
469
|
+
# (leptris-ruby#463 completed the surface: every contract
|
|
470
|
+
# face is a C method at 50-84ns).
|
|
466
471
|
ELEMENT = Class.new(::Leptris::XML::NativeNode) do
|
|
472
|
+
alias_method :native_name, :name
|
|
467
473
|
include ::Moxml::Node
|
|
468
474
|
include ::Moxml::Element
|
|
475
|
+
|
|
476
|
+
# Memo form kept: a warm read is an ivar hit (~30ns
|
|
477
|
+
# vs the C call's 84ns); the cold read drops the
|
|
478
|
+
# adapter round-trip and its PI-check bind_call.
|
|
479
|
+
def name
|
|
480
|
+
@name ||= native_name
|
|
481
|
+
end
|
|
469
482
|
end
|
|
470
483
|
TEXT = Class.new(::Leptris::XML::NativeNode) do
|
|
484
|
+
alias_method :native_content, :content
|
|
471
485
|
include ::Moxml::Node
|
|
472
486
|
include ::Moxml::Text
|
|
487
|
+
|
|
488
|
+
# Text#content's memo + entity restore are the
|
|
489
|
+
# contract; the backing read drops the adapter
|
|
490
|
+
# round-trip (dispatch + is_a? + bind_call) for the
|
|
491
|
+
# C face.
|
|
492
|
+
def raw_content
|
|
493
|
+
native_content
|
|
494
|
+
end
|
|
473
495
|
end
|
|
474
496
|
CDATA = Class.new(::Leptris::XML::NativeNode) do
|
|
497
|
+
alias_method :native_content, :content
|
|
475
498
|
include ::Moxml::Node
|
|
476
499
|
include ::Moxml::Cdata
|
|
500
|
+
|
|
501
|
+
def content
|
|
502
|
+
native_content
|
|
503
|
+
end
|
|
477
504
|
end
|
|
478
505
|
COMMENT = Class.new(::Leptris::XML::NativeNode) do
|
|
506
|
+
alias_method :native_content, :content
|
|
479
507
|
include ::Moxml::Node
|
|
480
508
|
include ::Moxml::Comment
|
|
509
|
+
|
|
510
|
+
# The nil bridge stays for floor-admitted bindings
|
|
511
|
+
# where the native face answers nil for comment-kind
|
|
512
|
+
# (leptris-ruby#313).
|
|
513
|
+
def content
|
|
514
|
+
content = native_content
|
|
515
|
+
return ::Moxml::Adapter::Leptris.to_binding(self).content if content.nil?
|
|
516
|
+
|
|
517
|
+
content
|
|
518
|
+
end
|
|
481
519
|
end
|
|
482
520
|
PROCESSING_INSTRUCTION = Class.new(::Leptris::XML::NativeNode) do
|
|
521
|
+
alias_method :native_content, :content
|
|
483
522
|
include ::Moxml::Node
|
|
484
523
|
include ::Moxml::ProcessingInstruction
|
|
524
|
+
|
|
525
|
+
# Same nil bridge as Comment (leptris-ruby#344).
|
|
526
|
+
def content
|
|
527
|
+
content = native_content
|
|
528
|
+
return ::Moxml::Adapter::Leptris.to_binding(self).content if content.nil?
|
|
529
|
+
|
|
530
|
+
content
|
|
531
|
+
end
|
|
485
532
|
end
|
|
486
533
|
|
|
487
534
|
TYPES = {
|
|
@@ -537,6 +584,46 @@ module Moxml
|
|
|
537
584
|
end
|
|
538
585
|
ELEMENT.include(Reads)
|
|
539
586
|
TEXT.include(Reads)
|
|
587
|
+
|
|
588
|
+
# leptris-ruby#503: name_read joins attr_read/text_read as
|
|
589
|
+
# the unshadowed contract faces. With them, the hot reads
|
|
590
|
+
# plain-dispatch the C faces (one Ruby frame over the
|
|
591
|
+
# ~45ns native call) instead of the adapter bind_call
|
|
592
|
+
# crossing. Face-probed — older bindings keep the
|
|
593
|
+
# adapter paths. The memos ride the existing
|
|
594
|
+
# invalidation: name= and clear_native_memo! clear
|
|
595
|
+
# @name, content= clears @content.
|
|
596
|
+
if NN.method_defined?(:name_read)
|
|
597
|
+
module ContractReads
|
|
598
|
+
def name
|
|
599
|
+
@name ||= name_read
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
ELEMENT.include(ContractReads)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
if NN.method_defined?(:text_read)
|
|
606
|
+
module TextContractReads
|
|
607
|
+
# The stamp fast path is inlined (same ivar protocol
|
|
608
|
+
# as Reads#doc_entity_bearing?, which stays the
|
|
609
|
+
# single writer on recompute) — the two frames it
|
|
610
|
+
# saves are ~20% of the cold read.
|
|
611
|
+
def content
|
|
612
|
+
value = @content
|
|
613
|
+
return value if value
|
|
614
|
+
|
|
615
|
+
value = text_read
|
|
616
|
+
stamp = @doc_entity_stamp
|
|
617
|
+
if stamp && (stamp >> 1) == ::Moxml::Adapter::Leptris.serialize_generation
|
|
618
|
+
value = ::Moxml::Adapter::Leptris.restore_entities(value) if stamp.allbits?(1)
|
|
619
|
+
elsif doc_entity_bearing?
|
|
620
|
+
value = ::Moxml::Adapter::Leptris.restore_entities(value)
|
|
621
|
+
end
|
|
622
|
+
@content = value
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
TEXT.include(TextContractReads)
|
|
626
|
+
end
|
|
540
627
|
end
|
|
541
628
|
|
|
542
629
|
# leptris-ruby#246 klass-propagating reads: the read
|
|
@@ -1246,10 +1333,28 @@ module Moxml
|
|
|
1246
1333
|
native_fragment.to_html
|
|
1247
1334
|
end
|
|
1248
1335
|
|
|
1249
|
-
|
|
1336
|
+
# leptris#1771's parse-lint (§13.2.5): the binding's parse_html
|
|
1337
|
+
# accepts collect_errors (and mode) and records the error-token
|
|
1338
|
+
# stream into the document's diagnostic channel, which
|
|
1339
|
+
# parse_diagnostics reads. Face-probed — the zero-dep gem lets
|
|
1340
|
+
# consumers resolve bindings predating the kwarg.
|
|
1341
|
+
COLLECT_ERRORS_NATIVE = ::Leptris::XML
|
|
1342
|
+
.method(:parse_html).parameters.any? do |_, key|
|
|
1343
|
+
key == :collect_errors
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def parse_html(html, options = {}, _context = nil)
|
|
1250
1347
|
html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
|
|
1251
1348
|
native_doc = begin
|
|
1252
|
-
|
|
1349
|
+
if COLLECT_ERRORS_NATIVE && options[:collect_errors]
|
|
1350
|
+
::Leptris::XML.parse_html(
|
|
1351
|
+
html_string,
|
|
1352
|
+
mode: options.fetch(:mode, :whatwg),
|
|
1353
|
+
collect_errors: true,
|
|
1354
|
+
)
|
|
1355
|
+
else
|
|
1356
|
+
::Leptris::XML.parse_html(html_string)
|
|
1357
|
+
end
|
|
1253
1358
|
rescue ::Leptris::XML::ParseError => e
|
|
1254
1359
|
raise Moxml::ParseError.new(e.message)
|
|
1255
1360
|
end
|
|
@@ -2743,6 +2848,16 @@ module Moxml
|
|
|
2743
2848
|
else
|
|
2744
2849
|
node.content = content.to_s
|
|
2745
2850
|
end
|
|
2851
|
+
rescue ::Leptris::XML::Error => e
|
|
2852
|
+
# Detached (parentless) nodes answer the engine's bare
|
|
2853
|
+
# "Invalid argument" (issue #401); created-unattached nodes
|
|
2854
|
+
# work, so only the failing detachment is re-raised, named.
|
|
2855
|
+
raise if node.parent
|
|
2856
|
+
|
|
2857
|
+
raise Moxml::Error,
|
|
2858
|
+
"cannot set text content on a detached (parentless) " \
|
|
2859
|
+
"node — attach it to a document first (issue #401): " \
|
|
2860
|
+
"#{e.message}"
|
|
2746
2861
|
end
|
|
2747
2862
|
|
|
2748
2863
|
def cdata_content(node)
|
|
@@ -2842,6 +2957,7 @@ module Moxml
|
|
|
2842
2957
|
PARENT_AXIS_CACHE = XPath::Cache.new(100)
|
|
2843
2958
|
|
|
2844
2959
|
def xpath(node, expression, namespaces = {})
|
|
2960
|
+
namespaces = normalize_xpath_namespaces(namespaces)
|
|
2845
2961
|
node = to_binding(node) if NATIVE_READ_LAYER
|
|
2846
2962
|
native = native_xpath(node, expression, namespaces)
|
|
2847
2963
|
return native unless native.nil?
|
|
@@ -2873,6 +2989,7 @@ module Moxml
|
|
|
2873
2989
|
end
|
|
2874
2990
|
|
|
2875
2991
|
def at_xpath(node, expression, namespaces = {})
|
|
2992
|
+
namespaces = normalize_xpath_namespaces(namespaces)
|
|
2876
2993
|
native = native_xpath(node, expression, namespaces, first_only: true)
|
|
2877
2994
|
return native unless native.nil?
|
|
2878
2995
|
|
|
@@ -2880,6 +2997,16 @@ module Moxml
|
|
|
2880
2997
|
result.is_a?(Array) ? result.first : result
|
|
2881
2998
|
end
|
|
2882
2999
|
|
|
3000
|
+
# Bindings arrive in both spellings (symbol keys are the
|
|
3001
|
+
# common Nokogiri idiom); every seam below — the "xmlns"
|
|
3002
|
+
# reserved-key check and the native pairs set — is
|
|
3003
|
+
# string-keyed (issue #395).
|
|
3004
|
+
def normalize_xpath_namespaces(namespaces)
|
|
3005
|
+
return namespaces unless namespaces.is_a?(Hash)
|
|
3006
|
+
|
|
3007
|
+
namespaces.transform_keys(&:to_s)
|
|
3008
|
+
end
|
|
3009
|
+
|
|
2883
3010
|
# @return [Array, Object, nil] native results, or nil when the
|
|
2884
3011
|
# query must run on the Ruby engine
|
|
2885
3012
|
def native_xpath(node, expression, namespaces, first_only: false)
|
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -768,6 +768,11 @@ module Moxml
|
|
|
768
768
|
|
|
769
769
|
native_node = unpatch_node(node)
|
|
770
770
|
return nil unless native_node
|
|
771
|
+
# Documents are container nodes (issue #400): their text is
|
|
772
|
+
# the root's concatenated descendant text; LibXML::XML::
|
|
773
|
+
# Document carries no content face of its own.
|
|
774
|
+
return native_node.root&.content.to_s if
|
|
775
|
+
native_node.is_a?(::LibXML::XML::Document)
|
|
771
776
|
|
|
772
777
|
native_node.content
|
|
773
778
|
end
|
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -452,6 +452,16 @@ module Moxml
|
|
|
452
452
|
end
|
|
453
453
|
|
|
454
454
|
def text_content(node)
|
|
455
|
+
# Documents are container nodes (issue #400): their text is
|
|
456
|
+
# the whole tree's text; Oga documents carry no text face
|
|
457
|
+
# (root_node answers the document itself), so fold the
|
|
458
|
+
# element children.
|
|
459
|
+
if node.is_a?(::Oga::XML::Document)
|
|
460
|
+
return node.children
|
|
461
|
+
.filter_map { |c| c.text if c.is_a?(::Oga::XML::Element) }
|
|
462
|
+
.join
|
|
463
|
+
end
|
|
464
|
+
|
|
455
465
|
node.text
|
|
456
466
|
end
|
|
457
467
|
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -607,6 +607,11 @@ module Moxml
|
|
|
607
607
|
# XPath.match for every expression: get_elements only
|
|
608
608
|
# handles simple child-name paths — attribute terminals,
|
|
609
609
|
# absolute paths and predicates silently degrade there.
|
|
610
|
+
# REXML resolves prefixes by string spelling — symbol-keyed
|
|
611
|
+
# bindings (the common Nokogiri idiom) must normalize or
|
|
612
|
+
# namespaced elements never match (issue #395).
|
|
613
|
+
namespaces = namespaces.transform_keys(&:to_s) if
|
|
614
|
+
namespaces.is_a?(Hash)
|
|
610
615
|
if namespaces && !namespaces.empty?
|
|
611
616
|
::REXML::XPath.match(node, expression, namespaces).to_a
|
|
612
617
|
else
|