moxml 0.5.146 → 0.5.148
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 +100 -3
- data/lib/moxml/adapter/libxml.rb +5 -0
- data/lib/moxml/adapter/oga.rb +10 -0
- data/lib/moxml/document.rb +27 -4
- data/lib/moxml/node.rb +12 -0
- data/lib/moxml/version.rb +1 -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 +1 -1
- data/spec/moxml/adapter/leptris_contract_reads_spec.rb +29 -0
- data/spec/moxml/adapter/leptris_detached_text_spec.rb +41 -0
- data/spec/moxml/adapter/leptris_parse_lint_spec.rb +47 -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 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b51347e90c3dae3f4b0b41268b4ee8cd92fa91a4210a17b1ea75da85db0f82a
|
|
4
|
+
data.tar.gz: d251705fd512baed6295dad6a56c12e7db531dfa4845b2f77983c9c305bfaffd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ffcc8a80a304b656005d171627c40a982ba1b1594bbd2cb9090f390150f799b69041e338a54d2e79245a3fdf526639406938f4fdd82b290f55092c43ac20f34
|
|
7
|
+
data.tar.gz: 764dd4cd2fa5346a701224b7841ff0fb7cfced542b68ad914f4200acda168c5060813cb737429a5bcfd67eca12e74870b8bc4449bc83214678f5c86beaf3aa8c
|
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
|
|
@@ -105,6 +105,9 @@ module Moxml
|
|
|
105
105
|
::Leptris::XML::Document.method(:parse)
|
|
106
106
|
.parameters.any? { |_, key| key == :strict }
|
|
107
107
|
|
|
108
|
+
ADD_CHILD_NATIVE =
|
|
109
|
+
NATIVE_READ_LAYER && NN.method_defined?(:add_child_native)
|
|
110
|
+
|
|
108
111
|
if NATIVE_READ_LAYER
|
|
109
112
|
NN_ATTRIBUTE = NN.instance_method(:attribute)
|
|
110
113
|
NN_NAME = NN.instance_method(:name)
|
|
@@ -624,6 +627,31 @@ module Moxml
|
|
|
624
627
|
end
|
|
625
628
|
TEXT.include(TextContractReads)
|
|
626
629
|
end
|
|
630
|
+
|
|
631
|
+
# leptris-ruby#507: children_read joins the alias set. The
|
|
632
|
+
# generation-checked memo STAYS — re-walks of the same
|
|
633
|
+
# document are the common harness/consumer shape and the
|
|
634
|
+
# memo serves them allocation-free (dropping it regressed
|
|
635
|
+
# the walk 2x in GC). Only the MISS gets cheaper: the
|
|
636
|
+
# klass-children face plain-dispatches and returns the
|
|
637
|
+
# already-minted wrappers, so the adapter crossing
|
|
638
|
+
# (bind_call + to_a + keep-refs probe) drops out, and
|
|
639
|
+
# entity-marker documents take super for the split.
|
|
640
|
+
if NN.method_defined?(:children_read)
|
|
641
|
+
module ChildReads
|
|
642
|
+
def children
|
|
643
|
+
@children = nil if @children_gen != context.children_generation
|
|
644
|
+
@children_gen = context.children_generation
|
|
645
|
+
@children ||= if doc_entity_bearing?
|
|
646
|
+
super
|
|
647
|
+
else
|
|
648
|
+
NodeSet.new(children_read, context, self)
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
ELEMENT.include(ChildReads)
|
|
653
|
+
TEXT.include(ChildReads)
|
|
654
|
+
end
|
|
627
655
|
end
|
|
628
656
|
|
|
629
657
|
# leptris-ruby#246 klass-propagating reads: the read
|
|
@@ -1333,10 +1361,28 @@ module Moxml
|
|
|
1333
1361
|
native_fragment.to_html
|
|
1334
1362
|
end
|
|
1335
1363
|
|
|
1336
|
-
|
|
1364
|
+
# leptris#1771's parse-lint (§13.2.5): the binding's parse_html
|
|
1365
|
+
# accepts collect_errors (and mode) and records the error-token
|
|
1366
|
+
# stream into the document's diagnostic channel, which
|
|
1367
|
+
# parse_diagnostics reads. Face-probed — the zero-dep gem lets
|
|
1368
|
+
# consumers resolve bindings predating the kwarg.
|
|
1369
|
+
COLLECT_ERRORS_NATIVE = ::Leptris::XML
|
|
1370
|
+
.method(:parse_html).parameters.any? do |_, key|
|
|
1371
|
+
key == :collect_errors
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
def parse_html(html, options = {}, _context = nil)
|
|
1337
1375
|
html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
|
|
1338
1376
|
native_doc = begin
|
|
1339
|
-
|
|
1377
|
+
if COLLECT_ERRORS_NATIVE && options[:collect_errors]
|
|
1378
|
+
::Leptris::XML.parse_html(
|
|
1379
|
+
html_string,
|
|
1380
|
+
mode: options.fetch(:mode, :whatwg),
|
|
1381
|
+
collect_errors: true,
|
|
1382
|
+
)
|
|
1383
|
+
else
|
|
1384
|
+
::Leptris::XML.parse_html(html_string)
|
|
1385
|
+
end
|
|
1340
1386
|
rescue ::Leptris::XML::ParseError => e
|
|
1341
1387
|
raise Moxml::ParseError.new(e.message)
|
|
1342
1388
|
end
|
|
@@ -1446,6 +1492,14 @@ module Moxml
|
|
|
1446
1492
|
count = ffi.leptris_document_parse_diag_count(doc)
|
|
1447
1493
|
return [] if count.zero?
|
|
1448
1494
|
|
|
1495
|
+
# leptris#1771: one face carries the diag's machine-readable
|
|
1496
|
+
# code plus line/column with the entry (nil when the engine
|
|
1497
|
+
# has no token code — pre-#1771 records). The face probe
|
|
1498
|
+
# keeps older bindings on kind+message (issue #408).
|
|
1499
|
+
lint_code = if ffi.respond_to?(:leptris_document_parse_diag_code)
|
|
1500
|
+
ffi.method(:leptris_document_parse_diag_code)
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1449
1503
|
kind_ptr = ::FFI::MemoryPointer.new(:int)
|
|
1450
1504
|
diagnostics = []
|
|
1451
1505
|
count.times do |i|
|
|
@@ -1455,7 +1509,19 @@ module Moxml
|
|
|
1455
1509
|
) == 1
|
|
1456
1510
|
|
|
1457
1511
|
kind = DIAG_KINDS[kind_ptr.read_int] || :unknown
|
|
1458
|
-
|
|
1512
|
+
diag = { kind: kind, message: buffer.read_string }
|
|
1513
|
+
if lint_code
|
|
1514
|
+
code_buf = ::FFI::MemoryPointer.new(:char, 128)
|
|
1515
|
+
line_ptr = ::FFI::MemoryPointer.new(:int)
|
|
1516
|
+
column_ptr = ::FFI::MemoryPointer.new(:int)
|
|
1517
|
+
lint_code.call(doc, i, code_buf, 128, line_ptr, column_ptr)
|
|
1518
|
+
code = code_buf.read_string
|
|
1519
|
+
diag[:code] = code.empty? ? nil : code
|
|
1520
|
+
diag[:line] = line_ptr.read_int.zero? ? nil : line_ptr.read_int
|
|
1521
|
+
diag[:column] =
|
|
1522
|
+
column_ptr.read_int.zero? ? nil : column_ptr.read_int
|
|
1523
|
+
end
|
|
1524
|
+
diagnostics << diag
|
|
1459
1525
|
end
|
|
1460
1526
|
diagnostics
|
|
1461
1527
|
end
|
|
@@ -2478,6 +2544,27 @@ module Moxml
|
|
|
2478
2544
|
end
|
|
2479
2545
|
|
|
2480
2546
|
def add_child(parent, child)
|
|
2547
|
+
# Same-document Identity append — the builder hot path. One
|
|
2548
|
+
# document pair decides the verdict: the pin, the absorb
|
|
2549
|
+
# checks and the binding bridges only exist for
|
|
2550
|
+
# cross-document work, and the plain C append (version bump
|
|
2551
|
+
# inside) is the whole job. bind_call, not dispatch — the
|
|
2552
|
+
# contract modules shadow add_child on Identity receivers
|
|
2553
|
+
# (moxml#2560 note below).
|
|
2554
|
+
if NATIVE_MUTATIONS_COHERENT && parent.is_a?(NN) && child.is_a?(NN) &&
|
|
2555
|
+
!child.is_a?(CustomizedLeptris::EntityReference)
|
|
2556
|
+
parent_doc = NN_DOCUMENT.bind_call(parent)
|
|
2557
|
+
if parent_doc
|
|
2558
|
+
child_doc = NN_DOCUMENT.bind_call(child)
|
|
2559
|
+
if child_doc && parent_doc.equal?(child_doc)
|
|
2560
|
+
# add_child_native (leptris-ruby#509) plain-dispatches
|
|
2561
|
+
# on Identity receivers — one frame, no bind_call.
|
|
2562
|
+
return parent.add_child_native(child) if ADD_CHILD_NATIVE
|
|
2563
|
+
|
|
2564
|
+
return NN_ADD_CHILD.bind_call(parent, child)
|
|
2565
|
+
end
|
|
2566
|
+
end
|
|
2567
|
+
end
|
|
2481
2568
|
# 1.9.311: binding-family children (created PIs, texts...) —
|
|
2482
2569
|
# a DIRECT absorb makes the binding add below move the node
|
|
2483
2570
|
# by reference. A source already owned elsewhere would need
|
|
@@ -2830,6 +2917,16 @@ module Moxml
|
|
|
2830
2917
|
else
|
|
2831
2918
|
node.content = content.to_s
|
|
2832
2919
|
end
|
|
2920
|
+
rescue ::Leptris::XML::Error => e
|
|
2921
|
+
# Detached (parentless) nodes answer the engine's bare
|
|
2922
|
+
# "Invalid argument" (issue #401); created-unattached nodes
|
|
2923
|
+
# work, so only the failing detachment is re-raised, named.
|
|
2924
|
+
raise if node.parent
|
|
2925
|
+
|
|
2926
|
+
raise Moxml::Error,
|
|
2927
|
+
"cannot set text content on a detached (parentless) " \
|
|
2928
|
+
"node — attach it to a document first (issue #401): " \
|
|
2929
|
+
"#{e.message}"
|
|
2833
2930
|
end
|
|
2834
2931
|
|
|
2835
2932
|
def cdata_content(node)
|
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/document.rb
CHANGED
|
@@ -73,6 +73,15 @@ module Moxml
|
|
|
73
73
|
root_element ? Moxml::Node.wrap(root_element, context) : nil
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Documents are container nodes: #text concatenates descendant
|
|
77
|
+
# text, Nokogiri's Document#text contract (issue #400) — the base
|
|
78
|
+
# Node#text answered "" and silently emptied consumers'
|
|
79
|
+
# content-preservation checks.
|
|
80
|
+
def text
|
|
81
|
+
val = adapter.text_content(@native)
|
|
82
|
+
entity_bearing? ? adapter.restore_entities(val) : val
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# Materialize the root subtree — see Moxml::Materializer.
|
|
77
86
|
def materialize(&block)
|
|
78
87
|
return to_enum(:materialize) unless block
|
|
@@ -154,11 +163,21 @@ module Moxml
|
|
|
154
163
|
end
|
|
155
164
|
|
|
156
165
|
def create_element(name)
|
|
157
|
-
|
|
166
|
+
native = adapter.create_element(name, owner_doc: @native)
|
|
167
|
+
# leptris-ruby#508 mints registered klasses at create: the node
|
|
168
|
+
# IS a contract-carrying identity (@native is self) — prime it
|
|
169
|
+
# in place instead of shelling a second wrapper (a raw mint
|
|
170
|
+
# reaching ==/adapter dies on the nil @context).
|
|
171
|
+
return native.prime_contract(native, context, adapter) if native.is_a?(Moxml::Node)
|
|
172
|
+
|
|
173
|
+
Wrappers::Element.new(native, context)
|
|
158
174
|
end
|
|
159
175
|
|
|
160
176
|
def create_text(content)
|
|
161
|
-
|
|
177
|
+
native = adapter.create_text(content, owner_doc: @native)
|
|
178
|
+
return native.prime_contract(native, context, adapter) if native.is_a?(Moxml::Node)
|
|
179
|
+
|
|
180
|
+
Wrappers::Text.new(native, context)
|
|
162
181
|
end
|
|
163
182
|
|
|
164
183
|
def create_cdata(content)
|
|
@@ -218,9 +237,13 @@ module Moxml
|
|
|
218
237
|
raise Error, "Document already has a root element"
|
|
219
238
|
else
|
|
220
239
|
adapter.add_child(@native, node.native)
|
|
221
|
-
# Refresh native for adapters where identity changes (e.g., LibXML doc.root=)
|
|
240
|
+
# Refresh native for adapters where identity changes (e.g., LibXML doc.root=).
|
|
241
|
+
# Native equality via the adapter, not wrapper !=: re-mints can
|
|
242
|
+
# arrive without context primed (1.9.373 actual_native), and
|
|
243
|
+
# wrapper == dispatches through the adapter (issue #409-class).
|
|
222
244
|
refreshed = adapter.actual_native(node.native, @native)
|
|
223
|
-
node.refresh_native!(refreshed) if refreshed &&
|
|
245
|
+
node.refresh_native!(refreshed) if refreshed &&
|
|
246
|
+
!adapter.same_node?(refreshed, node.native)
|
|
224
247
|
end
|
|
225
248
|
node.parent_node = self
|
|
226
249
|
invalidate_children_cache!
|