moxml 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/opal.yml +37 -0
- data/.rspec-opal +5 -0
- data/Gemfile +6 -0
- data/Rakefile +67 -0
- data/lib/compat/opal/rexml/namespace.rb +56 -0
- data/lib/compat/opal/rexml/parsers/baseparser.rb +952 -0
- data/lib/compat/opal/rexml/source.rb +213 -0
- data/lib/compat/opal/rexml/text.rb +418 -0
- data/lib/compat/opal/rexml/xmltokens.rb +45 -0
- data/lib/compat/opal/rexml_compat.rb +76 -0
- data/lib/moxml/adapter/base.rb +5 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +3 -0
- data/lib/moxml/adapter/customized_libxml/text.rb +6 -1
- data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -10
- data/lib/moxml/adapter/headed_ox.rb +2 -6
- data/lib/moxml/adapter/libxml/entity_ref_registry.rb +105 -0
- data/lib/moxml/adapter/libxml/entity_restorer.rb +92 -0
- data/lib/moxml/adapter/libxml.rb +386 -382
- data/lib/moxml/adapter/nokogiri.rb +7 -18
- data/lib/moxml/adapter/oga.rb +4 -22
- data/lib/moxml/adapter/ox.rb +8 -23
- data/lib/moxml/adapter/rexml.rb +29 -33
- data/lib/moxml/adapter.rb +38 -8
- data/lib/moxml/config.rb +1 -1
- data/lib/moxml/entity_registry.rb +36 -31
- data/lib/moxml/entity_registry_opal_data.rb +2137 -0
- data/lib/moxml/node.rb +19 -26
- data/lib/moxml/sax/namespace_splitter.rb +54 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +9 -1
- data/spec/consistency/adapter_parity_spec.rb +1 -1
- data/spec/integration/all_adapters_spec.rb +1 -1
- data/spec/integration/w3c_namespace_spec.rb +1 -1
- data/spec/moxml/adapter/libxml_internals_spec.rb +167 -0
- data/spec/moxml/adapter/ox_spec.rb +8 -0
- data/spec/moxml/adapter/platform_spec.rb +69 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -6
- data/spec/moxml/entity_registry_spec.rb +10 -0
- data/spec/moxml/native_attachment/opal_spec.rb +39 -2
- data/spec/moxml/node_type_map_spec.rb +43 -0
- data/spec/moxml/opal_rexml_adapter_spec.rb +14 -0
- data/spec/moxml/opal_smoke_spec.rb +61 -0
- data/spec/moxml/sax/namespace_splitter_spec.rb +67 -0
- data/spec/moxml/text_spec.rb +1 -1
- data/spec/performance/benchmark_spec.rb +1 -1
- data/spec/spec_helper.rb +32 -13
- data/spec/support/opal.rb +16 -0
- metadata +21 -2
data/lib/moxml/node.rb
CHANGED
|
@@ -135,19 +135,6 @@ module Moxml
|
|
|
135
135
|
children.last
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
# Returns the text content of this node
|
|
139
|
-
# For elements, returns concatenated text of all text children
|
|
140
|
-
# For text nodes, returns the content if available
|
|
141
|
-
def text
|
|
142
|
-
if respond_to?(:content)
|
|
143
|
-
content
|
|
144
|
-
elsif respond_to?(:children)
|
|
145
|
-
children.grep(Text).map(&:content).join
|
|
146
|
-
else
|
|
147
|
-
""
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
138
|
# Returns the text content of this node
|
|
152
139
|
# Subclasses should override this method
|
|
153
140
|
# Element and Text have their own implementations
|
|
@@ -220,22 +207,28 @@ module Moxml
|
|
|
220
207
|
nil
|
|
221
208
|
end
|
|
222
209
|
|
|
210
|
+
# Registry mapping node type symbols to wrapper classes.
|
|
211
|
+
# Built lazily to avoid load-order issues with subclasses.
|
|
212
|
+
def self.node_type_map
|
|
213
|
+
@node_type_map ||= {
|
|
214
|
+
element: Element,
|
|
215
|
+
text: Text,
|
|
216
|
+
cdata: Cdata,
|
|
217
|
+
comment: Comment,
|
|
218
|
+
processing_instruction: ProcessingInstruction,
|
|
219
|
+
document: Document,
|
|
220
|
+
declaration: Declaration,
|
|
221
|
+
doctype: Doctype,
|
|
222
|
+
attribute: Attribute,
|
|
223
|
+
entity_reference: EntityReference,
|
|
224
|
+
}.freeze
|
|
225
|
+
end
|
|
226
|
+
|
|
223
227
|
def self.wrap(node, context)
|
|
224
228
|
return nil if node.nil?
|
|
225
229
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
when :text then Text
|
|
229
|
-
when :cdata then Cdata
|
|
230
|
-
when :comment then Comment
|
|
231
|
-
when :processing_instruction then ProcessingInstruction
|
|
232
|
-
when :document then Document
|
|
233
|
-
when :declaration then Declaration
|
|
234
|
-
when :doctype then Doctype
|
|
235
|
-
when :attribute then Attribute
|
|
236
|
-
when :entity_reference then EntityReference
|
|
237
|
-
else self
|
|
238
|
-
end
|
|
230
|
+
type = adapter(context).node_type(node)
|
|
231
|
+
klass = node_type_map[type] || self
|
|
239
232
|
|
|
240
233
|
klass.new(node, context)
|
|
241
234
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module SAX
|
|
5
|
+
# Splits a flat attribute hash into regular attributes and namespace declarations.
|
|
6
|
+
#
|
|
7
|
+
# Every adapter SAX bridge performs the same split: attributes whose names
|
|
8
|
+
# start with "xmlns" are namespace declarations, everything else is a regular
|
|
9
|
+
# attribute. This module provides a single implementation.
|
|
10
|
+
module NamespaceSplitter
|
|
11
|
+
# @param attributes [Hash, Array<Array>] attributes as a hash or array of pairs
|
|
12
|
+
# @return [Array(Hash, Hash)] [regular_attrs, namespaces]
|
|
13
|
+
def split_attributes_and_namespaces(attributes)
|
|
14
|
+
attrs = {}
|
|
15
|
+
ns = {}
|
|
16
|
+
|
|
17
|
+
each_attribute(attributes) do |name, value|
|
|
18
|
+
name_s = name.to_s
|
|
19
|
+
if name_s == "xmlns" || name_s.start_with?("xmlns:")
|
|
20
|
+
prefix = name_s == "xmlns" ? nil : name_s.sub("xmlns:", "")
|
|
21
|
+
ns[prefix] = value
|
|
22
|
+
else
|
|
23
|
+
attrs[name_s] = value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
[attrs, ns]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def each_attribute(attributes, &block)
|
|
33
|
+
case attributes
|
|
34
|
+
when Hash
|
|
35
|
+
attributes.each(&block)
|
|
36
|
+
when Array
|
|
37
|
+
attributes.each { |pair| yield pair[0], pair[1] }
|
|
38
|
+
when nil
|
|
39
|
+
# nothing
|
|
40
|
+
else
|
|
41
|
+
if attributes.respond_to?(:each)
|
|
42
|
+
attributes.each do |item|
|
|
43
|
+
if item.is_a?(Array) && item.size >= 2
|
|
44
|
+
yield item[0], item[1]
|
|
45
|
+
elsif item.respond_to?(:name) && item.respond_to?(:value)
|
|
46
|
+
yield item.name, item.value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xml_utils.rb
CHANGED
|
@@ -75,7 +75,15 @@ module Moxml
|
|
|
75
75
|
# (W3C Namespaces in XML, https://www.w3.org/TR/xml-names/).
|
|
76
76
|
# Use split instead of parse to avoid scheme-specific validation
|
|
77
77
|
# that rejects valid opaque URIs like "mailto:bar".
|
|
78
|
-
URI::RFC3986_PARSER
|
|
78
|
+
if defined?(URI::RFC3986_PARSER)
|
|
79
|
+
URI::RFC3986_PARSER.split(uri)
|
|
80
|
+
elsif uri.match?(/\A[a-zA-Z][a-zA-Z0-9+\-.]*:[^\x00-\x20]*\z/)
|
|
81
|
+
# Minimal URI validation for Opal (no RFC3986_PARSER available)
|
|
82
|
+
else
|
|
83
|
+
# Accept relative references and bare paths
|
|
84
|
+
return unless uri.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F]/)
|
|
85
|
+
raise ValidationError, "Invalid URI: #{uri}"
|
|
86
|
+
end
|
|
79
87
|
rescue URI::InvalidURIError
|
|
80
88
|
raise ValidationError, "Invalid URI: #{uri}"
|
|
81
89
|
end
|
|
@@ -6,7 +6,7 @@ RSpec.describe "Adapter Examples" do
|
|
|
6
6
|
describe "Serialization consistency" do
|
|
7
7
|
it "produces equivalent XML across adapters",
|
|
8
8
|
skip: "No easy way to exclude the declaration from Nokogiri documents" do
|
|
9
|
-
docs = Moxml::Adapter::
|
|
9
|
+
docs = Moxml::Adapter::AVAILABLE_ADAPTERS.map do |adapter|
|
|
10
10
|
Moxml.new(adapter).parse(xml, fragment: true)
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -32,7 +32,7 @@ RSpec.describe "Cross-adapter integration" do
|
|
|
32
32
|
"Performance Examples",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
|
-
Moxml::Adapter::
|
|
35
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
36
36
|
context "with #{adapter_name}" do
|
|
37
37
|
around do |example|
|
|
38
38
|
Moxml.with_config(adapter_name) do
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# invalid - validity error; non-validating parsers should accept
|
|
12
12
|
|
|
13
13
|
RSpec.describe "W3C XML Namespaces 1.0 test suite" do
|
|
14
|
-
Moxml::Adapter::
|
|
14
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
15
15
|
context "with #{adapter_name}" do
|
|
16
16
|
around do |example|
|
|
17
17
|
Moxml.with_config(adapter_name) do
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "libxml"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
return
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "moxml/adapter/libxml"
|
|
10
|
+
|
|
11
|
+
# Targeted tests for private helpers extracted during the perf refactor.
|
|
12
|
+
# The public `serialize` path covers them transitively, but a future
|
|
13
|
+
# refactor of those helpers benefits from fine-grained safety nets.
|
|
14
|
+
RSpec.describe Moxml::Adapter::Libxml do
|
|
15
|
+
describe ".emit_children_with_layout" do
|
|
16
|
+
let(:adapter) { described_class }
|
|
17
|
+
|
|
18
|
+
def parse_root(xml)
|
|
19
|
+
doc = LibXML::XML::Parser.string(xml).parse
|
|
20
|
+
doc.root
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def emit(root, indent_size: 2, depth: 0, eref_active: false)
|
|
24
|
+
output = +""
|
|
25
|
+
adapter.send(:emit_children_with_layout, output, root, indent_size, depth,
|
|
26
|
+
eref_active: eref_active)
|
|
27
|
+
output
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "with all-element children" do
|
|
31
|
+
it "emits a newline + per-level padding before each child" do
|
|
32
|
+
root = parse_root("<root><a/><b/></root>")
|
|
33
|
+
expect(emit(root)).to eq("\n <a></a>\n <b></b>")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "emits no padding when indent_size is zero" do
|
|
37
|
+
root = parse_root("<root><a/><b/></root>")
|
|
38
|
+
expect(emit(root, indent_size: 0)).to eq("\n<a></a>\n<b></b>")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "scales padding with depth" do
|
|
42
|
+
root = parse_root("<root><a/></root>")
|
|
43
|
+
expect(emit(root, depth: 2)).to eq("\n <a></a>")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context "with mixed text + element content" do
|
|
48
|
+
it "does not emit newlines (text suppresses block layout)" do
|
|
49
|
+
root = parse_root("<p>hello<b>world</b>!</p>")
|
|
50
|
+
expect(emit(root)).to eq("hello<b>world</b>!")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "emits a newline only before the first element when text follows" do
|
|
54
|
+
root = parse_root("<p><b>world</b>!</p>")
|
|
55
|
+
# First child is element, prev_block starts true → newline before it.
|
|
56
|
+
# Then text "!" sets prev_block false; no further block-level
|
|
57
|
+
# children follow, so no additional newlines.
|
|
58
|
+
expect(emit(root)).to eq("\n <b>world</b>!")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context "with CDATA + element children" do
|
|
63
|
+
it "treats cdata as text-like and suppresses surrounding newlines" do
|
|
64
|
+
cdata_xml = "<r><![CDATA[X]]><a/></r>"
|
|
65
|
+
root = parse_root(cdata_xml)
|
|
66
|
+
# CDATA is text-like → no newline before it, prev_block goes false,
|
|
67
|
+
# then <a/> follows but prev_block was set false by CDATA, so no \n.
|
|
68
|
+
expect(emit(root)).to eq("<![CDATA[X]]><a></a>")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context "with comment + element children" do
|
|
73
|
+
it "treats comment as block-level and emits newlines between siblings" do
|
|
74
|
+
root = parse_root("<x><!-- c --><y/></x>")
|
|
75
|
+
expect(emit(root)).to eq("\n <!-- c -->\n <y></y>")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context "with whitespace-only text children" do
|
|
80
|
+
it "skips them and produces the same layout as a doc without them" do
|
|
81
|
+
with_ws = emit(parse_root("<root> <a/> <b/> </root>"))
|
|
82
|
+
without_ws = emit(parse_root("<root><a/><b/></root>"))
|
|
83
|
+
expect(with_ws).to eq(without_ws)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "with no children" do
|
|
88
|
+
it "appends nothing" do
|
|
89
|
+
root = parse_root("<empty/>")
|
|
90
|
+
expect(emit(root)).to eq("")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe ".lookup_entity_ref_serialization" do
|
|
96
|
+
let(:adapter) { described_class }
|
|
97
|
+
let(:context) { Moxml.new(:libxml) }
|
|
98
|
+
|
|
99
|
+
# `lookup_entity_ref_serialization` is called from the recursive
|
|
100
|
+
# serialize path with a raw libxml ::Node (not the wrapper), so we
|
|
101
|
+
# unwrap to match the call-site contract.
|
|
102
|
+
def libxml_native(moxml_node)
|
|
103
|
+
adapter.send(:unpatch_node, moxml_node.native)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "returns [nil, nil] when the document has no entity-ref attachments" do
|
|
107
|
+
root = libxml_native(context.parse("<root><a/></root>").root)
|
|
108
|
+
expect(adapter.send(:lookup_entity_ref_serialization, root)).to eq([nil, nil])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "returns [nil, nil] for an element with no entity refs even when the doc has erefs elsewhere" do
|
|
112
|
+
doc = context.parse("<root><a/><b/></root>")
|
|
113
|
+
a = doc.root.children.first
|
|
114
|
+
b = doc.root.children.last
|
|
115
|
+
eref = Moxml::EntityReference.new(
|
|
116
|
+
adapter.create_native_entity_reference("amp"), context
|
|
117
|
+
)
|
|
118
|
+
a.add_child(eref)
|
|
119
|
+
|
|
120
|
+
expect(adapter.send(:lookup_entity_ref_serialization, libxml_native(b)))
|
|
121
|
+
.to eq([nil, nil])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "returns [refs, sequence] when both are registered for the element" do
|
|
125
|
+
doc = context.parse("<root><a>text</a></root>")
|
|
126
|
+
a = doc.root.children.first
|
|
127
|
+
eref = Moxml::EntityReference.new(
|
|
128
|
+
adapter.create_native_entity_reference("amp"), context
|
|
129
|
+
)
|
|
130
|
+
a.add_child(eref)
|
|
131
|
+
|
|
132
|
+
refs, seq = adapter.send(:lookup_entity_ref_serialization, libxml_native(a))
|
|
133
|
+
expect(refs).to be_an(Array).and(satisfy { |r| !r.empty? })
|
|
134
|
+
expect(seq).to be_an(Array).and(include(:eref))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe "entity-ref interleaved serialization" do
|
|
139
|
+
let(:adapter) { described_class }
|
|
140
|
+
let(:context) { Moxml.new(:libxml) }
|
|
141
|
+
|
|
142
|
+
it "preserves normal child indentation when entity refs are present" do
|
|
143
|
+
doc = context.parse("<root><a><b/></a></root>")
|
|
144
|
+
a = doc.root.children.first
|
|
145
|
+
eref = Moxml::EntityReference.new(
|
|
146
|
+
adapter.create_native_entity_reference("amp"), context
|
|
147
|
+
)
|
|
148
|
+
a.add_child(eref)
|
|
149
|
+
|
|
150
|
+
expect(doc.to_xml(no_declaration: true, indent: 2))
|
|
151
|
+
.to eq("<root>\n <a>\n <b></b>&</a></root>")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe Moxml::Adapter::Libxml::EntityRestorer do
|
|
156
|
+
let(:context) { Moxml.new(:libxml) }
|
|
157
|
+
|
|
158
|
+
it "restores entities through its public entry point" do
|
|
159
|
+
doc = context.parse("<p>\u00A9</p>")
|
|
160
|
+
context.config.restore_entities = true
|
|
161
|
+
|
|
162
|
+
described_class.new(doc).run
|
|
163
|
+
|
|
164
|
+
expect(doc.to_xml(no_declaration: true)).to eq("<p>©</p>")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -11,6 +11,14 @@ RSpec.describe Moxml::Adapter::Ox do
|
|
|
11
11
|
|
|
12
12
|
it_behaves_like "xml adapter"
|
|
13
13
|
|
|
14
|
+
describe "node_type" do
|
|
15
|
+
it "returns :namespace for CustomizedOx::Namespace nodes" do
|
|
16
|
+
element = described_class.create_native_element("test")
|
|
17
|
+
ns = described_class.create_native_namespace(element, "ns", "http://example.com")
|
|
18
|
+
expect(described_class.node_type(ns)).to eq(:namespace)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
describe "text handling" do
|
|
15
23
|
let(:doc) { described_class.create_document }
|
|
16
24
|
let(:element) { described_class.create_native_element("test") }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Adapter, ".platform_adapters" do
|
|
6
|
+
it "includes all known adapters under MRI" do
|
|
7
|
+
expect(described_class.platform_adapters).to include(:nokogiri, :oga, :rexml, :ox)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "uses the AVAILABLE_ADAPTERS constant under MRI" do
|
|
11
|
+
expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RSpec.describe Moxml::Adapter, ".available?" do
|
|
16
|
+
it "returns true for :oga" do
|
|
17
|
+
expect(described_class.available?(:oga)).to be true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "returns true for :nokogiri under MRI" do
|
|
21
|
+
expect(described_class.available?(:nokogiri)).to be true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context "when Opal platform adapters are in effect" do
|
|
25
|
+
before do
|
|
26
|
+
allow(described_class).to receive(:platform_adapters)
|
|
27
|
+
.and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "returns false for :nokogiri" do
|
|
31
|
+
expect(described_class.available?(:nokogiri)).to be false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "returns true for :rexml" do
|
|
35
|
+
expect(described_class.available?(:rexml)).to be true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
RSpec.describe Moxml::Adapter, ".load" do
|
|
41
|
+
context "when Opal platform adapters are in effect" do
|
|
42
|
+
before do
|
|
43
|
+
allow(described_class).to receive(:platform_adapters)
|
|
44
|
+
.and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "raises AdapterError for :nokogiri" do
|
|
48
|
+
expect { described_class.load(:nokogiri) }.to raise_error(
|
|
49
|
+
Moxml::AdapterError, /not available on this platform/
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
RSpec.describe "Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS" do
|
|
56
|
+
it "contains only :rexml" do
|
|
57
|
+
expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[rexml])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
RSpec.describe "Moxml::Adapter::CONST_NAME_MAP" do
|
|
62
|
+
it "maps :headed_ox to HeadedOx" do
|
|
63
|
+
expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "falls back to capitalize for unmapped adapters" do
|
|
67
|
+
expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -159,9 +159,6 @@ RSpec.shared_examples "xml adapter" do
|
|
|
159
159
|
if described_class.name.include?("Oga")
|
|
160
160
|
pending("Oga does not support indentation settings")
|
|
161
161
|
end
|
|
162
|
-
if described_class.name.include?("Rexml")
|
|
163
|
-
pending("Postponed for Rexml till better times")
|
|
164
|
-
end
|
|
165
162
|
if described_class.name.include?("Libxml")
|
|
166
163
|
skip("LibXML serialization does not support indentation (documented limitation)")
|
|
167
164
|
end
|
|
@@ -219,9 +216,6 @@ RSpec.shared_examples "xml adapter" do
|
|
|
219
216
|
end
|
|
220
217
|
|
|
221
218
|
it "preserves and correctly handles multiple namespaces" do
|
|
222
|
-
if described_class.name.include?("Rexml")
|
|
223
|
-
pending("Rexml does not respect ZPath namespaces")
|
|
224
|
-
end
|
|
225
219
|
# Parse original XML
|
|
226
220
|
doc = described_class.parse(xml).native
|
|
227
221
|
|
|
@@ -180,6 +180,16 @@ RSpec.describe Moxml::EntityRegistry do
|
|
|
180
180
|
registry = described_class.new
|
|
181
181
|
expect(registry.load_all).to be(registry)
|
|
182
182
|
end
|
|
183
|
+
|
|
184
|
+
it "emits deprecation warnings" do
|
|
185
|
+
registry = described_class.new
|
|
186
|
+
[-> { registry.load_html5 },
|
|
187
|
+
-> { registry.load_mathml },
|
|
188
|
+
-> { registry.load_iso },
|
|
189
|
+
-> { registry.load_all }].each do |callable|
|
|
190
|
+
expect { callable.call }.to output.to_stderr
|
|
191
|
+
end
|
|
192
|
+
end
|
|
183
193
|
end
|
|
184
194
|
|
|
185
195
|
describe "#standard_entity?" do
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
-
require_relative "shared_examples"
|
|
5
4
|
|
|
6
5
|
RSpec.describe Moxml::NativeAttachment::Opal do
|
|
7
|
-
|
|
6
|
+
subject(:attachments) { described_class.new }
|
|
7
|
+
|
|
8
|
+
let(:native) { Object.new }
|
|
9
|
+
let(:other_native) { Object.new }
|
|
10
|
+
|
|
11
|
+
it "stores and reads attachments by native object and key" do
|
|
12
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
13
|
+
attachments.set(native, :doctype, "html")
|
|
14
|
+
attachments.set(other_native, :entity_refs, ["lt"])
|
|
15
|
+
|
|
16
|
+
aggregate_failures do
|
|
17
|
+
expect(attachments.get(native, :entity_refs)).to eq(["amp"])
|
|
18
|
+
expect(attachments.get(native, :doctype)).to eq("html")
|
|
19
|
+
expect(attachments.get(other_native, :entity_refs)).to eq(["lt"])
|
|
20
|
+
expect(attachments.key?(native, :entity_refs)).to be(true)
|
|
21
|
+
expect(attachments.key?(native, :missing)).to be(false)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "preserves explicit nil attachments" do
|
|
26
|
+
attachments.set(native, :xml_declaration, nil)
|
|
27
|
+
|
|
28
|
+
aggregate_failures do
|
|
29
|
+
expect(attachments.get(native, :xml_declaration)).to be_nil
|
|
30
|
+
expect(attachments.key?(native, :xml_declaration)).to be(true)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "deletes attachments" do
|
|
35
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
36
|
+
|
|
37
|
+
expect(attachments.delete(native, :entity_refs)).to eq(["amp"])
|
|
38
|
+
|
|
39
|
+
aggregate_failures do
|
|
40
|
+
expect(attachments.get(native, :entity_refs)).to be_nil
|
|
41
|
+
expect(attachments.key?(native, :entity_refs)).to be(false)
|
|
42
|
+
expect(attachments.delete(native, :entity_refs)).to be_nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
8
45
|
|
|
9
46
|
it "stores attachments in Moxml-owned instance variables" do
|
|
10
47
|
attachments = described_class.new
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Node, "node_type_map" do
|
|
6
|
+
it "maps every type symbol to a Node subclass" do
|
|
7
|
+
described_class.node_type_map.each_value do |klass|
|
|
8
|
+
expect(klass < described_class).to be(true), "Expected #{klass} < Moxml::Node"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "covers all standard node types" do
|
|
13
|
+
expected = %i[element text cdata comment processing_instruction
|
|
14
|
+
document declaration doctype attribute entity_reference]
|
|
15
|
+
expect(described_class.node_type_map.keys).to match_array(expected)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "is frozen to prevent modification" do
|
|
19
|
+
expect(described_class.node_type_map).to be_frozen
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe ".wrap" do
|
|
23
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
24
|
+
|
|
25
|
+
it "returns nil for nil input" do
|
|
26
|
+
expect(described_class.wrap(nil, ctx)).to be_nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "wraps native nodes using node_type_map" do
|
|
30
|
+
doc = ctx.parse("<root><child>text</child></root>")
|
|
31
|
+
root = doc.root
|
|
32
|
+
expect(root).to be_a(Moxml::Element)
|
|
33
|
+
|
|
34
|
+
text_node = root.children.first.children.first
|
|
35
|
+
expect(text_node).to be_a(Moxml::Text)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "returns a Node for unknown types" do
|
|
39
|
+
node = described_class.wrap(Object.new, ctx)
|
|
40
|
+
expect(node).to be_a(described_class)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/adapter/shared_examples/adapter_contract"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Adapter::Rexml, if: RUBY_ENGINE == "opal" do
|
|
7
|
+
around do |example|
|
|
8
|
+
Moxml.with_config(:rexml, true, "UTF-8") do
|
|
9
|
+
example.run
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it_behaves_like "xml adapter"
|
|
14
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Moxml Opal smoke test", if: RUBY_ENGINE == "opal" do
|
|
6
|
+
let(:context) { Moxml.new(:rexml) }
|
|
7
|
+
|
|
8
|
+
it "parses XML string to document" do
|
|
9
|
+
doc = context.parse("<root><child>text</child></root>")
|
|
10
|
+
expect(doc).not_to be_nil
|
|
11
|
+
root = doc.root
|
|
12
|
+
expect(root.name).to eq("root")
|
|
13
|
+
expect(root.children.first.name).to eq("child")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "round-trips parse and serialize" do
|
|
17
|
+
xml = '<person name="Alice"><age>30</age></person>'
|
|
18
|
+
doc = context.parse(xml)
|
|
19
|
+
serialized = doc.to_xml
|
|
20
|
+
expect(serialized).to include("person")
|
|
21
|
+
expect(serialized).to include("Alice")
|
|
22
|
+
expect(serialized).to include("30")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "builds XML document from scratch" do
|
|
26
|
+
doc = context.create_document
|
|
27
|
+
root = doc.create_element("root")
|
|
28
|
+
root["attr"] = "value"
|
|
29
|
+
text = doc.create_text("hello")
|
|
30
|
+
root.add_child(text)
|
|
31
|
+
doc.add_child(root)
|
|
32
|
+
|
|
33
|
+
expect(doc.to_xml).to include('attr="value"')
|
|
34
|
+
expect(doc.to_xml).to include("hello")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "handles namespaces" do
|
|
38
|
+
xml = '<root xmlns:ns="http://example.com"><ns:child>data</ns:child></root>'
|
|
39
|
+
doc = context.parse(xml)
|
|
40
|
+
expect(doc.to_xml).to include("ns:child")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "handles CDATA sections" do
|
|
44
|
+
xml = "<root><![CDATA[<script>alert('xss')</script>]]></root>"
|
|
45
|
+
doc = context.parse(xml)
|
|
46
|
+
expect(doc.to_xml).to include("<script>")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "handles comments" do
|
|
50
|
+
xml = "<root><!-- a comment --><child/></root>"
|
|
51
|
+
doc = context.parse(xml)
|
|
52
|
+
expect(doc.to_xml).to include("a comment")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "handles processing instructions" do
|
|
56
|
+
xml = '<?xml version="1.0"?><?pi-target pi-data?><root/>'
|
|
57
|
+
doc = context.parse(xml)
|
|
58
|
+
serialized = doc.to_xml
|
|
59
|
+
expect(serialized).to include("pi-target")
|
|
60
|
+
end
|
|
61
|
+
end
|