moxml 0.5.30 → 0.5.32
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 +68 -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/document_parts.rb +8 -0
- data/lib/moxml/adapter/leptris/markers.rb +4 -1
- data/lib/moxml/adapter/leptris/serialize.rb +71 -19
- 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 +247 -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: 627111c496efca4687eabeeb78989d611d3d308adc5ed32443c7058da51c57c9
|
|
4
|
+
data.tar.gz: 86d1cc6c471d07084e0d2e0de9412b286b5486b202f9fe677b7417f514c1fe55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1108c98dc8af92913605dd08b84eb50bbfec92a5dd286e15656c3ecd57937ce56bc186c9393a740f6bd8a8316f06fb059db226f20ffc9f0b84cfb433e1a532d
|
|
7
|
+
data.tar.gz: 02d109a32659b30fc34ac5d6d3f01bc0d32d9e1ca6a38cfdc2fe3880a02c779ce5ff41fa2f0eda04adc2a9ccd6702f372d29b4d2a6fe33faeb0456cb8852d26b
|
|
@@ -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,74 @@ 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
|
+
Since 1.9.118/1.9.121 the engine also handles WHATWG foreign
|
|
82
|
+
content — embedded SVG and MathML in HTML parse into the tree
|
|
83
|
+
(preserving `foreignObject` subtrees that Nokogiri drops), the
|
|
84
|
+
source HTML doctype round-trips, and `template`/`colgroup` place
|
|
85
|
+
correctly. Foreign-content camelCase names are currently
|
|
86
|
+
lowercased (engine conformance note leptris/leptris#1013).
|
|
87
|
+
|
|
88
|
+
Adapters without an engine HTML mode (Ox, Oga, REXML, LibXML) raise
|
|
89
|
+
`Moxml::AdapterError`; Nokogiri parses through libxml2's HTML mode.
|
|
90
|
+
|
|
91
|
+
== Streaming iteration — `iterparse`
|
|
92
|
+
|
|
93
|
+
`Context#iterparse(xml, mode:)` / `iterparse_file(path, mode:)`
|
|
94
|
+
expose the engine's incremental parser (libleptris v1.6, #586):
|
|
95
|
+
completed elements yield while parsing, prior subtrees release, and
|
|
96
|
+
memory stays bounded by the largest subtree — `iterparse_file`
|
|
97
|
+
streams the file C-side. `:top_level` yields the root's children;
|
|
98
|
+
`:full_document` yields every element in completion (post-order)
|
|
99
|
+
order.
|
|
100
|
+
|
|
101
|
+
Measured on a 21MB / 108k-element file: **+20MB peak RSS** (full
|
|
102
|
+
parse: +169MB; Nokogiri Reader: +62MB), bare iteration ~97ms on
|
|
103
|
+
leptris >= 1.9.105 (Nokogiri Reader: 179ms) — ~1.8x faster at a
|
|
104
|
+
third of the memory. Attribute reads on yielded elements improved
|
|
105
|
+
~16x on current builds (the parentless-read cost filed in
|
|
106
|
+
leptris-ruby#152 is fixed).
|
|
107
|
+
|
|
108
|
+
The yielded elements are parentless (`#document` nil) and valid
|
|
109
|
+
only inside the block — the subtree is released when the iterator
|
|
110
|
+
advances. Since binding 1.9.105, access after the iteration raises
|
|
111
|
+
`UseAfterFreeError` instead of crashing (leptris-ruby#152). Adapters without an engine iterator raise
|
|
112
|
+
`Moxml::AdapterError`.
|
|
113
|
+
|
|
114
|
+
[source,ruby]
|
|
115
|
+
----
|
|
116
|
+
Moxml.new(:leptris).iterparse_file("huge.xml") do |record|
|
|
117
|
+
process(record.at_attrs, record.children.map(&:text))
|
|
118
|
+
end
|
|
119
|
+
----
|
|
120
|
+
|
|
121
|
+
== Lifecycle
|
|
54
122
|
== Lifecycle
|
|
55
123
|
|
|
56
124
|
`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
|
|
@@ -64,6 +64,14 @@ module Moxml
|
|
|
64
64
|
texts << child
|
|
65
65
|
attachments.set(doc, :document_text, texts)
|
|
66
66
|
child
|
|
67
|
+
when ::Leptris::XML::Comment
|
|
68
|
+
# The tree model supports document comments (they parse
|
|
69
|
+
# and serialize, libleptris 1.9.3 #578) but the engine
|
|
70
|
+
# has no add entry yet (leptris/leptris#1032).
|
|
71
|
+
raise Moxml::NotImplementedError.new(
|
|
72
|
+
"Adding document-level comments requires an engine entry (leptris/leptris#1032)",
|
|
73
|
+
feature: "add_document_child", adapter: "Leptris",
|
|
74
|
+
)
|
|
67
75
|
else
|
|
68
76
|
raise Moxml::DocumentStructureError.new(
|
|
69
77
|
"Unsupported document child: #{child.class}",
|
|
@@ -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
|
|
|
@@ -33,13 +33,53 @@ module Moxml
|
|
|
33
33
|
# Entity restoration belongs to the wrapper layer
|
|
34
34
|
# (Node#to_xml runs adapter.restore_entities for every
|
|
35
35
|
# adapter); doing it here scanned the output a second time.
|
|
36
|
-
xml =
|
|
36
|
+
xml = if native_expand?(node, options)
|
|
37
|
+
opts = options.dup
|
|
38
|
+
opts[:__expand_handled_natively] = true
|
|
39
|
+
normalize_serialization(raw_serialize(node, opts), opts)
|
|
40
|
+
else
|
|
41
|
+
normalize_serialization(raw_serialize(node, options), options)
|
|
42
|
+
end
|
|
37
43
|
# The binding's FFI strings come back binary-tagged; the
|
|
38
44
|
# engine encoded the bytes per this option, so tag them.
|
|
39
45
|
xml.force_encoding(options[:encoding]) if options[:encoding]
|
|
40
46
|
xml
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
# Element-face trailing-newline strip: engine fix landed in
|
|
50
|
+
# 1.9.42; armed only on older floor bindings.
|
|
51
|
+
TRAILING_NL_STRIP_ACTIVE =
|
|
52
|
+
Gem::Version.new(::Leptris::VERSION) < Gem::Version.new("1.9.42")
|
|
53
|
+
|
|
54
|
+
# The binding's element face with all-default options — the
|
|
55
|
+
# frozen splat keeps the hot argless path allocation-free.
|
|
56
|
+
# Native expand-empty (engine #882, libleptris 1.9.95,
|
|
57
|
+
# bindings 1.9.144): empty elements emit <a></a> through a
|
|
58
|
+
# C-side ext entry — the Ruby full-output regex rewrite and
|
|
59
|
+
# its "/>" probe scan drop out of the element path. The
|
|
60
|
+
# document face does not expose the option yet; documents
|
|
61
|
+
# keep the Ruby pass.
|
|
62
|
+
EXPAND_EMPTY_NATIVE =
|
|
63
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.144")
|
|
64
|
+
|
|
65
|
+
ELEMENT_DEFAULT_KWARGS = { indent: 0, no_decl: true, encoding: nil }.freeze
|
|
66
|
+
ELEMENT_EXPAND_KWARGS =
|
|
67
|
+
if EXPAND_EMPTY_NATIVE
|
|
68
|
+
{ indent: 0, no_decl: true, encoding: nil, expand_empty: true }.freeze
|
|
69
|
+
else
|
|
70
|
+
ELEMENT_DEFAULT_KWARGS
|
|
71
|
+
end.freeze
|
|
72
|
+
|
|
73
|
+
# Elements (not documents) with expand_empty and NO
|
|
74
|
+
# indent-unit string: the C ext entry handles expansion. The
|
|
75
|
+
# element unit serializer does not take the flag — those keep
|
|
76
|
+
# the Ruby pass.
|
|
77
|
+
def native_expand?(node, options)
|
|
78
|
+
EXPAND_EMPTY_NATIVE && options[:expand_empty] &&
|
|
79
|
+
!node.is_a?(::Leptris::XML::Document) &&
|
|
80
|
+
!options[:indent_text].is_a?(String)
|
|
81
|
+
end
|
|
82
|
+
|
|
43
83
|
def raw_serialize(node, options)
|
|
44
84
|
# CDATA must precede Text in this chain: CDATA < Text in the
|
|
45
85
|
# binding, so a Text branch first would swallow CDATA nodes.
|
|
@@ -60,23 +100,33 @@ module Moxml
|
|
|
60
100
|
return serialize_document(node, options)
|
|
61
101
|
end
|
|
62
102
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
xml =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
# Element serialization never emits a declaration — the C
|
|
104
|
+
# element serializer ignores the flag (verified
|
|
105
|
+
# byte-identical); skipping the declaration fetch avoids a
|
|
106
|
+
# document attachment walk per element serialize. Likewise
|
|
107
|
+
# an unset encoding serializes UTF-8 — byte-identical to an
|
|
108
|
+
# explicit "UTF-8" — and the binding then reuses its shared
|
|
109
|
+
# DEFAULT_OPTIONS instead of rebuilding an options struct
|
|
110
|
+
# per call; the wrapper force-tags the string either way.
|
|
111
|
+
indent = options.fetch(:indent, 0)
|
|
112
|
+
encoding = options[:encoding] == "UTF-8" ? nil : options[:encoding]
|
|
113
|
+
native_expand = native_expand?(node, options)
|
|
114
|
+
xml = if indent.zero? && encoding.nil?
|
|
115
|
+
node.to_xml(**(native_expand ? ELEMENT_EXPAND_KWARGS : ELEMENT_DEFAULT_KWARGS))
|
|
116
|
+
else
|
|
117
|
+
kwargs = { indent: indent, no_decl: true, encoding: encoding }
|
|
118
|
+
kwargs[:expand_empty] = true if native_expand
|
|
119
|
+
if INDENT_UNIT_SUPPORTED && options[:indent_text].is_a?(String)
|
|
120
|
+
kwargs[:indent_text] = options[:indent_text]
|
|
121
|
+
end
|
|
122
|
+
node.to_xml(**kwargs)
|
|
123
|
+
end
|
|
124
|
+
# Element output always ends with the close tag — but older
|
|
125
|
+
# engines append a stray trailing newline when the element's
|
|
126
|
+
# last text child is non-ASCII (fixed engine side in
|
|
127
|
+
# 1.9.42). The strip stays armed only below that version —
|
|
128
|
+
# a regex sub per element serialize is measurable in bulk.
|
|
129
|
+
TRAILING_NL_STRIP_ACTIVE ? xml.sub(/\n+\z/, "") : xml
|
|
80
130
|
end
|
|
81
131
|
|
|
82
132
|
# A bare ampersand — not starting a named or numeric entity
|
|
@@ -102,7 +152,9 @@ module Moxml
|
|
|
102
152
|
# The libxml2-layout serializer (>= 1.9.42) keeps attribute
|
|
103
153
|
# apostrophes literal; older engines escaped them.
|
|
104
154
|
needs_apos = !LIBXML2_LAYOUT_PARITY && xml.include?("'")
|
|
105
|
-
needs_expand = options[:expand_empty] &&
|
|
155
|
+
needs_expand = options[:expand_empty] &&
|
|
156
|
+
!options[:__expand_handled_natively] &&
|
|
157
|
+
xml.include?("/>")
|
|
106
158
|
# Corruption guards: the 1-char ampersand probe is ~1µs
|
|
107
159
|
# (memchr-class); the raw-< scan runs only on builds that
|
|
108
160
|
# still carry the parse race (leptris-ruby#131).
|