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
data/lib/moxml/node_set.rb
CHANGED
|
@@ -6,10 +6,14 @@ module Moxml
|
|
|
6
6
|
|
|
7
7
|
attr_reader :context
|
|
8
8
|
|
|
9
|
+
# nodes: Array of natives, or an adapter's LazyNodeSet — the
|
|
10
|
+
# native set is held unmaterialized until an operation needs an
|
|
11
|
+
# Array (#+, #<<, #delete, Range slices).
|
|
9
12
|
def initialize(nodes, context, parent_node = nil)
|
|
10
|
-
@nodes = Array
|
|
13
|
+
@nodes = nodes.is_a?(Array) ? nodes : nil
|
|
14
|
+
@lazy = @nodes ? nil : nodes
|
|
11
15
|
@context = context
|
|
12
|
-
@wrapped =
|
|
16
|
+
@wrapped = nil
|
|
13
17
|
@parent_node = parent_node
|
|
14
18
|
end
|
|
15
19
|
|
|
@@ -17,15 +21,17 @@ module Moxml
|
|
|
17
21
|
# The public surface is wrapper-only: every Enumerable access
|
|
18
22
|
# goes through #each/#[]/#to_a, which wrap.
|
|
19
23
|
def native_nodes
|
|
20
|
-
@nodes
|
|
24
|
+
return @nodes unless @nodes.nil?
|
|
25
|
+
|
|
26
|
+
@nodes = @lazy.to_a
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def each
|
|
24
30
|
return to_enum(:each) unless block_given?
|
|
25
31
|
|
|
26
|
-
wrapped =
|
|
32
|
+
wrapped = wrapped_buffer
|
|
27
33
|
index = 0
|
|
28
|
-
@nodes.each do |node|
|
|
34
|
+
(@nodes || @lazy).each do |node|
|
|
29
35
|
wrapper = wrapped[index]
|
|
30
36
|
unless wrapper
|
|
31
37
|
wrapper = wrap_with_parent(node)
|
|
@@ -40,51 +46,56 @@ module Moxml
|
|
|
40
46
|
def [](index)
|
|
41
47
|
case index
|
|
42
48
|
when Integer
|
|
43
|
-
actual = index.negative? ?
|
|
44
|
-
return nil unless actual >= 0 && actual <
|
|
49
|
+
actual = index.negative? ? native_size + index : index
|
|
50
|
+
return nil unless actual >= 0 && actual < native_size
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
wrapped_buffer[actual] ||= wrap_with_parent((@nodes || @lazy)[actual])
|
|
47
53
|
when Range
|
|
48
|
-
self.class.new(
|
|
54
|
+
self.class.new(native_nodes[index], @context)
|
|
49
55
|
end
|
|
50
56
|
end
|
|
51
57
|
|
|
52
58
|
def first(n = nil)
|
|
53
59
|
if n.nil?
|
|
54
|
-
|
|
60
|
+
native_size.zero? ? nil : self[0]
|
|
55
61
|
else
|
|
56
62
|
n.times.filter_map { |i| self[i] }
|
|
57
63
|
end
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def last
|
|
61
|
-
|
|
67
|
+
native_size.zero? ? nil : self[native_size - 1]
|
|
62
68
|
end
|
|
63
69
|
|
|
64
70
|
def empty?
|
|
65
|
-
|
|
71
|
+
native_size.zero?
|
|
66
72
|
end
|
|
67
73
|
|
|
68
74
|
def size
|
|
69
|
-
|
|
75
|
+
native_size
|
|
70
76
|
end
|
|
71
77
|
alias length size
|
|
72
78
|
|
|
73
79
|
def to_a
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
i = 0
|
|
81
|
+
wrapped = wrapped_buffer
|
|
82
|
+
(@nodes || @lazy).each do |node|
|
|
83
|
+
wrapped[i] ||= wrap_with_parent(node)
|
|
84
|
+
i += 1
|
|
76
85
|
end
|
|
77
|
-
|
|
86
|
+
wrapped.compact
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
def +(other)
|
|
81
|
-
self.class.new(
|
|
90
|
+
self.class.new(native_nodes + other.native_nodes, @context, @parent_node)
|
|
82
91
|
end
|
|
83
92
|
|
|
84
93
|
def <<(node)
|
|
85
94
|
native_node = node.is_a?(Node) ? node.native : node
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
# Materialize the buffer first so it allocates at the pre-append
|
|
96
|
+
# size and stays in lockstep with the natives.
|
|
97
|
+
wrapped_buffer << nil
|
|
98
|
+
native_nodes << native_node
|
|
88
99
|
self
|
|
89
100
|
end
|
|
90
101
|
alias push <<
|
|
@@ -94,7 +105,7 @@ module Moxml
|
|
|
94
105
|
# which may yield the same native node multiple times
|
|
95
106
|
def uniq_by_native
|
|
96
107
|
seen = {}
|
|
97
|
-
unique_natives =
|
|
108
|
+
unique_natives = native_nodes.select do |native|
|
|
98
109
|
id = native.object_id
|
|
99
110
|
if seen[id]
|
|
100
111
|
false
|
|
@@ -109,7 +120,7 @@ module Moxml
|
|
|
109
120
|
def ==(other)
|
|
110
121
|
self.class == other.class &&
|
|
111
122
|
length == other.length &&
|
|
112
|
-
|
|
123
|
+
native_nodes.each_with_index.all? do |_node, index|
|
|
113
124
|
self[index] == other[index]
|
|
114
125
|
end
|
|
115
126
|
end
|
|
@@ -127,18 +138,30 @@ module Moxml
|
|
|
127
138
|
# Accepts both wrapped Moxml nodes and native nodes
|
|
128
139
|
def delete(node)
|
|
129
140
|
native_node = node.is_a?(Node) ? node.native : node
|
|
130
|
-
idx =
|
|
141
|
+
idx = native_nodes.index(native_node)
|
|
131
142
|
if idx
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
native_nodes.delete_at(idx)
|
|
144
|
+
wrapped_buffer.delete_at(idx) if @wrapped
|
|
134
145
|
else
|
|
135
|
-
|
|
146
|
+
native_nodes.delete(native_node)
|
|
136
147
|
end
|
|
137
148
|
self
|
|
138
149
|
end
|
|
139
150
|
|
|
140
151
|
private
|
|
141
152
|
|
|
153
|
+
# Allocated on first wrapped access — .size/.empty? consumers of
|
|
154
|
+
# large result sets never pay for the slot array.
|
|
155
|
+
def wrapped_buffer
|
|
156
|
+
return @wrapped unless @wrapped.nil?
|
|
157
|
+
|
|
158
|
+
@wrapped = Array.new(native_size)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def native_size
|
|
162
|
+
@nodes ? @nodes.size : @lazy.length
|
|
163
|
+
end
|
|
164
|
+
|
|
142
165
|
def wrap_with_parent(native_node)
|
|
143
166
|
wrapped = Moxml::Node.wrap(native_node, @context)
|
|
144
167
|
if @parent_node && wrapped
|
|
@@ -139,24 +139,28 @@ module Moxml
|
|
|
139
139
|
@handlers[:start_document]&.call
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
# The per-event hash lookup fires for every node in the
|
|
143
|
+
# document; the DSL is fixed after construction, so the
|
|
144
|
+
# resolved block memoizes into an ivar on first fire (nil
|
|
145
|
+
# short-circuits the same way &. does).
|
|
142
146
|
# @private
|
|
143
147
|
def on_end_document
|
|
144
|
-
@handlers[:end_document]&.call
|
|
148
|
+
(@end_document_block ||= @handlers[:end_document])&.call
|
|
145
149
|
end
|
|
146
150
|
|
|
147
151
|
# @private
|
|
148
152
|
def on_start_element(name, attributes = {}, namespaces = {})
|
|
149
|
-
@handlers[:start_element]&.call(name, attributes, namespaces)
|
|
153
|
+
(@start_element_block ||= @handlers[:start_element])&.call(name, attributes, namespaces)
|
|
150
154
|
end
|
|
151
155
|
|
|
152
156
|
# @private
|
|
153
157
|
def on_end_element(name)
|
|
154
|
-
@handlers[:end_element]&.call(name)
|
|
158
|
+
(@end_element_block ||= @handlers[:end_element])&.call(name)
|
|
155
159
|
end
|
|
156
160
|
|
|
157
161
|
# @private
|
|
158
162
|
def on_characters(text)
|
|
159
|
-
@handlers[:characters]&.call(text)
|
|
163
|
+
(@characters_block ||= @handlers[:characters])&.call(text)
|
|
160
164
|
end
|
|
161
165
|
|
|
162
166
|
# @private
|
|
@@ -12,22 +12,31 @@ module Moxml
|
|
|
12
12
|
# @yieldparam value [Object] raw attribute/namespace value
|
|
13
13
|
# @yieldreturn [Object] transformed value to store
|
|
14
14
|
# @return [Array(Hash, Hash)] [regular_attrs, namespaces]
|
|
15
|
+
# Shared for the overwhelmingly common no-declaration element:
|
|
16
|
+
# one Hash allocation per start_element event saved. Frozen —
|
|
17
|
+
# event hashes are read-only data, not scratch.
|
|
18
|
+
EMPTY_NAMESPACES = {}.freeze
|
|
19
|
+
|
|
15
20
|
def split_attributes_and_namespaces(attributes)
|
|
16
21
|
attrs = {}
|
|
17
|
-
ns =
|
|
22
|
+
ns = nil
|
|
18
23
|
|
|
19
24
|
each_attribute(attributes) do |name, value|
|
|
20
25
|
name_s = name.to_s
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
if name_s.start_with?("xmlns")
|
|
27
|
+
if name_s == "xmlns"
|
|
28
|
+
(ns ||= {})[nil] = block_given? ? yield(value) : value
|
|
29
|
+
elsif name_s.bytesize > 5 && name_s.getbyte(5) == 58 # ":"
|
|
30
|
+
(ns ||= {})[name_s[6..]] = block_given? ? yield(value) : value
|
|
31
|
+
else
|
|
32
|
+
attrs[name_s] = block_given? ? yield(value) : value
|
|
33
|
+
end
|
|
25
34
|
else
|
|
26
|
-
attrs[name_s] =
|
|
35
|
+
attrs[name_s] = block_given? ? yield(value) : value
|
|
27
36
|
end
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
[attrs, ns]
|
|
39
|
+
[attrs, ns || EMPTY_NAMESPACES]
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
private
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xml_utils.rb
CHANGED
|
@@ -42,8 +42,19 @@ module Moxml
|
|
|
42
42
|
raise ValidationError, "XML comment cannot contain double hyphens (--)"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Real documents reuse a handful of distinct element names; the
|
|
46
|
+
# memo turns the per-create regex into a hash probe after the
|
|
47
|
+
# first sight of a name. Capped — hostile vocabularies fall back
|
|
48
|
+
# to the regex.
|
|
49
|
+
VALID_ELEMENT_NAMES = {}.compare_by_identity
|
|
50
|
+
|
|
45
51
|
def validate_element_name(name)
|
|
46
|
-
return if name.is_a?(String) &&
|
|
52
|
+
return if name.is_a?(String) && VALID_ELEMENT_NAMES.key?(name)
|
|
53
|
+
|
|
54
|
+
if name.is_a?(String) && name.match?(/^[a-zA-Z_][\w\-.:]*$/)
|
|
55
|
+
VALID_ELEMENT_NAMES[name] = true if VALID_ELEMENT_NAMES.size < 1024
|
|
56
|
+
return
|
|
57
|
+
end
|
|
47
58
|
|
|
48
59
|
raise ValidationError, "Invalid XML element name: #{name}"
|
|
49
60
|
end
|
data/lib/moxml.rb
CHANGED
|
@@ -68,6 +68,7 @@ module Moxml
|
|
|
68
68
|
autoload :Context, "moxml/context"
|
|
69
69
|
autoload :Node, "moxml/node"
|
|
70
70
|
autoload :NodeSet, "moxml/node_set"
|
|
71
|
+
autoload :LazyNodeSet, "moxml/lazy_node_set"
|
|
71
72
|
autoload :Document, "moxml/document"
|
|
72
73
|
autoload :Element, "moxml/element"
|
|
73
74
|
autoload :Text, "moxml/text"
|
|
@@ -52,10 +52,15 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
52
52
|
expect(doc.xpath("//item[@p:kind='a']").map { |n| n["id"] }).to eq(["1"])
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
it "
|
|
55
|
+
it "evaluates attribute-node queries with proper wrappers" do
|
|
56
|
+
# Native since 1.9.105 (leptris-ruby#153: ResultAttr with
|
|
57
|
+
# name/value); earlier bindings returned generic nodes whose
|
|
58
|
+
# #name raised, so the Ruby engine owned them.
|
|
59
|
+
skip "requires native attr results (leptris 1.9.105+)" unless described_class::ATTR_RESULT_NATIVE
|
|
56
60
|
attrs = doc.xpath("//item/@id")
|
|
57
61
|
expect(attrs.map(&:name)).to eq(%w[id id])
|
|
58
62
|
expect(attrs.map(&:value)).to eq(%w[1 2])
|
|
63
|
+
expect(attrs.first).to be_a(Moxml::Attribute)
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
it "evaluates element-context queries on the native engine" do
|
|
@@ -314,6 +319,216 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
314
319
|
end
|
|
315
320
|
end
|
|
316
321
|
|
|
322
|
+
describe "HTML parsing (leptris/leptris#659)" do
|
|
323
|
+
before do
|
|
324
|
+
skip "requires leptris 1.9.80+ (HTML engine mode)" unless described_class::HTML_PARSE_SUPPORTED
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
328
|
+
|
|
329
|
+
it "synthesizes the html/body structure with lowercased names" do
|
|
330
|
+
doc = ctx.parse_html(%(<DIV CLASS="x">t</DIV>))
|
|
331
|
+
expect(doc.root.name).to eq("html")
|
|
332
|
+
body = doc.root.children.find(&:element?)
|
|
333
|
+
expect(body.name).to eq("body")
|
|
334
|
+
div = body.at_xpath(".//div")
|
|
335
|
+
expect(div["class"]).to eq("x")
|
|
336
|
+
expect(div.text).to eq("t")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "implies end tags for list items" do
|
|
340
|
+
doc = ctx.parse_html(%(<ul><li>one<li>two</ul>))
|
|
341
|
+
expect(doc.xpath("//li").map(&:text)).to eq(%w[one two])
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it "keeps void elements as empty elements" do
|
|
345
|
+
doc = ctx.parse_html(%(<br><img src="x.png">))
|
|
346
|
+
expect(doc.xpath("//br").size).to eq(1)
|
|
347
|
+
expect(doc.xpath("//img").first["src"]).to eq("x.png")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it "decodes HTML named entities into text" do
|
|
351
|
+
doc = ctx.parse_html(%(<p>café ©</p>))
|
|
352
|
+
expect(doc.at_xpath("//p").text).to eq("caf\u00e9 \u00a0\u00a9")
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "materializes boolean attributes" do
|
|
356
|
+
doc = ctx.parse_html(%(<a href="/x" disabled>t</a>))
|
|
357
|
+
link = doc.at_xpath("//a")
|
|
358
|
+
expect(link["href"]).to eq("/x")
|
|
359
|
+
expect(link["disabled"]).to eq("")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it "reads script content as raw text" do
|
|
363
|
+
doc = ctx.parse_html(%(<script>if (a < b) { x("</div>"); }</script>))
|
|
364
|
+
expect(doc.at_xpath("//script").text).to include("a < b")
|
|
365
|
+
expect(doc.at_xpath("//script").text).to include("</div>")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it "serializes to well-formed XML that reparses strictly" do
|
|
369
|
+
doc = ctx.parse_html(%(<p>a & b < c</p><script>y = "</q>";</script>))
|
|
370
|
+
out = doc.to_xml
|
|
371
|
+
expect(out).to include("<")
|
|
372
|
+
reparsed = Nokogiri::XML(out, &:strict)
|
|
373
|
+
expect(reparsed).to be_a(Nokogiri::XML::Document)
|
|
374
|
+
expect(reparsed.errors).to be_empty
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it "round-trips a parsed tree through mutation" do
|
|
378
|
+
doc = ctx.parse_html(%(<ul><li>a<li>b</ul>))
|
|
379
|
+
doc.at_xpath("//ul").add_child(doc.create_element("li"))
|
|
380
|
+
expect(doc.xpath("//li").map(&:name)).to eq(%w[li li li])
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe "iterparse streaming" do
|
|
385
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
386
|
+
let(:xml) do
|
|
387
|
+
%(<catalog>#{Array.new(3) { |i| %(<record id="r#{i}"><field name="f">v#{i} & x</field></record>) }.join}</catalog>)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
it "yields completed top-level children with readable attributes" do
|
|
391
|
+
seen = []
|
|
392
|
+
ctx.iterparse(xml) { |e| seen << [e.name, e["id"]] }
|
|
393
|
+
expect(seen).to eq([["record", "r0"], ["record", "r1"], ["record", "r2"]])
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it "full_document yields every element in completion order" do
|
|
397
|
+
seen = []
|
|
398
|
+
ctx.iterparse(xml, mode: :full_document) { |e| seen << e.name }
|
|
399
|
+
expect(seen).to eq(%w[field record field record field record catalog])
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it "reads children, text, and serializes inside the block" do
|
|
403
|
+
outs = []
|
|
404
|
+
ctx.iterparse(xml) do |e|
|
|
405
|
+
outs << [e.children.first["name"], e.children.first.text,
|
|
406
|
+
Nokogiri::XML(e.to_xml, &:strict).root["id"]]
|
|
407
|
+
end
|
|
408
|
+
expect(outs.map(&:first)).to all(eq("f"))
|
|
409
|
+
expect(outs.map { |row| row[1] }).to all(match(/v\d & x/))
|
|
410
|
+
expect(outs.map { |row| row[2] }).to eq(%w[r0 r1 r2])
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
it "streams from a file" do
|
|
414
|
+
require "tmpdir"
|
|
415
|
+
Dir.mktmpdir do |dir|
|
|
416
|
+
path = File.join(dir, "doc.xml")
|
|
417
|
+
File.write(path, xml)
|
|
418
|
+
seen = []
|
|
419
|
+
ctx.iterparse_file(path) { |e| seen << e["id"] }
|
|
420
|
+
expect(seen).to eq(%w[r0 r1 r2])
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it "answers subqueries on yielded elements via the Ruby engine" do
|
|
425
|
+
# Parentless elements have no document handle for the compiled
|
|
426
|
+
# native eval — the gate must route them, not crash.
|
|
427
|
+
seen = []
|
|
428
|
+
ctx.iterparse(xml) do |e|
|
|
429
|
+
seen << [e.at_xpath(".//field")["name"], e.xpath("count(.//field)")]
|
|
430
|
+
end
|
|
431
|
+
expect(seen).to eq([["f", 1.0]] * 3)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
it "requires a block" do
|
|
435
|
+
expect { ctx.iterparse(xml) }.to raise_error(ArgumentError, /block/)
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
describe "C14n native delegation" do
|
|
440
|
+
it "matches the Ruby reference byte-for-byte on the default path" do
|
|
441
|
+
# Whether the NATIVE_C14N_BYTE_SAFE probe is armed (fixed
|
|
442
|
+
# engine builds delegate to the C canonicalizer) or not, the
|
|
443
|
+
# default-path output must equal the Ruby reference — this is
|
|
444
|
+
# the safety net that lets the probe auto-adopt future builds.
|
|
445
|
+
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>)
|
|
446
|
+
ctx = Moxml.new(:leptris)
|
|
447
|
+
root = ctx.parse(xml).root
|
|
448
|
+
expect(Moxml::C14n.canonicalize(root))
|
|
449
|
+
.to eq(Moxml::C14n::Inclusive10.new.canonicalize(root))
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
describe "wrapper lifecycle" do
|
|
454
|
+
it "releases wrappers when documents are dropped (WeakMap registry)" do
|
|
455
|
+
# The identity map must hold wrappers weakly: parse-and-drop
|
|
456
|
+
# workloads otherwise pin wrappers, natives, and (via the
|
|
457
|
+
# binding finalizer never running) the C subtrees.
|
|
458
|
+
xml = %(<r>#{Array.new(50) { |i| "<e id=\"i#{i}\">x</e>" }.join}</r>)
|
|
459
|
+
ctx = Moxml.new(:leptris)
|
|
460
|
+
count = -> {
|
|
461
|
+
n = 0
|
|
462
|
+
ObjectSpace.each_object(Moxml::Element) { n += 1 }
|
|
463
|
+
n
|
|
464
|
+
}
|
|
465
|
+
walk = ->(doc) { doc.root.children.to_a }
|
|
466
|
+
|
|
467
|
+
GC.start
|
|
468
|
+
before = count.call
|
|
469
|
+
20.times do
|
|
470
|
+
doc = ctx.parse(xml)
|
|
471
|
+
walk.(doc)
|
|
472
|
+
nil
|
|
473
|
+
end
|
|
474
|
+
5.times { GC.start }
|
|
475
|
+
|
|
476
|
+
# The binding retains a constant one-document wrapper set of
|
|
477
|
+
# its own; moxml must not retain beyond a couple of dropped
|
|
478
|
+
# documents' worth (was: all 20 pinned under the strong map).
|
|
479
|
+
expect(count.call - before).to be < 2 * 51
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
it "keeps wrapper identity while a document is alive" do
|
|
483
|
+
ctx = Moxml.new(:leptris)
|
|
484
|
+
doc = ctx.parse(%(<r><a/></r>))
|
|
485
|
+
expect(doc.root.children.first).to equal(doc.root.children.first)
|
|
486
|
+
GC.start
|
|
487
|
+
expect(doc.root.children.first).to equal(doc.root.children.first)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
describe "lazy xpath result sets" do
|
|
492
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
493
|
+
let(:doc) do
|
|
494
|
+
ctx.parse(%(<r>#{Array.new(50) { |i| "<li>#{i}</li>" }.join}</r>))
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
it "answers size, first, and indexing without materializing" do
|
|
498
|
+
set = doc.xpath("//li")
|
|
499
|
+
expect(set.size).to eq(50)
|
|
500
|
+
expect(set.first.text).to eq("0")
|
|
501
|
+
expect(set[10].text).to eq("10")
|
|
502
|
+
expect(set[-1].text).to eq("49")
|
|
503
|
+
expect(set.empty?).to be(false)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it "enumerates and wraps on demand" do
|
|
507
|
+
expect(doc.xpath("//li").each.to_a.size).to eq(50)
|
|
508
|
+
expect(doc.xpath("//li").to_a.map(&:name).uniq).to eq(%w[li])
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
it "keeps the mutating set operations working" do
|
|
512
|
+
set = doc.xpath("//li")
|
|
513
|
+
expect((set + set).size).to eq(100)
|
|
514
|
+
expect(set.uniq_by_native.size).to eq(50)
|
|
515
|
+
set << doc.create_element("li")
|
|
516
|
+
expect(set.size).to eq(51)
|
|
517
|
+
expect(set.last.name).to eq("li")
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
it "slices ranges" do
|
|
521
|
+
expect(doc.xpath("//li")[0..2].size).to eq(3)
|
|
522
|
+
expect(doc.xpath("//li")[5...8].map(&:text)).to eq(%w[5 6 7])
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
it "returns scalars and at_xpath firsts unwrapped-set" do
|
|
526
|
+
expect(doc.xpath("count(//li)")).to eq(50.0)
|
|
527
|
+
expect(doc.at_xpath("//li").text).to eq("0")
|
|
528
|
+
expect(doc.at_xpath("//nope")).to be_nil
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
317
532
|
describe "DTD ATTLIST defaults" do
|
|
318
533
|
# libleptris 1.9.8: plain parse excludes ATTLIST defaults,
|
|
319
534
|
# matching libxml2/Nokogiri/REXML; dtdattr: true opts in.
|
|
@@ -12,6 +12,23 @@ RSpec.describe Moxml::Adapter::Nokogiri do
|
|
|
12
12
|
|
|
13
13
|
it_behaves_like "xml adapter"
|
|
14
14
|
|
|
15
|
+
describe "HTML parsing" do
|
|
16
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
17
|
+
|
|
18
|
+
it "synthesizes the html/body structure with lowercased names" do
|
|
19
|
+
doc = ctx.parse_html(%(<DIV CLASS="x">t</DIV>))
|
|
20
|
+
expect(doc.root.name).to eq("html")
|
|
21
|
+
body = doc.root.children.find(&:element?)
|
|
22
|
+
expect(body.name).to eq("body")
|
|
23
|
+
expect(body.at_xpath(".//div")["class"]).to eq("x")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "implies end tags and decodes HTML entities" do
|
|
27
|
+
doc = ctx.parse_html(%(<ul><li>café</ul>))
|
|
28
|
+
expect(doc.xpath("//li").map(&:text)).to eq(["caf\u00e9"])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
15
32
|
describe "recover-mode error channel" do
|
|
16
33
|
it "exposes recoverable syntax errors as parse_errors" do
|
|
17
34
|
doc = Moxml.new(:nokogiri).parse("<root><unclosed>", strict: false)
|
data/spec/moxml/context_spec.rb
CHANGED
|
@@ -13,6 +13,19 @@ RSpec.describe Moxml::Context do
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
describe "#parse_html" do
|
|
17
|
+
it "delegates to the adapter's HTML mode" do
|
|
18
|
+
ctx = Moxml.new(:nokogiri)
|
|
19
|
+
doc = ctx.parse_html(%(<p>x</p>))
|
|
20
|
+
expect(doc.root.name).to eq("html")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "raises Moxml::AdapterError on adapters without an HTML mode" do
|
|
24
|
+
ctx = Moxml.new(:ox)
|
|
25
|
+
expect { ctx.parse_html(%(<p>x</p>)) }.to raise_error(Moxml::AdapterError, /not supported/)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
16
29
|
describe "#config" do
|
|
17
30
|
it "has a configuration" do
|
|
18
31
|
expect(context.config).to be_a(Moxml::Config)
|
data/spec/moxml/element_spec.rb
CHANGED
|
@@ -140,4 +140,29 @@ RSpec.describe Moxml::Element do
|
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
|
+
|
|
144
|
+
describe "#append_xml" do
|
|
145
|
+
it "appends a fragment's top-level nodes with entities round-tripped" do
|
|
146
|
+
%w[leptris nokogiri oga rexml].each do |adapter|
|
|
147
|
+
ctx = Moxml.new(adapter.to_sym)
|
|
148
|
+
doc = ctx.parse(%(<root/>))
|
|
149
|
+
result = doc.root.append_xml(%(<a x="1">t1 & u</a><b/>tail))
|
|
150
|
+
expect(result).to equal(doc.root)
|
|
151
|
+
out = doc.to_xml
|
|
152
|
+
expect(out).to include(%(<a x="1">t1 & u</a>))
|
|
153
|
+
expect(out).to include("<b></b>")
|
|
154
|
+
expect(out).to include("tail")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "raises AdapterError on the ox adapter" do
|
|
159
|
+
doc = Moxml.new(:ox).parse(%(<root/>))
|
|
160
|
+
expect { doc.root.append_xml(%(<a/>)) }.to raise_error(Moxml::AdapterError, /Ox/)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "raises ParseError for non-well-formed fragments" do
|
|
164
|
+
doc = Moxml.new(:leptris).parse(%(<root/>))
|
|
165
|
+
expect { doc.root.append_xml(%(<a><b></a>)) }.to raise_error(Moxml::ParseError)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
143
168
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.31
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -205,6 +205,7 @@ files:
|
|
|
205
205
|
- lib/moxml/entity_registry.rb
|
|
206
206
|
- lib/moxml/entity_registry_opal_data.rb
|
|
207
207
|
- lib/moxml/error.rb
|
|
208
|
+
- lib/moxml/lazy_node_set.rb
|
|
208
209
|
- lib/moxml/materializer.rb
|
|
209
210
|
- lib/moxml/namespace.rb
|
|
210
211
|
- lib/moxml/native_attachment.rb
|