moxml 0.5.30 → 0.5.31
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/docs/_pages/adapter-protocol.adoc +7 -0
- data/docs/_pages/adapters/leptris.adoc +61 -0
- data/docs/_pages/conversion-apis.adoc +18 -0
- data/docs/_pages/performance.adoc +142 -0
- data/lib/compat/opal/moxml_boot.rb +1 -0
- data/lib/moxml/adapter/base.rb +64 -0
- data/lib/moxml/adapter/leptris/markers.rb +4 -1
- data/lib/moxml/adapter/leptris/serialize.rb +34 -17
- data/lib/moxml/adapter/leptris.rb +151 -4
- data/lib/moxml/adapter/libxml.rb +13 -2
- data/lib/moxml/adapter/nokogiri.rb +55 -4
- data/lib/moxml/adapter/ox.rb +8 -0
- data/lib/moxml/adapter/rexml.rb +8 -0
- data/lib/moxml/attribute_resolver.rb +35 -0
- data/lib/moxml/c14n.rb +30 -0
- data/lib/moxml/context.rb +67 -7
- data/lib/moxml/document.rb +2 -2
- data/lib/moxml/element.rb +50 -5
- data/lib/moxml/lazy_node_set.rb +16 -0
- data/lib/moxml/native_attachment/native.rb +8 -2
- data/lib/moxml/node.rb +33 -22
- data/lib/moxml/node_set.rb +48 -25
- data/lib/moxml/sax/block_handler.rb +8 -4
- data/lib/moxml/sax/namespace_splitter.rb +16 -7
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +12 -1
- data/lib/moxml.rb +1 -0
- data/spec/moxml/adapter/leptris_spec.rb +216 -1
- data/spec/moxml/adapter/nokogiri_spec.rb +17 -0
- data/spec/moxml/context_spec.rb +13 -0
- data/spec/moxml/element_spec.rb +25 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32f1034201de38bed35a6b1496d10f96a580f0e4c92517c2253ded0631331439
|
|
4
|
+
data.tar.gz: bb3d697a73fbac699e00b51905ef446f9837b9ee649785bb1ed4df2027870e78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a854ab201001d88494c37d79b163bbf4246880702f5c7d61cf52fd41128459be2a1e8c4601c894852b6aa73e33efca3c471ebfdbf9a126120afa698ab28aff1
|
|
7
|
+
data.tar.gz: 531a60b0d52e84f4f0df5a026d51b5eeed850b56da1c1eea23297c2c2fb331efa4047a9dc73ef21a430998035aad7a58fe8adb7905047f2001f5b941d07353fc
|
|
@@ -29,6 +29,13 @@ whitespace-only text nodes where the engine has the flag
|
|
|
29
29
|
(#147; recover-mode diagnostics — `[]` on clean parses; engines with
|
|
30
30
|
an error channel map it here, e.g. Nokogiri's `doc.errors`).
|
|
31
31
|
|
|
32
|
+
HTML mode::
|
|
33
|
+
`parse_html(html, options, context)` — tolerant HTML4/5 parsing into
|
|
34
|
+
the standard DOM, where the engine offers it (leptris >= 1.9.80 via
|
|
35
|
+
engine #659; Nokogiri via libxml2's HTML parser). The base class
|
|
36
|
+
raises `Moxml::AdapterError` — adapters without an HTML mode keep
|
|
37
|
+
that default.
|
|
38
|
+
|
|
32
39
|
Tree navigation::
|
|
33
40
|
`children`, `parent`, `next_sibling`, `previous_sibling`, `root`,
|
|
34
41
|
`set_root`, `document`, `duplicate_node`. `children` on a document
|
|
@@ -51,6 +51,67 @@ libxml2/Nokogiri/REXML semantics (libleptris >= 1.9.8).
|
|
|
51
51
|
Raise `Moxml::ParseError` on malformed input instead of returning an
|
|
52
52
|
empty document.
|
|
53
53
|
|
|
54
|
+
== HTML parsing
|
|
55
|
+
|
|
56
|
+
`Context#parse_html(html)` (leptris >= 1.9.80; engine
|
|
57
|
+
leptris/leptris/#659) parses tolerant HTML4/5 into the standard XML
|
|
58
|
+
DOM: implied end tags (`<li>a<li>b`), void elements (`<br>`, `<img>`),
|
|
59
|
+
raw-text script/style, case-insensitive lowercased names and
|
|
60
|
+
attributes, minimized/boolean attributes, and the HTML named-entity
|
|
61
|
+
table. `html`/`head`/`body` are synthesized when the input implies
|
|
62
|
+
them (no empty head for headless fragments); tables get no implied
|
|
63
|
+
`tbody`. Malformed input degrades to text — the parse never fails;
|
|
64
|
+
only an entirely empty result raises `Moxml::ParseError`.
|
|
65
|
+
|
|
66
|
+
[source,ruby]
|
|
67
|
+
----
|
|
68
|
+
doc = Moxml.new(:leptris).parse_html(%(<ul><li>a<li>b</ul>))
|
|
69
|
+
doc.xpath("//li").map(&:text) # => ["a", "b"]
|
|
70
|
+
----
|
|
71
|
+
|
|
72
|
+
Serialization is XML-shaped: text content is escaped (`a < b`),
|
|
73
|
+
so script bodies with markup characters round-trip through a strict
|
|
74
|
+
XML reparse. The adapter stands the entity-marker pipeline down for
|
|
75
|
+
HTML parses — the engine decodes HTML entities directly into text.
|
|
76
|
+
|
|
77
|
+
Measured on 1.9.105 (named-entity decoding fixed, engine #848):
|
|
78
|
+
parse 2.5-3.5x, serialize 4.1x, xpath 6-29x faster than Nokogiri —
|
|
79
|
+
uniformly, including entity-laden pages.
|
|
80
|
+
|
|
81
|
+
Adapters without an engine HTML mode (Ox, Oga, REXML, LibXML) raise
|
|
82
|
+
`Moxml::AdapterError`; Nokogiri parses through libxml2's HTML mode.
|
|
83
|
+
|
|
84
|
+
== Streaming iteration — `iterparse`
|
|
85
|
+
|
|
86
|
+
`Context#iterparse(xml, mode:)` / `iterparse_file(path, mode:)`
|
|
87
|
+
expose the engine's incremental parser (libleptris v1.6, #586):
|
|
88
|
+
completed elements yield while parsing, prior subtrees release, and
|
|
89
|
+
memory stays bounded by the largest subtree — `iterparse_file`
|
|
90
|
+
streams the file C-side. `:top_level` yields the root's children;
|
|
91
|
+
`:full_document` yields every element in completion (post-order)
|
|
92
|
+
order.
|
|
93
|
+
|
|
94
|
+
Measured on a 21MB / 108k-element file: **+20MB peak RSS** (full
|
|
95
|
+
parse: +169MB; Nokogiri Reader: +62MB), bare iteration ~97ms on
|
|
96
|
+
leptris >= 1.9.105 (Nokogiri Reader: 179ms) — ~1.8x faster at a
|
|
97
|
+
third of the memory. Attribute reads on yielded elements improved
|
|
98
|
+
~16x on current builds (the parentless-read cost filed in
|
|
99
|
+
leptris-ruby#152 is fixed).
|
|
100
|
+
|
|
101
|
+
The yielded elements are parentless (`#document` nil) and valid
|
|
102
|
+
only inside the block — the subtree is released when the iterator
|
|
103
|
+
advances. Since binding 1.9.105, access after the iteration raises
|
|
104
|
+
`UseAfterFreeError` instead of crashing (leptris-ruby#152). Adapters without an engine iterator raise
|
|
105
|
+
`Moxml::AdapterError`.
|
|
106
|
+
|
|
107
|
+
[source,ruby]
|
|
108
|
+
----
|
|
109
|
+
Moxml.new(:leptris).iterparse_file("huge.xml") do |record|
|
|
110
|
+
process(record.at_attrs, record.children.map(&:text))
|
|
111
|
+
end
|
|
112
|
+
----
|
|
113
|
+
|
|
114
|
+
== Lifecycle
|
|
54
115
|
== Lifecycle
|
|
55
116
|
|
|
56
117
|
`Document#free` releases the C tree deterministically — batch
|
|
@@ -89,6 +89,24 @@ context.parse(dtd_xml) # plain: ATTLIST defaults excluded
|
|
|
89
89
|
context.parse(dtd_xml, dtdattr: true) # opt-in: attr="default" materialized
|
|
90
90
|
----
|
|
91
91
|
|
|
92
|
+
== HTML input — `parse_html`
|
|
93
|
+
|
|
94
|
+
Web-scraped and HTML-fragment inputs parse tolerantly into the
|
|
95
|
+
standard DOM (leptris >= 1.9.80 via engine #659; Nokogiri via
|
|
96
|
+
libxml2's HTML mode; other adapters raise `Moxml::AdapterError`):
|
|
97
|
+
|
|
98
|
+
[source,ruby]
|
|
99
|
+
----
|
|
100
|
+
doc = context.parse_html(%(<ul><li>a<li>b</ul>))
|
|
101
|
+
doc.xpath("//li").map(&:text) # => ["a", "b"]
|
|
102
|
+
----
|
|
103
|
+
|
|
104
|
+
Implied end tags, void elements, lowercased names, boolean
|
|
105
|
+
attributes, and HTML named entities decode at parse; serialization
|
|
106
|
+
is XML-shaped. See the leptris adapter page for the semantics
|
|
107
|
+
matrix. On leptris >= 1.9.105 every HTML operation — including
|
|
108
|
+
entity-laden pages — measures 2.5-4x faster than Nokogiri.
|
|
109
|
+
|
|
92
110
|
== Pretty-print parity (issue #129)
|
|
93
111
|
|
|
94
112
|
Through the leptris adapter, byte-identical output to raw Nokogiri:
|
|
@@ -56,6 +56,148 @@ Consumers on CRuby 3.3+ (canon's pretty-print pipeline, batch
|
|
|
56
56
|
conversion) should treat it as the default; moxml's own CI
|
|
57
57
|
performance gates run with YJIT enabled.
|
|
58
58
|
|
|
59
|
+
=== Wrapper retention — the identity map is weak
|
|
60
|
+
|
|
61
|
+
`Context` keeps a native→wrapper identity map so repeated traversals
|
|
62
|
+
hand back the same wrapper. Since 0.5.31 it is an
|
|
63
|
+
`ObjectSpace::WeakMap`: entries die with their native, so
|
|
64
|
+
parse-and-drop workloads release wrappers, natives, and (once the
|
|
65
|
+
binding's finalizer runs) the C subtrees. Measured walking and
|
|
66
|
+
dropping 220 documents (~600 elements each) with one long-lived
|
|
67
|
+
Context: **+1.9MB RSS growth vs +41MB before**, zero wrapper
|
|
68
|
+
retention beyond the binding's own constant one-document cache —
|
|
69
|
+
and no identity loss, which the previous 65,536-entry wholesale
|
|
70
|
+
clear traded away when it fired.
|
|
71
|
+
|
|
72
|
+
=== Measuring moxml — methodology and traps
|
|
73
|
+
|
|
74
|
+
Every number in this page follows the same discipline; contributors
|
|
75
|
+
benchmarking moxml should too. The traps below all produced wrong
|
|
76
|
+
conclusions at least once.
|
|
77
|
+
|
|
78
|
+
*Timing:*
|
|
79
|
+
|
|
80
|
+
- min-of-N (>= 10 samples) around loops, never single-shot averages;
|
|
81
|
+
`Process.clock_gettime(Process::CLOCK_MONOTONIC)`; `GC.start`
|
|
82
|
+
before each round.
|
|
83
|
+
- Time LOOPS, not single calls: single sub-microsecond operations
|
|
84
|
+
read as 0 on some clocks (macOS CLOCK_MONOTONIC granularity).
|
|
85
|
+
- Gate on load average — shared machines under build load (load 30+)
|
|
86
|
+
swing identical runs 2x. Wait for a calm window (< 5).
|
|
87
|
+
- One heavy benchmark at a time; concurrent suites and benchmarks
|
|
88
|
+
have crashed this machine.
|
|
89
|
+
- dup benchmarks must free each copy — native trees accumulate
|
|
90
|
+
otherwise and the numbers degrade monotonically (that drift is a
|
|
91
|
+
leak signal, not noise).
|
|
92
|
+
|
|
93
|
+
*Allocation counts (GC.stat(:total_allocated_objects)) are
|
|
94
|
+
load-immune — use them when the machine is busy:*
|
|
95
|
+
|
|
96
|
+
- WARM THE PATH FIRST: the first call through any code path pays
|
|
97
|
+
autoloads (adapter, xpath engine, serialize modules — dozens of
|
|
98
|
+
classes and regexps). A cold first run charges those to whichever
|
|
99
|
+
variant ran first and fabricates "superadditive" overhead.
|
|
100
|
+
- Keep the fixture shape identical across compared cells: parent
|
|
101
|
+
vs leaf elements allocate differently in every engine.
|
|
102
|
+
- Negative deltas are impossible — if you see one, the harness is
|
|
103
|
+
broken, not the code.
|
|
104
|
+
|
|
105
|
+
*Correctness under load:* race repros and corruption checks need
|
|
106
|
+
hundreds of iterations — spot checks of 30 have missed 5-30% races
|
|
107
|
+
on this codebase.
|
|
108
|
+
|
|
109
|
+
=== leptris 1.9.105 numbers (reference table)
|
|
110
|
+
|
|
111
|
+
XML parse 2.4-2.8x, document serialize 2.4x, element queries 32x,
|
|
112
|
+
attribute-node queries 1.4x (native since 1.9.105), HTML parse
|
|
113
|
+
2.5-3.5x including entity-laden pages (engine #848 fixed), HTML
|
|
114
|
+
serialize 4.1x, streaming iteration ~97ms/21MB at +21MB RSS (~1.8x Nokogiri
|
|
115
|
+
Reader), C14n 1.0 native on leptris >= 1.9.121 (engine #919 fixed)
|
|
116
|
+
at 1.8x+ Nokogiri through the full document path.
|
|
117
|
+
|
|
118
|
+
SAX (calm-machine, verified): the raw binding parses 1.19x faster
|
|
119
|
+
than Nokogiri; through moxml's unified handler the API contract
|
|
120
|
+
(per-event attribute Hash + namespace separation) nets 0.74x interp
|
|
121
|
+
/ 0.93x YJIT.
|
|
122
|
+
|
|
123
|
+
=== Element serialization
|
|
124
|
+
|
|
125
|
+
Bulk `element.to_xml` (per-element, no options) was wrapper-tax
|
|
126
|
+
bound — the raw engine serializes 3x faster than Nokogiri but the
|
|
127
|
+
Ruby option/guard layers spent 4.6x the C call's cost per element.
|
|
128
|
+
The argless path is now allocation-free (frozen prebuilt defaults),
|
|
129
|
+
the element face skips the declaration fetch, UTF-8 requests ride
|
|
130
|
+
the binding's shared default options, the trailing-newline strip is
|
|
131
|
+
version-gated (engine fixed in 1.9.42), and `entity_bearing?` memoizes
|
|
132
|
+
against an adapter generation counter. Measured per element:
|
|
133
|
+
3-child **0.74x -> 1.0x** (parity — fixed guard tax on tiny output),
|
|
134
|
+
21-child **1.7x**; document-level serialize stays 2.4x.
|
|
135
|
+
|
|
136
|
+
=== Held-document footprint
|
|
137
|
+
|
|
138
|
+
Four lifecycles, measured (RSS, isolated processes; leptris 1.9.80
|
|
139
|
+
vs Nokogiri/libxml2):
|
|
140
|
+
|
|
141
|
+
| lifecycle | moxml(leptris) | nokogiri | |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| bare tree (parse+hold, unaccessed) | **203 B/node** | 408 B/node | engine tree 2.0x smaller |
|
|
144
|
+
| parse + walk + drop | leak-free (weak registry) | — | `#free` for deterministic release |
|
|
145
|
+
| peak transient, 3.6MB doc (parse+pretty-print) | +48.8MB | +55.6MB | **0.88x smaller peak** |
|
|
146
|
+
| held + walked (37KB doc) | 1186 kB/doc | 754 kB/doc | 1.57x larger — binding wrapper layer |
|
|
147
|
+
|
|
148
|
+
The one regression is held-and-walked: the binding's per-node Ruby
|
|
149
|
+
wrappers (element 80 B + a dedicated 88 B `FFI::Pointer` + cache
|
|
150
|
+
slot ≈ 170+ B/node) outweigh even the C tree; upstream
|
|
151
|
+
leptris-ruby#147 tracks it with a TypedData fix path. Guidance for
|
|
152
|
+
held workloads: keep documents you will not re-traverse in the
|
|
153
|
+
parse-and-read lifecycle (`readonly:`, `#free`), and prefer
|
|
154
|
+
`materialize` for one-shot conversion.
|
|
155
|
+
|
|
156
|
+
=== Programmatic construction
|
|
157
|
+
|
|
158
|
+
Building documents node-by-node is the one surface where the C
|
|
159
|
+
engine's lead does not carry through: each create/attach/attribute
|
|
160
|
+
write crosses the FFI seam, and Nokogiri's C-extension calls are
|
|
161
|
+
cheaper per crossing. Measured end to end:
|
|
162
|
+
|
|
163
|
+
| builder shape | interp | YJIT |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| node churn (1052 tiny nodes — worst case) | 0.40x | 0.78x |
|
|
166
|
+
| payload-realistic (attrs + long text) | 0.58x | **~1.0x (parity)** |
|
|
167
|
+
|
|
168
|
+
Run builders with YJIT (`RUBY_YJIT_ENABLE=1`, see above): it
|
|
169
|
+
compiles moxml's wrapper layer away (-47% on the churn shape) while
|
|
170
|
+
leaving Nokogiri's thin C layer unchanged. The remaining churn-shape
|
|
171
|
+
gap is the binding's own floor — even a zero-overhead wrapper
|
|
172
|
+
measures 0.90x under YJIT (leptris-ruby#149 tracks it, with a
|
|
173
|
+
batch-create proposal to collapse the crossings).
|
|
174
|
+
|
|
175
|
+
=== Bulk construction — `append_xml`
|
|
176
|
+
|
|
177
|
+
`element.append_xml(fragment)` appends a raw fragment's top-level
|
|
178
|
+
nodes by parsing it and attaching the subtrees — the engine's C
|
|
179
|
+
parser does the node construction in one crossing instead of an FFI
|
|
180
|
+
round trip per create/attach/attribute call:
|
|
181
|
+
|
|
182
|
+
| 300-node subtree | time | vs per-node |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| per-node build (leptris) | 2065µs | — |
|
|
185
|
+
| **append_xml (leptris)** | **1019µs** | **2.0x** |
|
|
186
|
+
| per-node build (nokogiri) | 1746µs | append_xml is 1.7x faster |
|
|
187
|
+
|
|
188
|
+
The fragment must be namespace-self-contained and well-formed as
|
|
189
|
+
the content of one wrapper element. Supported on leptris, nokogiri,
|
|
190
|
+
oga, and rexml; the Ox adapter raises (its customized node wrappers
|
|
191
|
+
do not survive cross-document attachment).
|
|
192
|
+
|
|
193
|
+
=== Lazy result sets
|
|
194
|
+
|
|
195
|
+
`xpath` results and `children` node sets allocate wrapper slots on
|
|
196
|
+
first access, not at construction — `.size`/`.empty?`/`.first` on a
|
|
197
|
+
1000-node result no longer allocate the 1000-slot wrapper array
|
|
198
|
+
(that path also skips the binding's per-node wrapper materialization
|
|
199
|
+
entirely; see `LazyNodeSet` under the leptris adapter).
|
|
200
|
+
|
|
59
201
|
=== Prefer bulk paths for per-node conversion
|
|
60
202
|
|
|
61
203
|
Per-node Ruby iteration pays the wrapper tax per element. When the
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -28,6 +28,35 @@ module Moxml
|
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Streaming incremental parse (leptris engine): yields each
|
|
32
|
+
# completed element while the parse runs, releasing prior
|
|
33
|
+
# subtrees — memory bounded by the largest subtree, not the
|
|
34
|
+
# document. Adapters without an engine iterator raise.
|
|
35
|
+
def iterparse(_xml, _mode = :top_level, _context = nil)
|
|
36
|
+
raise Moxml::AdapterError.new(
|
|
37
|
+
"Streaming iteration is not supported by the #{name.split('::').last} adapter",
|
|
38
|
+
adapter: name, operation: "iterparse",
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def iterparse_file(_path, _mode = :top_level, _context = nil)
|
|
43
|
+
raise Moxml::AdapterError.new(
|
|
44
|
+
"Streaming file iteration is not supported by the #{name.split('::').last} adapter",
|
|
45
|
+
adapter: name, operation: "iterparse_file",
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Tolerant HTML4/5 parsing into the standard DOM (engine
|
|
50
|
+
# issue leptris/leptris#659): implied end tags, void elements,
|
|
51
|
+
# case-insensitive lowercased names, the HTML named-entity
|
|
52
|
+
# table. Adapters whose engine has an HTML mode override this.
|
|
53
|
+
def parse_html(_html, _options = {}, _context = nil)
|
|
54
|
+
raise Moxml::AdapterError.new(
|
|
55
|
+
"HTML parsing is not supported by the #{name.split('::').last} adapter",
|
|
56
|
+
adapter: name, operation: "parse_html",
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
31
60
|
def parse(_xml, _options = {}, _context = nil)
|
|
32
61
|
raise Moxml::NotImplementedError.new(
|
|
33
62
|
"parse not implemented",
|
|
@@ -181,6 +210,41 @@ namespace_validation_mode: :strict)
|
|
|
181
210
|
false
|
|
182
211
|
end
|
|
183
212
|
|
|
213
|
+
# Whether add_child can keep tracking the same native —
|
|
214
|
+
# adapters that may recreate the node on attach (libxml's
|
|
215
|
+
# doc.root=) override to false so the wrapper refresh path
|
|
216
|
+
# stays armed.
|
|
217
|
+
def native_identity_stable?
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Whether a BARE-name attribute READ addresses only the
|
|
222
|
+
# no-namespace attribute (qualified-name semantics) — the
|
|
223
|
+
# gate for Element#[]'s fast path (bare_attr_value).
|
|
224
|
+
# Differs per engine: rexml's bare read returns a namespaced
|
|
225
|
+
# sibling's value; oga's raw values need resolver-only
|
|
226
|
+
# marker restoration.
|
|
227
|
+
def bare_get_qname_safe?
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Whether set_attribute with a BARE name behaves as a
|
|
232
|
+
# qualified-name write: replaces only the no-namespace
|
|
233
|
+
# attribute and never touches a namespaced p:<local> sibling.
|
|
234
|
+
# Verified per engine; oga's repeated bare writes diverge, so
|
|
235
|
+
# it stays false there and assign keeps the full resolve.
|
|
236
|
+
def bare_set_qname_safe?
|
|
237
|
+
false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Generation of adapter-level state that cached serialize
|
|
241
|
+
# decisions depend on (leptris: the entity-marker document
|
|
242
|
+
# flag). Bumping invalidates wrapper-level memos; adapters
|
|
243
|
+
# whose answers are static keep the constant zero.
|
|
244
|
+
def serialize_generation
|
|
245
|
+
0
|
|
246
|
+
end
|
|
247
|
+
|
|
184
248
|
# Whether the subtree at native can contain entity markers.
|
|
185
249
|
# Marker-tracking adapters override this so the post-serialize
|
|
186
250
|
# restore can skip its full-output scans on marker-free
|
|
@@ -15,8 +15,11 @@ module Moxml
|
|
|
15
15
|
CustomizedLeptris::DocumentPI
|
|
16
16
|
true
|
|
17
17
|
else
|
|
18
|
+
# Parentless elements come only from Iterparse —
|
|
19
|
+
# engine parsed, outside the marker pipeline: never
|
|
20
|
+
# bearing.
|
|
18
21
|
doc = native.document
|
|
19
|
-
doc.nil?
|
|
22
|
+
doc.nil? ? false : attachments.get(doc, :entity_markers) != false
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -40,6 +40,15 @@ module Moxml
|
|
|
40
40
|
xml
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Element-face trailing-newline strip: engine fix landed in
|
|
44
|
+
# 1.9.42; armed only on older floor bindings.
|
|
45
|
+
TRAILING_NL_STRIP_ACTIVE =
|
|
46
|
+
Gem::Version.new(::Leptris::VERSION) < Gem::Version.new("1.9.42")
|
|
47
|
+
|
|
48
|
+
# The binding's element face with all-default options — the
|
|
49
|
+
# frozen splat keeps the hot argless path allocation-free.
|
|
50
|
+
ELEMENT_DEFAULT_KWARGS = { indent: 0, no_decl: true, encoding: nil }.freeze
|
|
51
|
+
|
|
43
52
|
def raw_serialize(node, options)
|
|
44
53
|
# CDATA must precede Text in this chain: CDATA < Text in the
|
|
45
54
|
# binding, so a Text branch first would swallow CDATA nodes.
|
|
@@ -60,23 +69,31 @@ module Moxml
|
|
|
60
69
|
return serialize_document(node, options)
|
|
61
70
|
end
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
# Element serialization never emits a declaration — the C
|
|
73
|
+
# element serializer ignores the flag (verified
|
|
74
|
+
# byte-identical); skipping the declaration fetch avoids a
|
|
75
|
+
# document attachment walk per element serialize. Likewise
|
|
76
|
+
# an unset encoding serializes UTF-8 — byte-identical to an
|
|
77
|
+
# explicit "UTF-8" — and the binding then reuses its shared
|
|
78
|
+
# DEFAULT_OPTIONS instead of rebuilding an options struct
|
|
79
|
+
# per call; the wrapper force-tags the string either way.
|
|
80
|
+
indent = options.fetch(:indent, 0)
|
|
81
|
+
encoding = options[:encoding] == "UTF-8" ? nil : options[:encoding]
|
|
82
|
+
xml = if indent.zero? && encoding.nil?
|
|
83
|
+
node.to_xml(**ELEMENT_DEFAULT_KWARGS)
|
|
84
|
+
else
|
|
85
|
+
kwargs = { indent: indent, no_decl: true, encoding: encoding }
|
|
86
|
+
if INDENT_UNIT_SUPPORTED && options[:indent_text].is_a?(String)
|
|
87
|
+
kwargs[:indent_text] = options[:indent_text]
|
|
88
|
+
end
|
|
89
|
+
node.to_xml(**kwargs)
|
|
90
|
+
end
|
|
91
|
+
# Element output always ends with the close tag — but older
|
|
92
|
+
# engines append a stray trailing newline when the element's
|
|
93
|
+
# last text child is non-ASCII (fixed engine side in
|
|
94
|
+
# 1.9.42). The strip stays armed only below that version —
|
|
95
|
+
# a regex sub per element serialize is measurable in bulk.
|
|
96
|
+
TRAILING_NL_STRIP_ACTIVE ? xml.sub(/\n+\z/, "") : xml
|
|
80
97
|
end
|
|
81
98
|
|
|
82
99
|
# A bare ampersand — not starting a named or numeric entity
|
|
@@ -23,6 +23,58 @@ module Moxml
|
|
|
23
23
|
# the adapter no longer carries accommodation paths for them.
|
|
24
24
|
MINIMUM_BINDING_VERSION = "1.9.32"
|
|
25
25
|
|
|
26
|
+
# leptris_parse_html_string shipped in bindings 1.9.80
|
|
27
|
+
# (libleptris 1.9.75, engine #659) as Leptris::XML.parse_html.
|
|
28
|
+
HTML_PARSE_SUPPORTED =
|
|
29
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.80")
|
|
30
|
+
|
|
31
|
+
# Attribute-node xpath results carry proper wrappers since
|
|
32
|
+
# 1.9.105 (leptris-ruby#153: ResultAttr with name/value);
|
|
33
|
+
# before that the native gate routed them to the Ruby engine.
|
|
34
|
+
ATTR_RESULT_NATIVE =
|
|
35
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.105")
|
|
36
|
+
|
|
37
|
+
# Native C14N delegation probe: the engine's C14N must
|
|
38
|
+
# byte-match the Ruby reference (ported from canon) on a
|
|
39
|
+
# namespace-sorting + attributes + comments shape before
|
|
40
|
+
# Moxml::C14n hands the default path to it. The engine
|
|
41
|
+
# currently emits namespace declarations in document order
|
|
42
|
+
# instead of lexicographic (filed leptris/leptris#881); the
|
|
43
|
+
# probe auto-adopts the native path once a fixed build lands —
|
|
44
|
+
# no moxml release needed.
|
|
45
|
+
# Lazy, not load-time: the probe exercises the wrapper layer
|
|
46
|
+
# (Document/serialize/C14n), which is circular while THIS
|
|
47
|
+
# adapter file is still loading — the load-time form rescued
|
|
48
|
+
# to false on every build and masked a landed engine fix.
|
|
49
|
+
def self.native_c14n_byte_safe?
|
|
50
|
+
return @native_c14n_byte_safe unless @native_c14n_byte_safe.nil?
|
|
51
|
+
|
|
52
|
+
@native_c14n_byte_safe = begin
|
|
53
|
+
probe_xml = %(<?xml version="1.0"?><doc xmlns:p="urn:p" xmlns="urn:d" b="2" a="1"><e p:x="v" z="w">t & u</e><!-- c --></doc>)
|
|
54
|
+
native_doc = ::Leptris::XML::Document.parse(probe_xml)
|
|
55
|
+
native = native_doc.root.canonicalize(
|
|
56
|
+
::Leptris::XML::FFI::C14N_1_0, nil,
|
|
57
|
+
mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
|
|
58
|
+
)
|
|
59
|
+
wrapper = Moxml::Document.new(native_doc, Moxml::Context.new(:leptris))
|
|
60
|
+
reference = Moxml::C14n::Inclusive10.new.canonicalize(wrapper.root)
|
|
61
|
+
native == reference
|
|
62
|
+
rescue StandardError
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Bumped whenever a document's :entity_markers flag is written
|
|
68
|
+
# (parse, parse_html, entity-reference mint) so wrapper-level
|
|
69
|
+
# entity_bearing? memos invalidate.
|
|
70
|
+
def self.serialize_generation
|
|
71
|
+
@serialize_generation ||= 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.bump_serialize_generation
|
|
75
|
+
@serialize_generation = serialize_generation + 1
|
|
76
|
+
end
|
|
77
|
+
|
|
26
78
|
# leptris-ruby#103: prefixed attribute tests inside predicates
|
|
27
79
|
# stopped resolving through the document's in-scope declarations
|
|
28
80
|
# on released 1.9.37–1.9.39; 1.9.40 (engine 1.9.14+) restored
|
|
@@ -108,6 +160,39 @@ module Moxml
|
|
|
108
160
|
doc.root = element
|
|
109
161
|
end
|
|
110
162
|
|
|
163
|
+
def bare_set_qname_safe?
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def bare_get_qname_safe?
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Expanded-name (URI + local) attribute VALUE lookup on the
|
|
172
|
+
# engine — the same match rule as the resolver (xmlns
|
|
173
|
+
# declarations invisible, no-namespace never matches a
|
|
174
|
+
# prefixed name), without materializing the attribute list.
|
|
175
|
+
def expanded_attr_value(element, uri, local)
|
|
176
|
+
element.attribute_ns(uri, local)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Fast bare-name read: the binding call plus marker
|
|
180
|
+
# restoration when the document carries them (entity-free
|
|
181
|
+
# documents — the common case — skip the scan; parentless
|
|
182
|
+
# iterparse elements are never bearing).
|
|
183
|
+
def bare_attr_value(element, name)
|
|
184
|
+
value = element[name.to_s]
|
|
185
|
+
if value.is_a?(String) && entity_bearing?(element)
|
|
186
|
+
restore_entities(value)
|
|
187
|
+
else
|
|
188
|
+
value
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def native_identity_stable?
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
111
196
|
def parse(xml, options = {}, _context = nil)
|
|
112
197
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
113
198
|
# The marker flag rides preprocess's own `&` scan — no
|
|
@@ -153,11 +238,59 @@ module Moxml
|
|
|
153
238
|
|
|
154
239
|
record_source_declaration(native_doc, processed)
|
|
155
240
|
attachments.set(native_doc, :entity_markers, entity_markers)
|
|
241
|
+
bump_serialize_generation
|
|
156
242
|
attachments.set(native_doc, :parse_errors, recover_errors) if recover_errors
|
|
157
243
|
|
|
158
244
|
doc
|
|
159
245
|
end
|
|
160
246
|
|
|
247
|
+
# Tolerant HTML4/5 parsing into the standard DOM (engine
|
|
248
|
+
# leptris/leptris#659): implied end tags, void elements,
|
|
249
|
+
# raw-text script/style, case-insensitive lowercased names,
|
|
250
|
+
# the HTML named-entity table, synthesized html/head/body.
|
|
251
|
+
# The engine decodes HTML entities directly into text — no
|
|
252
|
+
# marker pipeline, so the marker split scan is stood down.
|
|
253
|
+
# Incremental parse (libleptris v1.6/#586): yields completed
|
|
254
|
+
# elements as the parse runs; each prior subtree is released.
|
|
255
|
+
# Yielded elements are parentless (document nil) and valid
|
|
256
|
+
# only inside the block — the wrapper mirrors that lifetime.
|
|
257
|
+
def iterparse(xml, mode = :top_level, _context = nil, &block)
|
|
258
|
+
raise ArgumentError, "iterparse requires a block" unless block
|
|
259
|
+
|
|
260
|
+
ctx = _context || Context.new(:leptris)
|
|
261
|
+
::Leptris::XML::Iterparse.parse(xml, mode: mode) do |element|
|
|
262
|
+
yield(Node.wrap(element, ctx))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def iterparse_file(path, mode = :top_level, _context = nil, &block)
|
|
267
|
+
raise ArgumentError, "iterparse_file requires a block" unless block
|
|
268
|
+
|
|
269
|
+
ctx = _context || Context.new(:leptris)
|
|
270
|
+
::Leptris::XML::Iterparse.parse_file(path, mode: mode) do |element|
|
|
271
|
+
yield(Node.wrap(element, ctx))
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def parse_html(html, _options = {}, _context = nil)
|
|
276
|
+
unless HTML_PARSE_SUPPORTED
|
|
277
|
+
raise Moxml::AdapterError.new(
|
|
278
|
+
"HTML parsing requires leptris >= 1.9.80 (have #{::Leptris::VERSION})",
|
|
279
|
+
adapter: name, operation: "parse_html",
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
|
|
284
|
+
native_doc = begin
|
|
285
|
+
::Leptris::XML.parse_html(html_string)
|
|
286
|
+
rescue ::Leptris::XML::ParseError => e
|
|
287
|
+
raise Moxml::ParseError.new(e.message)
|
|
288
|
+
end
|
|
289
|
+
attachments.set(native_doc, :entity_markers, false)
|
|
290
|
+
bump_serialize_generation
|
|
291
|
+
Document.new(native_doc, _context || Context.new(:leptris))
|
|
292
|
+
end
|
|
293
|
+
|
|
161
294
|
# nil when no parse flag is requested — the binding treats a
|
|
162
295
|
# nil options hash as plain defaults (matching libxml2/
|
|
163
296
|
# Nokogiri semantics: blanks kept, no ATTLIST defaults).
|
|
@@ -393,7 +526,7 @@ module Moxml
|
|
|
393
526
|
when ::Leptris::XML::Element then :element
|
|
394
527
|
when ::Leptris::XML::CDATA then :cdata
|
|
395
528
|
when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
|
|
396
|
-
when ::Leptris::XML::Attr then :attribute
|
|
529
|
+
when ::Leptris::XML::Attr, ::Leptris::XML::ResultAttr then :attribute
|
|
397
530
|
when ::Leptris::XML::Comment then :comment
|
|
398
531
|
when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then :processing_instruction
|
|
399
532
|
when ::Leptris::XML::Document then :document
|
|
@@ -455,7 +588,8 @@ module Moxml
|
|
|
455
588
|
# dominates cold children cost. Cross-document moves of
|
|
456
589
|
# marker-bearing text into an entity-free document degrade
|
|
457
590
|
# to literal text.
|
|
458
|
-
return natives if
|
|
591
|
+
return natives if node.document.nil? ||
|
|
592
|
+
attachments.get(node.document, :entity_markers) == false
|
|
459
593
|
|
|
460
594
|
split_entity_markers(natives, node)
|
|
461
595
|
end
|
|
@@ -561,6 +695,7 @@ module Moxml
|
|
|
561
695
|
marker = parent.document.create_text_node("#{Entity::MARKER}#{child.name};")
|
|
562
696
|
parent.add_child(marker)
|
|
563
697
|
attachments.set(parent.document, :entity_markers, true)
|
|
698
|
+
bump_serialize_generation
|
|
564
699
|
return child
|
|
565
700
|
end
|
|
566
701
|
child = parent.document.create_text_node(child) if child.is_a?(String)
|
|
@@ -772,8 +907,10 @@ module Moxml
|
|
|
772
907
|
end
|
|
773
908
|
case result
|
|
774
909
|
when ::Leptris::XML::NodeSet
|
|
775
|
-
|
|
776
|
-
|
|
910
|
+
# The binding's #to_a mints one Ruby wrapper per result
|
|
911
|
+
# node; hand the native set through so .size/.first stay
|
|
912
|
+
# native-side (LazyNodeSet holds it unmaterialized).
|
|
913
|
+
first_only ? result.first : result.extend(Moxml::LazyNodeSet)
|
|
777
914
|
else
|
|
778
915
|
result
|
|
779
916
|
end
|
|
@@ -785,6 +922,10 @@ module Moxml
|
|
|
785
922
|
end
|
|
786
923
|
|
|
787
924
|
def native_context_node?(node)
|
|
925
|
+
# Parentless elements (iterparse yields) have no document
|
|
926
|
+
# handle for the compiled eval — the Ruby engine owns them.
|
|
927
|
+
return false if node.is_a?(::Leptris::XML::Element) && node.document.nil?
|
|
928
|
+
|
|
788
929
|
node.is_a?(::Leptris::XML::Document) ||
|
|
789
930
|
node.is_a?(::Leptris::XML::Element)
|
|
790
931
|
end
|
|
@@ -815,6 +956,12 @@ module Moxml
|
|
|
815
956
|
next false if uses_xmlns_prefix?(ast)
|
|
816
957
|
next false if !PREFIXED_ATTR_PREDICATES_NATIVE && prefixed_attribute_test?(ast)
|
|
817
958
|
|
|
959
|
+
# Attribute-node results are native since 1.9.105
|
|
960
|
+
# (leptris-ruby#153: ResultAttr wrappers with name/value);
|
|
961
|
+
# older bindings returned generic Node wrappers whose
|
|
962
|
+
# #name raised, so the Ruby engine owned them.
|
|
963
|
+
next true if selects_attribute_results?(ast) && ATTR_RESULT_NATIVE
|
|
964
|
+
|
|
818
965
|
!selects_attribute_results?(ast)
|
|
819
966
|
end
|
|
820
967
|
rescue XPath::SyntaxError
|