moxml 0.2.0 → 0.4.0
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/lib/compat/opal/moxml_boot.rb +1 -1
- data/lib/moxml/adapter/base.rb +10 -7
- data/lib/moxml/adapter/leptris.rb +30 -1
- data/lib/moxml/adapter/libxml.rb +59 -25
- data/lib/moxml/adapter/oga.rb +48 -1
- data/lib/moxml/adapter/rexml.rb +3 -1
- data/lib/moxml/builder.rb +0 -18
- data/lib/moxml/config.rb +0 -2
- data/lib/moxml/context.rb +24 -0
- data/lib/moxml/document.rb +1 -1
- data/lib/moxml/entity/restorer.rb +95 -0
- data/lib/moxml/entity.rb +1 -0
- data/lib/moxml/entity_registry.rb +1 -1
- data/lib/moxml/error.rb +0 -42
- data/lib/moxml/node.rb +45 -9
- data/lib/moxml/node_set.rb +1 -1
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +85 -2
- data/lib/moxml.rb +0 -3
- data/spec/integration/all_adapters_spec.rb +1 -1
- data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb} +1 -2
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +15 -0
- data/spec/moxml/adapter/leptris_spec.rb +9 -1
- data/spec/moxml/adapter/libxml_internals_spec.rb +1 -1
- data/spec/moxml/config_spec.rb +0 -1
- data/spec/moxml/context_spec.rb +22 -0
- data/spec/moxml/error_spec.rb +0 -50
- data/spec/moxml/xpath/axes_spec.rb +40 -0
- metadata +4 -6
- data/lib/moxml/adapter/libxml/entity_restorer.rb +0 -94
- data/lib/moxml/document_builder.rb +0 -167
- data/spec/moxml/document_builder_spec.rb +0 -30
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Moxml
|
|
|
22
22
|
class Compiler
|
|
23
23
|
# Shared context for compiled Procs
|
|
24
24
|
CONTEXT = Context.new
|
|
25
|
+
private_constant :CONTEXT
|
|
25
26
|
|
|
26
27
|
# Expression cache
|
|
27
28
|
CACHE = Cache.new
|
|
@@ -294,6 +295,12 @@ module Moxml
|
|
|
294
295
|
|
|
295
296
|
handler = axis_name.gsub("-", "_")
|
|
296
297
|
|
|
298
|
+
unless respond_to?(:"on_axis_#{handler}", true)
|
|
299
|
+
raise XPath::Error.new(
|
|
300
|
+
"XPath axis #{axis_name.inspect} is not supported by this engine",
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
|
|
297
304
|
send(:"on_axis_#{handler}", test, input, &block)
|
|
298
305
|
end
|
|
299
306
|
|
|
@@ -362,6 +369,78 @@ module Moxml
|
|
|
362
369
|
end
|
|
363
370
|
end
|
|
364
371
|
|
|
372
|
+
# AXIS: ancestor - all ancestors up to the document
|
|
373
|
+
def on_axis_ancestor(ast, input)
|
|
374
|
+
ancestor = unique_literal(:ancestor)
|
|
375
|
+
|
|
376
|
+
document_or_node(input).if_true do
|
|
377
|
+
input.ancestors.each.add_block(ancestor) do
|
|
378
|
+
condition = process(ast, ancestor)
|
|
379
|
+
if block_given?
|
|
380
|
+
condition.if_true { yield ancestor }
|
|
381
|
+
else
|
|
382
|
+
condition.if_true { ancestor }
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# AXIS: ancestor-or-self - the context node and its ancestors
|
|
389
|
+
def on_axis_ancestor_or_self(ast, input)
|
|
390
|
+
ancestor = unique_literal(:ancestor)
|
|
391
|
+
|
|
392
|
+
document_or_node(input).if_true do
|
|
393
|
+
self_result = process(ast, input)
|
|
394
|
+
self_collection = if block_given?
|
|
395
|
+
self_result.if_true { yield input }
|
|
396
|
+
else
|
|
397
|
+
self_result.if_true { input }
|
|
398
|
+
end
|
|
399
|
+
self_collection.followed_by do
|
|
400
|
+
input.ancestors.each.add_block(ancestor) do
|
|
401
|
+
condition = process(ast, ancestor)
|
|
402
|
+
if block_given?
|
|
403
|
+
condition.if_true { yield ancestor }
|
|
404
|
+
else
|
|
405
|
+
condition.if_true { ancestor }
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# AXIS: following-sibling - siblings after the context node
|
|
413
|
+
def on_axis_following_sibling(ast, input)
|
|
414
|
+
sibling = unique_literal(:sibling)
|
|
415
|
+
|
|
416
|
+
document_or_node(input).if_true do
|
|
417
|
+
input.following_siblings.each.add_block(sibling) do
|
|
418
|
+
condition = process(ast, sibling)
|
|
419
|
+
if block_given?
|
|
420
|
+
condition.if_true { yield sibling }
|
|
421
|
+
else
|
|
422
|
+
condition.if_true { sibling }
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# AXIS: preceding-sibling - siblings before the context node
|
|
429
|
+
def on_axis_preceding_sibling(ast, input)
|
|
430
|
+
sibling = unique_literal(:sibling)
|
|
431
|
+
|
|
432
|
+
document_or_node(input).if_true do
|
|
433
|
+
input.preceding_siblings.each.add_block(sibling) do
|
|
434
|
+
condition = process(ast, sibling)
|
|
435
|
+
if block_given?
|
|
436
|
+
condition.if_true { yield sibling }
|
|
437
|
+
else
|
|
438
|
+
condition.if_true { sibling }
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
365
444
|
# AXIS: descendant-or-self - Enables // operator
|
|
366
445
|
def on_axis_descendant_or_self(ast, input)
|
|
367
446
|
node = unique_literal(:descendant)
|
|
@@ -623,7 +702,11 @@ module Moxml
|
|
|
623
702
|
|
|
624
703
|
# Parent node (..)
|
|
625
704
|
def on_parent(_ast, input)
|
|
626
|
-
|
|
705
|
+
if block_given?
|
|
706
|
+
yield input.parent
|
|
707
|
+
else
|
|
708
|
+
input.parent
|
|
709
|
+
end
|
|
627
710
|
end
|
|
628
711
|
|
|
629
712
|
# ===== OPERATORS =====
|
|
@@ -1752,7 +1835,7 @@ module Moxml
|
|
|
1752
1835
|
|
|
1753
1836
|
# Helper: Check if AST node is a number
|
|
1754
1837
|
def number?(ast)
|
|
1755
|
-
|
|
1838
|
+
ast.type == :number
|
|
1756
1839
|
end
|
|
1757
1840
|
|
|
1758
1841
|
# Helper: Check if AST contains a call node with given name
|
data/lib/moxml.rb
CHANGED
|
@@ -26,7 +26,6 @@ module Moxml
|
|
|
26
26
|
default_indent: original.default_indent,
|
|
27
27
|
default_line_ending: original.default_line_ending,
|
|
28
28
|
entity_load_mode: original.entity_load_mode,
|
|
29
|
-
entity_encoding: original.entity_encoding,
|
|
30
29
|
restore_entities: original.restore_entities,
|
|
31
30
|
namespace_validation_mode: original.namespace_validation_mode,
|
|
32
31
|
entity_restoration_mode: original.entity_restoration_mode,
|
|
@@ -48,7 +47,6 @@ module Moxml
|
|
|
48
47
|
config.default_indent = saved_values[:default_indent]
|
|
49
48
|
config.default_line_ending = saved_values[:default_line_ending]
|
|
50
49
|
config.entity_load_mode = saved_values[:entity_load_mode]
|
|
51
|
-
config.entity_encoding = saved_values[:entity_encoding]
|
|
52
50
|
config.restore_entities = saved_values[:restore_entities]
|
|
53
51
|
config.namespace_validation_mode = saved_values[:namespace_validation_mode]
|
|
54
52
|
config.entity_restoration_mode = saved_values[:entity_restoration_mode]
|
|
@@ -82,7 +80,6 @@ module Moxml
|
|
|
82
80
|
autoload :Namespace, "moxml/namespace"
|
|
83
81
|
autoload :Doctype, "moxml/doctype"
|
|
84
82
|
autoload :EntityReference, "moxml/entity_reference"
|
|
85
|
-
autoload :DocumentBuilder, "moxml/document_builder"
|
|
86
83
|
autoload :Builder, "moxml/builder"
|
|
87
84
|
autoload :Entity, "moxml/entity"
|
|
88
85
|
autoload :EntityRegistry, "moxml/entity_registry"
|
data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb}
RENAMED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
RSpec.shared_examples "Moxml::
|
|
3
|
+
RSpec.shared_examples "Moxml::Parse Behavior" do
|
|
4
4
|
let(:context) { Moxml.new }
|
|
5
|
-
let(:builder) { Moxml::DocumentBuilder.new(context) }
|
|
6
5
|
|
|
7
6
|
describe "#build" do
|
|
8
7
|
it "builds a document model from native document" do
|
|
@@ -106,5 +106,20 @@ RSpec.shared_examples "Moxml::Node" do
|
|
|
106
106
|
expect(node.text).to eq("1")
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
|
+
|
|
110
|
+
describe "duplication" do
|
|
111
|
+
it "returns an independent deep copy" do
|
|
112
|
+
doc = context.parse("<root><a id='1'>text</a></root>")
|
|
113
|
+
original = doc.at_xpath("//a")
|
|
114
|
+
copy = original.dup
|
|
115
|
+
|
|
116
|
+
expect(copy).not_to equal(original)
|
|
117
|
+
expect(copy.name).to eq("a")
|
|
118
|
+
expect(copy["id"]).to eq("1")
|
|
119
|
+
expect(copy.text).to eq("text")
|
|
120
|
+
copy["id"] = "changed"
|
|
121
|
+
expect(original["id"]).to eq("1")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
109
124
|
end
|
|
110
125
|
end
|
|
@@ -58,8 +58,16 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
58
58
|
expect(attrs.map(&:value)).to eq(%w[1 2])
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
it "
|
|
61
|
+
it "evaluates element-context queries on the native engine" do
|
|
62
62
|
expect(doc.root.xpath(".//item").size).to eq(2)
|
|
63
|
+
first_item = doc.root.children.find(&:element?)
|
|
64
|
+
expect(first_item.xpath("following-sibling::*").map(&:name)).to eq(%w[item])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "falls back to the Ruby engine for parent-axis queries from the root" do
|
|
68
|
+
# The binding reports the root element parentless; moxml roots
|
|
69
|
+
# it at the document
|
|
70
|
+
expect(doc.root.xpath("../*").size).to eq(1)
|
|
63
71
|
end
|
|
64
72
|
|
|
65
73
|
it "falls back to the Ruby engine for the xmlns: reserved prefix" do
|
|
@@ -154,7 +154,7 @@ RSpec.describe Moxml::Adapter::Libxml do
|
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
describe Moxml::
|
|
157
|
+
describe Moxml::Entity::Restorer do
|
|
158
158
|
let(:context) { Moxml.new(:libxml) }
|
|
159
159
|
|
|
160
160
|
it "restores entities through its public entry point" do
|
data/spec/moxml/config_spec.rb
CHANGED
|
@@ -11,7 +11,6 @@ RSpec.describe Moxml::Config do
|
|
|
11
11
|
expect(config.default_encoding).to eq("UTF-8")
|
|
12
12
|
expect(config.default_indent).to eq(2)
|
|
13
13
|
expect(config.default_line_ending).to eq("\n")
|
|
14
|
-
expect(config.entity_encoding).to eq(:basic)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
it "sets default entity_load_mode to :required" do
|
data/spec/moxml/context_spec.rb
CHANGED
|
@@ -39,4 +39,26 @@ RSpec.describe Moxml::Context do
|
|
|
39
39
|
expect(doc.root.name).to eq("root")
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
describe "wrapper identity map" do
|
|
44
|
+
it "hands back the same wrapper for the same native" do
|
|
45
|
+
doc = context.parse("<root><item id='1'/></root>")
|
|
46
|
+
first = doc.xpath("//item").first
|
|
47
|
+
second = doc.xpath("//item").first
|
|
48
|
+
expect(first).to equal(second)
|
|
49
|
+
|
|
50
|
+
again = doc.root.children.last
|
|
51
|
+
expect(again).to equal(first)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "re-keys when refresh_native! swaps the native" do
|
|
55
|
+
doc = context.create_document
|
|
56
|
+
element = doc.create_element("x")
|
|
57
|
+
doc.add_child(element)
|
|
58
|
+
|
|
59
|
+
refreshed = doc.root
|
|
60
|
+
expect(refreshed.name).to eq("x")
|
|
61
|
+
expect(context.wrapper_for(refreshed.native)).to equal(refreshed)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
42
64
|
end
|
data/spec/moxml/error_spec.rb
CHANGED
|
@@ -150,31 +150,6 @@ RSpec.describe "Moxml errors" do
|
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
describe Moxml::SerializationError do
|
|
154
|
-
it "includes node, adapter, and format information" do
|
|
155
|
-
error = described_class.new(
|
|
156
|
-
"Failed to serialize",
|
|
157
|
-
adapter: "LibXML",
|
|
158
|
-
format: "xml",
|
|
159
|
-
)
|
|
160
|
-
expect(error.adapter).to eq("LibXML")
|
|
161
|
-
expect(error.format).to eq("xml")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
it "provides helpful error message" do
|
|
165
|
-
error = described_class.new(
|
|
166
|
-
"Failed to serialize",
|
|
167
|
-
adapter: "Ox",
|
|
168
|
-
format: "xml",
|
|
169
|
-
)
|
|
170
|
-
message = error.to_s
|
|
171
|
-
expect(message).to include("Failed to serialize")
|
|
172
|
-
expect(message).to include("Adapter: Ox")
|
|
173
|
-
expect(message).to include("Format: xml")
|
|
174
|
-
expect(message).to include("Hint:")
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
153
|
describe Moxml::DocumentStructureError do
|
|
179
154
|
it "includes attempted operation and state" do
|
|
180
155
|
error = described_class.new(
|
|
@@ -200,31 +175,6 @@ RSpec.describe "Moxml errors" do
|
|
|
200
175
|
end
|
|
201
176
|
end
|
|
202
177
|
|
|
203
|
-
describe Moxml::AttributeError do
|
|
204
|
-
it "includes attribute name, element, and value" do
|
|
205
|
-
error = described_class.new(
|
|
206
|
-
"Invalid attribute",
|
|
207
|
-
name: "id",
|
|
208
|
-
value: 123,
|
|
209
|
-
)
|
|
210
|
-
expect(error.attribute_name).to eq("id")
|
|
211
|
-
expect(error.value).to eq(123)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
it "provides helpful error message" do
|
|
215
|
-
error = described_class.new(
|
|
216
|
-
"Invalid attribute name",
|
|
217
|
-
name: "123invalid",
|
|
218
|
-
value: "test",
|
|
219
|
-
)
|
|
220
|
-
message = error.to_s
|
|
221
|
-
expect(message).to include("Invalid attribute name")
|
|
222
|
-
expect(message).to include("Attribute: 123invalid")
|
|
223
|
-
expect(message).to include("Value: \"test\"")
|
|
224
|
-
expect(message).to include("Hint:")
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
178
|
describe Moxml::NotImplementedError do
|
|
229
179
|
it "includes feature and adapter information" do
|
|
230
180
|
error = described_class.new(
|
|
@@ -205,6 +205,46 @@ RSpec.describe "XPath Axes" do
|
|
|
205
205
|
end
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
describe "ancestor and sibling axes" do
|
|
209
|
+
let(:family) do
|
|
210
|
+
context.parse("<root><parent id='p1'><b id='1'/><b id='2'/><c/><d/></parent></root>")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def select(expression, node = family)
|
|
214
|
+
ast = Moxml::XPath::Parser.parse(expression)
|
|
215
|
+
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
216
|
+
proc.call(node).map(&:name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "selects ancestors with ancestor::" do
|
|
220
|
+
expect(select("//c/ancestor::*")).to eq(%w[parent root])
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "includes the context node with ancestor-or-self::" do
|
|
224
|
+
expect(select("//c/ancestor-or-self::*")).to eq(%w[c parent root])
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "selects following siblings" do
|
|
228
|
+
expect(select("//c/following-sibling::*")).to eq(%w[d])
|
|
229
|
+
expect(select("//b[@id='1']/following-sibling::b")).to eq(%w[b])
|
|
230
|
+
expect(select("//b[@id='1']/following-sibling::*").size).to eq(3)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "selects preceding siblings" do
|
|
234
|
+
expect(select("//c/preceding-sibling::*")).to eq(%w[b b])
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "resolves the bare parent step and steps after it" do
|
|
238
|
+
expect(select("//c/..")).to eq(%w[parent])
|
|
239
|
+
expect(select("//c/../*").size).to eq(4)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "raises a clear error for unimplemented axes" do
|
|
243
|
+
expect { select("//c/following::*") }
|
|
244
|
+
.to raise_error(Moxml::XPath::Error, /not supported/)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
208
248
|
describe "descendant-or-self shorthand from an element" do
|
|
209
249
|
it "selects descendants with .//step" do
|
|
210
250
|
ast = Moxml::XPath::Parser.parse(".//grandchild")
|
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.
|
|
4
|
+
version: 0.4.0
|
|
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-08-
|
|
11
|
+
date: 2026-08-26 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
|
|
@@ -151,7 +151,6 @@ files:
|
|
|
151
151
|
- lib/moxml/adapter/leptris.rb
|
|
152
152
|
- lib/moxml/adapter/libxml.rb
|
|
153
153
|
- lib/moxml/adapter/libxml/entity_ref_registry.rb
|
|
154
|
-
- lib/moxml/adapter/libxml/entity_restorer.rb
|
|
155
154
|
- lib/moxml/adapter/nokogiri.rb
|
|
156
155
|
- lib/moxml/adapter/oga.rb
|
|
157
156
|
- lib/moxml/adapter/ox.rb
|
|
@@ -187,10 +186,10 @@ files:
|
|
|
187
186
|
- lib/moxml/declaration.rb
|
|
188
187
|
- lib/moxml/doctype.rb
|
|
189
188
|
- lib/moxml/document.rb
|
|
190
|
-
- lib/moxml/document_builder.rb
|
|
191
189
|
- lib/moxml/element.rb
|
|
192
190
|
- lib/moxml/entity.rb
|
|
193
191
|
- lib/moxml/entity/reference.rb
|
|
192
|
+
- lib/moxml/entity/restorer.rb
|
|
194
193
|
- lib/moxml/entity_reference.rb
|
|
195
194
|
- lib/moxml/entity_registry.rb
|
|
196
195
|
- lib/moxml/entity_registry_opal_data.rb
|
|
@@ -390,7 +389,7 @@ files:
|
|
|
390
389
|
- spec/integration/shared_examples/high_level/.gitkeep
|
|
391
390
|
- spec/integration/shared_examples/high_level/builder_behavior.rb
|
|
392
391
|
- spec/integration/shared_examples/high_level/context_behavior.rb
|
|
393
|
-
- spec/integration/shared_examples/high_level/
|
|
392
|
+
- spec/integration/shared_examples/high_level/parse_behavior.rb
|
|
394
393
|
- spec/integration/shared_examples/integration_workflows.rb
|
|
395
394
|
- spec/integration/shared_examples/line_ending_behavior.rb
|
|
396
395
|
- spec/integration/shared_examples/node_wrappers/.gitkeep
|
|
@@ -444,7 +443,6 @@ files:
|
|
|
444
443
|
- spec/moxml/declaration_preservation_spec.rb
|
|
445
444
|
- spec/moxml/declaration_spec.rb
|
|
446
445
|
- spec/moxml/doctype_spec.rb
|
|
447
|
-
- spec/moxml/document_builder_spec.rb
|
|
448
446
|
- spec/moxml/document_spec.rb
|
|
449
447
|
- spec/moxml/element_spec.rb
|
|
450
448
|
- spec/moxml/entity_preservation_spec.rb
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Moxml
|
|
4
|
-
module Adapter
|
|
5
|
-
class Libxml < Base
|
|
6
|
-
# Restores configured character entities into explicit Moxml
|
|
7
|
-
# EntityReference nodes after LibXML has parsed the native tree.
|
|
8
|
-
class EntityRestorer
|
|
9
|
-
def initialize(doc, adapter: Libxml)
|
|
10
|
-
@doc = doc
|
|
11
|
-
@ctx = doc.context
|
|
12
|
-
@registry = @ctx.entity_registry
|
|
13
|
-
@config = @ctx.config
|
|
14
|
-
@adapter = adapter
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def run
|
|
18
|
-
return unless @registry && @doc.root
|
|
19
|
-
|
|
20
|
-
walk(@doc.root)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def walk(element)
|
|
26
|
-
# Snapshot because we may add/remove siblings during the walk.
|
|
27
|
-
element.children.to_a.each do |child|
|
|
28
|
-
if child.is_a?(::Moxml::Text)
|
|
29
|
-
restore_text_node(child)
|
|
30
|
-
elsif child.is_a?(::Moxml::Element)
|
|
31
|
-
walk(child)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Matches DocumentBuilder's previous behavior, including the libxml
|
|
37
|
-
# limitation that adjacent native text nodes get merged.
|
|
38
|
-
def restore_text_node(text_node)
|
|
39
|
-
content = text_node.content
|
|
40
|
-
return unless content
|
|
41
|
-
|
|
42
|
-
chunks = chunk_text(content)
|
|
43
|
-
return if chunks.size == 1 && chunks.first.first == :text
|
|
44
|
-
|
|
45
|
-
parent = text_node.parent
|
|
46
|
-
return unless parent
|
|
47
|
-
|
|
48
|
-
text_node.remove
|
|
49
|
-
chunks.each { |type, payload| append_chunk(parent, type, payload) }
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def chunk_text(content)
|
|
53
|
-
chunks = []
|
|
54
|
-
buffer = +""
|
|
55
|
-
restorable = @registry.restorable_codepoints
|
|
56
|
-
|
|
57
|
-
content.each_char do |char|
|
|
58
|
-
cp = char.ord
|
|
59
|
-
if restorable.include?(cp) &&
|
|
60
|
-
(name = @registry.primary_name_for_codepoint(cp)) &&
|
|
61
|
-
@registry.should_restore?(cp, config: @config)
|
|
62
|
-
unless buffer.empty?
|
|
63
|
-
chunks << [:text, buffer.dup]
|
|
64
|
-
buffer.clear
|
|
65
|
-
end
|
|
66
|
-
chunks << [:eref, name]
|
|
67
|
-
else
|
|
68
|
-
buffer << char
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
chunks << [:text, buffer.dup] unless buffer.empty?
|
|
73
|
-
chunks
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def append_chunk(parent, type, payload)
|
|
77
|
-
case type
|
|
78
|
-
when :text
|
|
79
|
-
parent.add_child(::Moxml::Text.new(
|
|
80
|
-
@adapter.create_native_text(payload), @ctx
|
|
81
|
-
))
|
|
82
|
-
when :eref
|
|
83
|
-
parent.add_child(
|
|
84
|
-
::Moxml::EntityReference.new(
|
|
85
|
-
@adapter.create_native_entity_reference(payload),
|
|
86
|
-
@ctx,
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Moxml
|
|
4
|
-
class DocumentBuilder
|
|
5
|
-
attr_reader :context
|
|
6
|
-
|
|
7
|
-
def initialize(context)
|
|
8
|
-
@context = context
|
|
9
|
-
@node_stack = []
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def build(native_doc)
|
|
13
|
-
@current_doc = context.create_document(native_doc)
|
|
14
|
-
|
|
15
|
-
# Transfer has_declaration flag if present in attachments
|
|
16
|
-
if adapter.attachments.key?(native_doc, :has_declaration)
|
|
17
|
-
has_declaration = adapter.attachments.get(native_doc, :has_declaration)
|
|
18
|
-
@current_doc.has_xml_declaration = has_declaration
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Transfer DOCTYPE from parsed document if it exists in attachments
|
|
22
|
-
if adapter.attachments.key?(native_doc, :doctype)
|
|
23
|
-
doctype = adapter.attachments.get(native_doc, :doctype)
|
|
24
|
-
if doctype
|
|
25
|
-
adapter.attachments.set(@current_doc.native, :doctype, doctype)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
visit_node(native_doc)
|
|
30
|
-
@current_doc
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def visit_node(node)
|
|
36
|
-
case node_type(node)
|
|
37
|
-
when :document then visit_document(node)
|
|
38
|
-
when :element then visit_element(node)
|
|
39
|
-
when :text then visit_text(node)
|
|
40
|
-
when :cdata then visit_cdata(node)
|
|
41
|
-
when :comment then visit_comment(node)
|
|
42
|
-
when :processing_instruction then visit_processing_instruction(node)
|
|
43
|
-
when :doctype then visit_doctype(node)
|
|
44
|
-
when :entity_reference then visit_entity_reference(node)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def visit_document(doc)
|
|
49
|
-
@node_stack.push(@current_doc)
|
|
50
|
-
visit_children(doc)
|
|
51
|
-
@node_stack.clear
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def visit_element(node)
|
|
55
|
-
childless_node = adapter.duplicate_node(node)
|
|
56
|
-
adapter.replace_children(childless_node, [])
|
|
57
|
-
# Prepare node for new document (LibXML needs this)
|
|
58
|
-
childless_node = adapter.prepare_for_new_document(
|
|
59
|
-
childless_node,
|
|
60
|
-
@current_doc.native,
|
|
61
|
-
)
|
|
62
|
-
element = Element.new(childless_node, context)
|
|
63
|
-
@node_stack.last.add_child(element)
|
|
64
|
-
|
|
65
|
-
@node_stack.push(element) # add a parent for its children
|
|
66
|
-
visit_children(node)
|
|
67
|
-
@node_stack.pop # remove the parent
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def visit_text(node)
|
|
71
|
-
# Prepare node for new document before wrapping
|
|
72
|
-
prepared = adapter.prepare_for_new_document(node, @current_doc.native)
|
|
73
|
-
content = adapter.text_content(node)
|
|
74
|
-
|
|
75
|
-
# Check if we should restore entity references for this text
|
|
76
|
-
if context.config.restore_entities && text_has_restorable_entities?(content)
|
|
77
|
-
restore_entities_in_text(content)
|
|
78
|
-
else
|
|
79
|
-
@node_stack.last&.add_child(Text.new(prepared, context))
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def text_has_restorable_entities?(content)
|
|
84
|
-
return false unless content
|
|
85
|
-
|
|
86
|
-
registry = context.entity_registry
|
|
87
|
-
codepoints = registry.restorable_codepoints
|
|
88
|
-
content.each_char do |char|
|
|
89
|
-
return true if codepoints.include?(char.ord)
|
|
90
|
-
end
|
|
91
|
-
false
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def restore_entities_in_text(content)
|
|
95
|
-
parent = @node_stack.last
|
|
96
|
-
return unless parent
|
|
97
|
-
|
|
98
|
-
registry = context.entity_registry
|
|
99
|
-
config = context.config
|
|
100
|
-
buffer = +""
|
|
101
|
-
|
|
102
|
-
content.to_s.each_char do |char|
|
|
103
|
-
codepoint = char.ord
|
|
104
|
-
name = registry.primary_name_for_codepoint(codepoint)
|
|
105
|
-
|
|
106
|
-
if name && registry.should_restore?(codepoint, config: config)
|
|
107
|
-
# Flush buffered text before the entity
|
|
108
|
-
unless buffer.empty?
|
|
109
|
-
parent.add_child(Text.new(adapter.create_text(buffer), context))
|
|
110
|
-
buffer.clear
|
|
111
|
-
end
|
|
112
|
-
parent.add_child(
|
|
113
|
-
EntityReference.new(adapter.create_entity_reference(name), context),
|
|
114
|
-
)
|
|
115
|
-
else
|
|
116
|
-
buffer << char
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Flush remaining buffer
|
|
121
|
-
unless buffer.empty?
|
|
122
|
-
parent.add_child(Text.new(adapter.create_text(buffer), context))
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def visit_cdata(node)
|
|
127
|
-
prepared = adapter.prepare_for_new_document(node, @current_doc.native)
|
|
128
|
-
@node_stack.last&.add_child(Cdata.new(prepared, context))
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def visit_comment(node)
|
|
132
|
-
prepared = adapter.prepare_for_new_document(node, @current_doc.native)
|
|
133
|
-
@node_stack.last&.add_child(Comment.new(prepared, context))
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def visit_processing_instruction(node)
|
|
137
|
-
prepared = adapter.prepare_for_new_document(node, @current_doc.native)
|
|
138
|
-
@node_stack.last&.add_child(ProcessingInstruction.new(prepared, context))
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def visit_doctype(node)
|
|
142
|
-
prepared = adapter.prepare_for_new_document(node, @current_doc.native)
|
|
143
|
-
@node_stack.last&.add_child(Doctype.new(prepared, context))
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def visit_entity_reference(node)
|
|
147
|
-
prepared = adapter.prepare_for_new_document(node, @current_doc.native)
|
|
148
|
-
@node_stack.last&.add_child(EntityReference.new(prepared, context))
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def visit_children(node)
|
|
152
|
-
children(node).each { |child| visit_node(child) }
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def node_type(node)
|
|
156
|
-
context.config.adapter.node_type(node)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def children(node)
|
|
160
|
-
context.config.adapter.children(node)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def adapter
|
|
164
|
-
context.config.adapter
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|