moxml 0.5.146 → 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 +30 -2
- data/lib/moxml/adapter/libxml.rb +5 -0
- data/lib/moxml/adapter/oga.rb +10 -0
- data/lib/moxml/document.rb +9 -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_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 +10 -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
|
|
@@ -1333,10 +1333,28 @@ module Moxml
|
|
|
1333
1333
|
native_fragment.to_html
|
|
1334
1334
|
end
|
|
1335
1335
|
|
|
1336
|
-
|
|
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)
|
|
1337
1347
|
html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
|
|
1338
1348
|
native_doc = begin
|
|
1339
|
-
|
|
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
|
|
1340
1358
|
rescue ::Leptris::XML::ParseError => e
|
|
1341
1359
|
raise Moxml::ParseError.new(e.message)
|
|
1342
1360
|
end
|
|
@@ -2830,6 +2848,16 @@ module Moxml
|
|
|
2830
2848
|
else
|
|
2831
2849
|
node.content = content.to_s
|
|
2832
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}"
|
|
2833
2861
|
end
|
|
2834
2862
|
|
|
2835
2863
|
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
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Divergence register linkage (TODO.model/02): the register
|
|
6
|
+
# (docs/_pages/divergences.adoc) and the consistency suite must stay
|
|
7
|
+
# mechanically consistent — every capability-related skip cites a
|
|
8
|
+
# register ID, every register entry is cited by at least one spec, and
|
|
9
|
+
# no spec cites an unregistered ID.
|
|
10
|
+
RSpec.describe "divergence register linkage" do
|
|
11
|
+
REGISTER_PATH = "docs/_pages/divergences.adoc" # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
12
|
+
SCAN_GLOBS = %w[spec/consistency/**/*.rb spec/moxml/**/*.rb].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
13
|
+
NEXT_GATE = [ # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
14
|
+
/same engine as/, # engine dedup, not a divergence
|
|
15
|
+
/source_adapter == target_adapter/, # self round-trip
|
|
16
|
+
/MAX_SIZE/, # size guard
|
|
17
|
+
/AVAILABLE_ADAPTERS\.include/, # availability filter
|
|
18
|
+
/skip_shapes/, # shape routing (IDs cited above)
|
|
19
|
+
/leptris_fixed\?/, # version gate
|
|
20
|
+
/source_elements/, # data-presence guard
|
|
21
|
+
/next unless match/, # scanner-internal guard
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Version/availability gates are not divergences: a binding too old
|
|
25
|
+
# or an adapter not installed is a deployment fact, not an
|
|
26
|
+
# engine-vs-model difference at equal versions.
|
|
27
|
+
GATE = [ # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
28
|
+
/unavailable/i, /not available/, /predates/, /not installed/i,
|
|
29
|
+
/not present/i, /requires/i, /required\z/, /has no \S+ face/,
|
|
30
|
+
/lacks the /, /carries /, /degraded below/, /lockstep/i,
|
|
31
|
+
/unreleased/i, /stood down/, /recovered without/,
|
|
32
|
+
/Only \S+ available in CI/i, /adapter-specific/i, /noblanks/,
|
|
33
|
+
/SKIP_ parse flags/, /engine < \d/
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
def register_ids
|
|
37
|
+
File.readlines(REGISTER_PATH)
|
|
38
|
+
.filter_map { |line| line[/\A\|\s*(D-\d+)\s*$/, 1] }
|
|
39
|
+
.uniq
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def spec_files
|
|
43
|
+
SCAN_GLOBS.flat_map { |g| Dir[g] }.uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cited_ids
|
|
47
|
+
spec_files.flat_map { |path| File.readlines(path) }
|
|
48
|
+
.flat_map { |line| line.scan(/D-\d+/) }
|
|
49
|
+
.uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "has a register with entries" do
|
|
53
|
+
expect(register_ids).not_to be_empty
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "requires capability-related skips to cite a register ID" do
|
|
57
|
+
offenders = []
|
|
58
|
+
spec_files.each do |path|
|
|
59
|
+
File.readlines(path).each_with_index do |line, i|
|
|
60
|
+
next unless (msg = line[/skip\s+[&(]?\s*["'](.*?)["']/, 1])
|
|
61
|
+
|
|
62
|
+
next if GATE.any? { |re| re.match?(msg) }
|
|
63
|
+
next if msg.match?(/D-\d+/)
|
|
64
|
+
|
|
65
|
+
offenders << "#{path}:#{i + 1} #{msg.strip[0, 60]}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
expect(offenders).to be_empty,
|
|
69
|
+
"capability skips must cite a divergence-register " \
|
|
70
|
+
"ID (D-NN): #{offenders.join('; ')}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "requires adapter-exempting next-guards to cite a nearby register ID" do
|
|
74
|
+
offenders = []
|
|
75
|
+
spec_files.each do |path|
|
|
76
|
+
next if path.end_with?("divergence_register_spec.rb")
|
|
77
|
+
|
|
78
|
+
lines = File.readlines(path)
|
|
79
|
+
lines.each_with_index do |line, i|
|
|
80
|
+
next unless line.match?(/^\s*next\s+(if|unless)\s/)
|
|
81
|
+
next if NEXT_GATE.any? { |re| re.match?(line) }
|
|
82
|
+
next if line.match?(/D-\d+/)
|
|
83
|
+
|
|
84
|
+
context = lines[[i - 4, 0].max...i] + lines[(i + 1)..(i + 4)].to_a
|
|
85
|
+
next if context.any? { |l| l.match?(/D-\d+/) }
|
|
86
|
+
|
|
87
|
+
offenders << "#{path}:#{i + 1} #{line.strip[0, 60]}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
expect(offenders).to be_empty,
|
|
91
|
+
"adapter-exempting next-guards must cite a " \
|
|
92
|
+
"divergence-register ID (D-NN) on or above the " \
|
|
93
|
+
"line: #{offenders.join('; ')}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "cites every register entry from at least one spec" do
|
|
97
|
+
missing = register_ids - cited_ids
|
|
98
|
+
expect(missing).to be_empty,
|
|
99
|
+
"register entries never cited by a spec: " \
|
|
100
|
+
"#{missing.join(', ')}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "never cites an unregistered divergence ID" do
|
|
104
|
+
orphans = cited_ids - register_ids
|
|
105
|
+
expect(orphans).to be_empty,
|
|
106
|
+
"specs cite IDs missing from the register: " \
|
|
107
|
+
"#{orphans.join(', ')}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Issue #400: documents are container nodes — Document#text
|
|
6
|
+
# concatenates descendant text (Nokogiri's Document#text contract).
|
|
7
|
+
# The base Node#text answered "" and silently emptied consumers'
|
|
8
|
+
# content-preservation checks (canon's formatter asserted through
|
|
9
|
+
# doc.text).
|
|
10
|
+
RSpec.describe "Document#text concatenates descendant text" do
|
|
11
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
12
|
+
context "with #{adapter_name}" do
|
|
13
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
14
|
+
|
|
15
|
+
it "answers the concatenated descendant text" do
|
|
16
|
+
doc = ctx.parse("<r><a>x</a><b>y</b></r>")
|
|
17
|
+
expect(doc.text).to eq("xy")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "matches root.text (both containers recurse)" do
|
|
21
|
+
doc = ctx.parse("<r><a>x</a><b>y</b></r>")
|
|
22
|
+
expect(doc.text).to eq(doc.root.text)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "answers an empty string for element-free documents" do
|
|
26
|
+
expect(ctx.parse("<r/>").text).to eq("")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|