moxml 0.2.0 → 0.3.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 +0 -7
- data/lib/moxml/adapter/libxml.rb +6 -18
- 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 +21 -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 +83 -1
- 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/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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51298c0a0ce4ed7c7868e294dc8174ada558b4a1c36c168bc18cdcfe2f728aec
|
|
4
|
+
data.tar.gz: a5e5ffce0b817e397bf98852aecfd9bb341d5154a94540f486432ea32dee8788
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf7d542cb81a21f1cfa517fafa2c07bdd996b9b7665ebf98e91c4e3f87ce79a1e1fa07f0d8f19837f6f45c73c36663ee54d602b70d72228a8de6e1ccb201a764
|
|
7
|
+
data.tar.gz: b971104eebb5bb3383bdacbc895d6cd54f0b4df400126f83f9e8002d80ec3e75c0afc1d3369d2c1277732b64326a2c0c044bb156078fdb6c3bb81a1c9c9fdeab
|
|
@@ -25,6 +25,7 @@ require "moxml/doctype"
|
|
|
25
25
|
require "moxml/entity_reference"
|
|
26
26
|
require "moxml/entity_registry"
|
|
27
27
|
require "moxml/entity"
|
|
28
|
+
require "moxml/entity/restorer"
|
|
28
29
|
require "moxml/entity/reference"
|
|
29
30
|
require "moxml/xml_emitter"
|
|
30
31
|
require "moxml/adapter"
|
|
@@ -44,7 +45,6 @@ require "moxml/adapter/customized_oga/raw_value_override"
|
|
|
44
45
|
require "moxml/adapter/customized_oga/xml_declaration"
|
|
45
46
|
require "moxml/adapter/customized_oga/xml_generator"
|
|
46
47
|
require "moxml/adapter/oga"
|
|
47
|
-
require "moxml/document_builder"
|
|
48
48
|
require "moxml/builder"
|
|
49
49
|
require "moxml/context"
|
|
50
50
|
require "moxml/config"
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -183,13 +183,6 @@ namespace_validation_mode: :strict)
|
|
|
183
183
|
node
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
def prepare_for_new_document(node, _target_doc)
|
|
187
|
-
# Hook for adapters that need special handling when moving nodes
|
|
188
|
-
# between documents (e.g., LibXML's document.import)
|
|
189
|
-
# Default: no-op for backward compatibility
|
|
190
|
-
node
|
|
191
|
-
end
|
|
192
|
-
|
|
193
186
|
# Check if the native document has an XML declaration
|
|
194
187
|
# @param native_doc the native document object
|
|
195
188
|
# @param wrapper [Moxml::Document] the wrapper with has_xml_declaration flag
|
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -8,7 +8,6 @@ module Moxml
|
|
|
8
8
|
module Adapter
|
|
9
9
|
class Libxml < Base
|
|
10
10
|
autoload :EntityRefRegistry, "moxml/adapter/libxml/entity_ref_registry"
|
|
11
|
-
autoload :EntityRestorer, "moxml/adapter/libxml/entity_restorer"
|
|
12
11
|
|
|
13
12
|
# Wrapper class to store DOCTYPE information
|
|
14
13
|
class DoctypeWrapper
|
|
@@ -121,10 +120,7 @@ module Moxml
|
|
|
121
120
|
|
|
122
121
|
ctx = _context || Context.new(:libxml)
|
|
123
122
|
# Single parse path: wrap libxml's already-complete C-parsed tree
|
|
124
|
-
# directly (same pattern as
|
|
125
|
-
# DocumentBuilder.build path walked the entire parsed tree and
|
|
126
|
-
# re-added every node to a fresh moxml-managed document, which
|
|
127
|
-
# made parse O(N) Ruby work on top of an already-complete parse.
|
|
123
|
+
# directly (same pattern as the other adapters).
|
|
128
124
|
# Doctype/declaration/PI attachments set above remain on
|
|
129
125
|
# native_doc, so the serialize path still sees them.
|
|
130
126
|
#
|
|
@@ -134,7 +130,7 @@ module Moxml
|
|
|
134
130
|
# path; the restoration walk is just one of potentially several
|
|
135
131
|
# post-processing steps and doesn't fork the construction.
|
|
136
132
|
doc = Document.new(native_doc, ctx)
|
|
137
|
-
|
|
133
|
+
Entity::Restorer.new(doc).run if ctx.config.restore_entities
|
|
138
134
|
doc
|
|
139
135
|
end
|
|
140
136
|
|
|
@@ -219,7 +215,7 @@ module Moxml
|
|
|
219
215
|
return :unknown unless node
|
|
220
216
|
|
|
221
217
|
# Fast path: native libxml nodes are the vast majority during
|
|
222
|
-
# parse traversal (
|
|
218
|
+
# parse traversal (wrapping visits raw libxml children).
|
|
223
219
|
# Skip the wrapper checks below for them.
|
|
224
220
|
if node.is_a?(::LibXML::XML::Node)
|
|
225
221
|
return NATIVE_NODE_TYPE_MAP[node.node_type] || :unknown
|
|
@@ -1035,7 +1031,7 @@ module Moxml
|
|
|
1035
1031
|
end
|
|
1036
1032
|
|
|
1037
1033
|
# Shallow duplication: copies the node itself (name, attrs, namespaces)
|
|
1038
|
-
# but NOT its descendants
|
|
1034
|
+
# but NOT its descendants (deep duplication composes this).
|
|
1039
1035
|
# walks the source tree and re-adds children one at a time via
|
|
1040
1036
|
# add_child, so a deep copy here would be done only to be stripped
|
|
1041
1037
|
# by replace_children, then rebuilt — O(N²) waste on parse.
|
|
@@ -1061,7 +1057,8 @@ module Moxml
|
|
|
1061
1057
|
node
|
|
1062
1058
|
end
|
|
1063
1059
|
when :element
|
|
1064
|
-
|
|
1060
|
+
# Public dup contract: a full deep copy of the subtree
|
|
1061
|
+
deep_duplicate_node(native_node)
|
|
1065
1062
|
when :text
|
|
1066
1063
|
::LibXML::XML::Node.new_text(native_node.content)
|
|
1067
1064
|
when :cdata
|
|
@@ -1106,15 +1103,6 @@ module Moxml
|
|
|
1106
1103
|
end
|
|
1107
1104
|
end
|
|
1108
1105
|
|
|
1109
|
-
def prepare_for_new_document(node, target_doc)
|
|
1110
|
-
return node unless node && target_doc
|
|
1111
|
-
|
|
1112
|
-
# For LibXML, we need to duplicate ALL nodes to avoid
|
|
1113
|
-
# document ownership issues. Simply importing doesn't work
|
|
1114
|
-
# because nodes from the parsed document still have references.
|
|
1115
|
-
duplicate_node(node)
|
|
1116
|
-
end
|
|
1117
|
-
|
|
1118
1106
|
def has_declaration?(native_doc, wrapper)
|
|
1119
1107
|
decl = attachments.get(native_doc, :declaration)
|
|
1120
1108
|
if decl
|
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -40,8 +40,23 @@ module Moxml
|
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Oga parses multiple root elements leniently; the moxml
|
|
44
|
+
# contract requires a single root. to_a because the vendored
|
|
45
|
+
# fork's NodeSet#count ignores its block.
|
|
46
|
+
roots = native_doc.children.to_a.count do |child|
|
|
47
|
+
child.instance_of?(::Oga::XML::Element)
|
|
48
|
+
end
|
|
49
|
+
if roots > 1
|
|
50
|
+
raise Moxml::ParseError.new(
|
|
51
|
+
"multiple root elements",
|
|
52
|
+
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
ctx = _context || Context.new(:oga)
|
|
44
|
-
|
|
57
|
+
doc = Document.new(native_doc, ctx)
|
|
58
|
+
Entity::Restorer.new(doc).run if ctx.config.restore_entities
|
|
59
|
+
doc
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
# SAX parsing implementation for Oga.
|
|
@@ -239,6 +254,38 @@ module Moxml
|
|
|
239
254
|
node.parent if node.is_a?(::Oga::XML::Node)
|
|
240
255
|
end
|
|
241
256
|
|
|
257
|
+
# Oga's Node#dup shallow-copies the ivars, leaving attributes
|
|
258
|
+
# and children shared with the original; the public dup
|
|
259
|
+
# contract needs a full deep copy.
|
|
260
|
+
def duplicate_node(node)
|
|
261
|
+
case node
|
|
262
|
+
when ::Oga::XML::Element
|
|
263
|
+
copy = ::Oga::XML::Element.new(name: node.name)
|
|
264
|
+
copy.namespace_name = node.namespace_name if node.namespace_name
|
|
265
|
+
node.attributes.each do |attr|
|
|
266
|
+
copy.add_attribute(::Oga::XML::Attribute.new(
|
|
267
|
+
name: attr.name,
|
|
268
|
+
namespace_name: attr.namespace_name,
|
|
269
|
+
value: attr.value,
|
|
270
|
+
))
|
|
271
|
+
end
|
|
272
|
+
node.children.each do |child|
|
|
273
|
+
copy.children << duplicate_node(child)
|
|
274
|
+
end
|
|
275
|
+
copy
|
|
276
|
+
when ::Oga::XML::Text
|
|
277
|
+
::Oga::XML::Text.new(text: node.text)
|
|
278
|
+
when ::Oga::XML::Cdata
|
|
279
|
+
::Oga::XML::Cdata.new(text: node.text)
|
|
280
|
+
when ::Oga::XML::Comment
|
|
281
|
+
::Oga::XML::Comment.new(text: node.text)
|
|
282
|
+
when ::Oga::XML::ProcessingInstruction
|
|
283
|
+
::Oga::XML::ProcessingInstruction.new(name: node.name, text: node.text)
|
|
284
|
+
else
|
|
285
|
+
node.dup
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
242
289
|
def next_sibling(node)
|
|
243
290
|
node.next
|
|
244
291
|
end
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -41,7 +41,9 @@ module Moxml
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
ctx = _context || Context.new(:rexml)
|
|
44
|
-
|
|
44
|
+
doc = Document.new(native_doc, ctx)
|
|
45
|
+
Entity::Restorer.new(doc).run if ctx.config.restore_entities
|
|
46
|
+
doc
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def extract_encoding_from_xml(xml)
|
data/lib/moxml/builder.rb
CHANGED
|
@@ -75,24 +75,6 @@ module Moxml
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Batch element creation
|
|
78
|
-
def elements(element_specs)
|
|
79
|
-
element_specs.each do |name, content_or_attrs|
|
|
80
|
-
if content_or_attrs.is_a?(Hash)
|
|
81
|
-
element(name, content_or_attrs)
|
|
82
|
-
else
|
|
83
|
-
element(name) { text(content_or_attrs) }
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Helper for creating namespaced elements
|
|
89
|
-
def ns_element(namespace_uri, name, attributes = {}, &block)
|
|
90
|
-
el = element(name, attributes, &block)
|
|
91
|
-
prefix = @namespaces.key(namespace_uri)
|
|
92
|
-
el.namespace = { prefix => namespace_uri } if prefix
|
|
93
|
-
el
|
|
94
|
-
end
|
|
95
|
-
|
|
96
78
|
# Dynamic element creation DSL.
|
|
97
79
|
# xml.schema(attrs) { } creates <schema> with those attributes.
|
|
98
80
|
# Uses yield so blocks preserve the caller's self context.
|
data/lib/moxml/config.rb
CHANGED
|
@@ -78,7 +78,6 @@ module Moxml
|
|
|
78
78
|
attr_reader :adapter_name, :default_line_ending
|
|
79
79
|
attr_accessor :strict_parsing,
|
|
80
80
|
:default_encoding,
|
|
81
|
-
:entity_encoding,
|
|
82
81
|
:default_indent,
|
|
83
82
|
:restore_entities,
|
|
84
83
|
:preload_entity_sets,
|
|
@@ -104,7 +103,6 @@ module Moxml
|
|
|
104
103
|
@default_encoding = default_encoding || Config.default.default_encoding
|
|
105
104
|
@default_indent = 2
|
|
106
105
|
@default_line_ending = LINE_ENDING_LF
|
|
107
|
-
@entity_encoding = :basic
|
|
108
106
|
@restore_entities = false
|
|
109
107
|
@preload_entity_sets = []
|
|
110
108
|
@entity_load_mode = :required
|
data/lib/moxml/context.rb
CHANGED
|
@@ -10,6 +10,27 @@ module Moxml
|
|
|
10
10
|
# compare against it so every wrapper sees changes without
|
|
11
11
|
# needing wrapper identity.
|
|
12
12
|
@namespace_scope_generation = 0
|
|
13
|
+
# Native → wrapper identity map: repeated traversals hand back
|
|
14
|
+
# the same wrapper instead of allocating a fresh one per
|
|
15
|
+
# access. Keyed by object identity; re-keyed by
|
|
16
|
+
# Node#refresh_native! when an adapter swaps a native.
|
|
17
|
+
@wrappers = {}.compare_by_identity
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def wrapper_for(native)
|
|
21
|
+
@wrappers[native]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def register_wrapper(native, wrapper)
|
|
25
|
+
# Safety valve: adapters whose natives are recreated per access
|
|
26
|
+
# (nokogiri mints new Ruby objects for the same C node) would
|
|
27
|
+
# grow the map without bound in pathological loops.
|
|
28
|
+
@wrappers.clear if @wrappers.size >= 65_536
|
|
29
|
+
@wrappers[native] = wrapper
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unregister_wrapper(native)
|
|
33
|
+
@wrappers.delete(native)
|
|
13
34
|
end
|
|
14
35
|
|
|
15
36
|
def namespace_scope_generation
|
data/lib/moxml/document.rb
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Entity
|
|
5
|
+
# Restores configured character entities into explicit
|
|
6
|
+
# Moxml::Entity::Reference nodes after a native tree has been
|
|
7
|
+
# parsed and wrapped — for adapters whose parser resolves entities
|
|
8
|
+
# during parsing (libxml, and the restore_entities option on
|
|
9
|
+
# rexml/oga).
|
|
10
|
+
class Restorer
|
|
11
|
+
def initialize(doc)
|
|
12
|
+
@doc = doc
|
|
13
|
+
@ctx = doc.context
|
|
14
|
+
@registry = @ctx.entity_registry
|
|
15
|
+
@config = @ctx.config
|
|
16
|
+
@adapter = @ctx.config.adapter
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
return unless @registry && @doc.root
|
|
21
|
+
|
|
22
|
+
walk(@doc.root)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def walk(element)
|
|
28
|
+
# Snapshot because we may add/remove siblings during the walk.
|
|
29
|
+
element.children.to_a.each do |child|
|
|
30
|
+
if child.is_a?(::Moxml::Text)
|
|
31
|
+
restore_text_node(child)
|
|
32
|
+
elsif child.is_a?(::Moxml::Element)
|
|
33
|
+
walk(child)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Includes the adapter limitation that adjacent native text
|
|
39
|
+
# nodes get merged.
|
|
40
|
+
def restore_text_node(text_node)
|
|
41
|
+
content = text_node.content
|
|
42
|
+
return unless content
|
|
43
|
+
|
|
44
|
+
chunks = chunk_text(content)
|
|
45
|
+
return if chunks.size == 1 && chunks.first.first == :text
|
|
46
|
+
|
|
47
|
+
parent = text_node.parent
|
|
48
|
+
return unless parent
|
|
49
|
+
|
|
50
|
+
text_node.remove
|
|
51
|
+
chunks.each { |type, payload| append_chunk(parent, type, payload) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def chunk_text(content)
|
|
55
|
+
chunks = []
|
|
56
|
+
buffer = +""
|
|
57
|
+
restorable = @registry.restorable_codepoints
|
|
58
|
+
|
|
59
|
+
content.each_char do |char|
|
|
60
|
+
cp = char.ord
|
|
61
|
+
if restorable.include?(cp) &&
|
|
62
|
+
(name = @registry.primary_name_for_codepoint(cp)) &&
|
|
63
|
+
@registry.should_restore?(cp, config: @config)
|
|
64
|
+
unless buffer.empty?
|
|
65
|
+
chunks << [:text, buffer.dup]
|
|
66
|
+
buffer.clear
|
|
67
|
+
end
|
|
68
|
+
chunks << [:eref, name]
|
|
69
|
+
else
|
|
70
|
+
buffer << char
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
chunks << [:text, buffer.dup] unless buffer.empty?
|
|
75
|
+
chunks
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def append_chunk(parent, type, payload)
|
|
79
|
+
case type
|
|
80
|
+
when :text
|
|
81
|
+
parent.add_child(::Moxml::Text.new(
|
|
82
|
+
@adapter.create_native_text(payload), @ctx
|
|
83
|
+
))
|
|
84
|
+
when :eref
|
|
85
|
+
parent.add_child(
|
|
86
|
+
::Moxml::EntityReference.new(
|
|
87
|
+
@adapter.create_native_entity_reference(payload),
|
|
88
|
+
@ctx,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/moxml/entity.rb
CHANGED
|
@@ -29,6 +29,7 @@ module Moxml
|
|
|
29
29
|
# shared, single-sourced parts.
|
|
30
30
|
module Entity
|
|
31
31
|
autoload :Reference, "moxml/entity/reference"
|
|
32
|
+
autoload :Restorer, "moxml/entity/restorer"
|
|
32
33
|
|
|
33
34
|
# Marker for adapters that resolve entities during parsing.
|
|
34
35
|
# U+FFFC (Object Replacement Character) + U+FEFF (BOM) is a
|
|
@@ -199,7 +199,7 @@ module Moxml
|
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
# Returns the set of codepoints that could potentially be restored as entities.
|
|
202
|
-
# Used by
|
|
202
|
+
# Used by Entity::Restorer for O(1) fast-path checks.
|
|
203
203
|
# @return [Set<Integer>]
|
|
204
204
|
def restorable_codepoints
|
|
205
205
|
@restorable_codepoints ||= if @by_name.empty?
|
data/lib/moxml/error.rb
CHANGED
|
@@ -106,27 +106,6 @@ module Moxml
|
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
# Error raised when serialization fails
|
|
110
|
-
class SerializationError < Error
|
|
111
|
-
attr_reader :node, :adapter, :format
|
|
112
|
-
|
|
113
|
-
def initialize(message, node: nil, adapter: nil, format: nil)
|
|
114
|
-
@node = node
|
|
115
|
-
@adapter = adapter
|
|
116
|
-
@format = format
|
|
117
|
-
super(message)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def to_s
|
|
121
|
-
msg = super
|
|
122
|
-
msg += "\n Node: <#{@node.name}>" if @node.is_a?(Element) || @node.is_a?(Attribute)
|
|
123
|
-
msg += "\n Adapter: #{@adapter}" if @adapter
|
|
124
|
-
msg += "\n Format: #{@format}" if @format
|
|
125
|
-
msg += "\n Hint: Check that the node structure is valid for serialization"
|
|
126
|
-
msg
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
109
|
# Error raised when document structure is invalid
|
|
131
110
|
class DocumentStructureError < Error
|
|
132
111
|
attr_reader :attempted_operation, :current_state
|
|
@@ -146,27 +125,6 @@ module Moxml
|
|
|
146
125
|
end
|
|
147
126
|
end
|
|
148
127
|
|
|
149
|
-
# Error raised when attribute operations fail
|
|
150
|
-
class AttributeError < Error
|
|
151
|
-
attr_reader :attribute_name, :element, :value
|
|
152
|
-
|
|
153
|
-
def initialize(message, name: nil, element: nil, value: nil)
|
|
154
|
-
@attribute_name = name
|
|
155
|
-
@element = element
|
|
156
|
-
@value = value
|
|
157
|
-
super(message)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def to_s
|
|
161
|
-
msg = super
|
|
162
|
-
msg += "\n Attribute: #{@attribute_name}" if @attribute_name
|
|
163
|
-
msg += "\n Element: <#{@element.name}>" if @element.is_a?(Element)
|
|
164
|
-
msg += "\n Value: #{@value.inspect}" if @value
|
|
165
|
-
msg += "\n Hint: Verify attribute name follows XML naming rules"
|
|
166
|
-
msg
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
128
|
# Error raised when a feature is not implemented by an adapter
|
|
171
129
|
class NotImplementedError < Error
|
|
172
130
|
attr_reader :feature, :adapter
|
data/lib/moxml/node.rb
CHANGED
|
@@ -21,7 +21,12 @@ module Moxml
|
|
|
21
21
|
# Update native reference after identity-changing operations
|
|
22
22
|
# (e.g., LibXML doc.root= creates a new Ruby wrapper)
|
|
23
23
|
def refresh_native!(new_native)
|
|
24
|
-
@native
|
|
24
|
+
unless new_native.equal?(@native)
|
|
25
|
+
context.unregister_wrapper(@native)
|
|
26
|
+
@native = new_native
|
|
27
|
+
context.register_wrapper(new_native, self)
|
|
28
|
+
end
|
|
29
|
+
self
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def document
|
|
@@ -225,6 +230,41 @@ module Moxml
|
|
|
225
230
|
NodeSet.new(natives, context)
|
|
226
231
|
end
|
|
227
232
|
|
|
233
|
+
# Returns the siblings after this node, in document order.
|
|
234
|
+
#
|
|
235
|
+
# @return [NodeSet]
|
|
236
|
+
def following_siblings
|
|
237
|
+
parent = self.parent
|
|
238
|
+
return NodeSet.new([], context) unless parent
|
|
239
|
+
|
|
240
|
+
siblings = parent.children.to_a
|
|
241
|
+
index = siblings.index { |child| child.native.equal?(@native) }
|
|
242
|
+
return NodeSet.new([], context) if index.nil?
|
|
243
|
+
|
|
244
|
+
NodeSet.new(siblings[(index + 1)..].map(&:native), context)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Returns the siblings before this node, in document order.
|
|
248
|
+
#
|
|
249
|
+
# @return [NodeSet]
|
|
250
|
+
def preceding_siblings
|
|
251
|
+
parent = self.parent
|
|
252
|
+
return NodeSet.new([], context) unless parent
|
|
253
|
+
|
|
254
|
+
siblings = parent.children.to_a
|
|
255
|
+
index = siblings.index { |child| child.native.equal?(@native) }
|
|
256
|
+
return NodeSet.new([], context) if index.nil?
|
|
257
|
+
|
|
258
|
+
NodeSet.new(siblings[0...index].map(&:native), context)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Deep copy of the node (both dup and clone create deep copies for XML nodes)
|
|
262
|
+
def dup
|
|
263
|
+
Moxml::Node.wrap(adapter.duplicate_node(@native), context)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
alias clone dup
|
|
267
|
+
|
|
228
268
|
# Returns an XPath expression that uniquely locates this node within
|
|
229
269
|
# its document. Positional predicates are emitted only when sibling
|
|
230
270
|
# elements share the same qualified name, keeping paths minimal.
|
|
@@ -276,13 +316,6 @@ module Moxml
|
|
|
276
316
|
text.strip.empty?
|
|
277
317
|
end
|
|
278
318
|
|
|
279
|
-
# Deep copy of the node (both dup and clone create deep copies for XML nodes)
|
|
280
|
-
def dup
|
|
281
|
-
Moxml::Node.wrap(adapter.dup(@native), context)
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
alias clone dup
|
|
285
|
-
|
|
286
319
|
def ==(other)
|
|
287
320
|
self.class == other.class && @native == other.native
|
|
288
321
|
end
|
|
@@ -325,10 +358,13 @@ module Moxml
|
|
|
325
358
|
def self.wrap(node, context)
|
|
326
359
|
return nil if node.nil?
|
|
327
360
|
|
|
361
|
+
cached = context.wrapper_for(node)
|
|
362
|
+
return cached if cached
|
|
363
|
+
|
|
328
364
|
type = adapter(context).node_type(node)
|
|
329
365
|
klass = node_type_map[type] || self
|
|
330
366
|
|
|
331
|
-
klass.new(node, context)
|
|
367
|
+
klass.new(node, context).tap { |wrapper| context.register_wrapper(node, wrapper) }
|
|
332
368
|
end
|
|
333
369
|
|
|
334
370
|
# Internal: Set the parent node for cache invalidation tracking.
|
data/lib/moxml/node_set.rb
CHANGED
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -294,6 +294,12 @@ module Moxml
|
|
|
294
294
|
|
|
295
295
|
handler = axis_name.gsub("-", "_")
|
|
296
296
|
|
|
297
|
+
unless respond_to?(:"on_axis_#{handler}", true)
|
|
298
|
+
raise XPath::Error.new(
|
|
299
|
+
"XPath axis #{axis_name.inspect} is not supported by this engine",
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
|
|
297
303
|
send(:"on_axis_#{handler}", test, input, &block)
|
|
298
304
|
end
|
|
299
305
|
|
|
@@ -362,6 +368,78 @@ module Moxml
|
|
|
362
368
|
end
|
|
363
369
|
end
|
|
364
370
|
|
|
371
|
+
# AXIS: ancestor - all ancestors up to the document
|
|
372
|
+
def on_axis_ancestor(ast, input)
|
|
373
|
+
ancestor = unique_literal(:ancestor)
|
|
374
|
+
|
|
375
|
+
document_or_node(input).if_true do
|
|
376
|
+
input.ancestors.each.add_block(ancestor) do
|
|
377
|
+
condition = process(ast, ancestor)
|
|
378
|
+
if block_given?
|
|
379
|
+
condition.if_true { yield ancestor }
|
|
380
|
+
else
|
|
381
|
+
condition.if_true { ancestor }
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# AXIS: ancestor-or-self - the context node and its ancestors
|
|
388
|
+
def on_axis_ancestor_or_self(ast, input)
|
|
389
|
+
ancestor = unique_literal(:ancestor)
|
|
390
|
+
|
|
391
|
+
document_or_node(input).if_true do
|
|
392
|
+
self_result = process(ast, input)
|
|
393
|
+
self_collection = if block_given?
|
|
394
|
+
self_result.if_true { yield input }
|
|
395
|
+
else
|
|
396
|
+
self_result.if_true { input }
|
|
397
|
+
end
|
|
398
|
+
self_collection.followed_by do
|
|
399
|
+
input.ancestors.each.add_block(ancestor) do
|
|
400
|
+
condition = process(ast, ancestor)
|
|
401
|
+
if block_given?
|
|
402
|
+
condition.if_true { yield ancestor }
|
|
403
|
+
else
|
|
404
|
+
condition.if_true { ancestor }
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# AXIS: following-sibling - siblings after the context node
|
|
412
|
+
def on_axis_following_sibling(ast, input)
|
|
413
|
+
sibling = unique_literal(:sibling)
|
|
414
|
+
|
|
415
|
+
document_or_node(input).if_true do
|
|
416
|
+
input.following_siblings.each.add_block(sibling) do
|
|
417
|
+
condition = process(ast, sibling)
|
|
418
|
+
if block_given?
|
|
419
|
+
condition.if_true { yield sibling }
|
|
420
|
+
else
|
|
421
|
+
condition.if_true { sibling }
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# AXIS: preceding-sibling - siblings before the context node
|
|
428
|
+
def on_axis_preceding_sibling(ast, input)
|
|
429
|
+
sibling = unique_literal(:sibling)
|
|
430
|
+
|
|
431
|
+
document_or_node(input).if_true do
|
|
432
|
+
input.preceding_siblings.each.add_block(sibling) do
|
|
433
|
+
condition = process(ast, sibling)
|
|
434
|
+
if block_given?
|
|
435
|
+
condition.if_true { yield sibling }
|
|
436
|
+
else
|
|
437
|
+
condition.if_true { sibling }
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
365
443
|
# AXIS: descendant-or-self - Enables // operator
|
|
366
444
|
def on_axis_descendant_or_self(ast, input)
|
|
367
445
|
node = unique_literal(:descendant)
|
|
@@ -623,7 +701,11 @@ module Moxml
|
|
|
623
701
|
|
|
624
702
|
# Parent node (..)
|
|
625
703
|
def on_parent(_ast, input)
|
|
626
|
-
|
|
704
|
+
if block_given?
|
|
705
|
+
yield input.parent
|
|
706
|
+
else
|
|
707
|
+
input.parent
|
|
708
|
+
end
|
|
627
709
|
end
|
|
628
710
|
|
|
629
711
|
# ===== OPERATORS =====
|
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
|
|
@@ -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.3.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
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
|
|
5
|
-
# DocumentBuilder is an internal class for parsing
|
|
6
|
-
# These tests verify it correctly builds Moxml document from native documents
|
|
7
|
-
RSpec.describe Moxml::DocumentBuilder do
|
|
8
|
-
let(:context) { Moxml.new }
|
|
9
|
-
|
|
10
|
-
describe "#build" do
|
|
11
|
-
it "builds a Moxml document from native document" do
|
|
12
|
-
native_doc = context.parse("<root><child>content</child></root>").native
|
|
13
|
-
builder = described_class.new(context)
|
|
14
|
-
doc = builder.build(native_doc)
|
|
15
|
-
|
|
16
|
-
expect(doc).to be_a(Moxml::Document)
|
|
17
|
-
expect(doc.root.name).to eq("root")
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
describe "element handling" do
|
|
22
|
-
it "handles nested elements" do
|
|
23
|
-
native_doc = context.parse("<parent><child1/><child2/></parent>").native
|
|
24
|
-
builder = described_class.new(context)
|
|
25
|
-
doc = builder.build(native_doc)
|
|
26
|
-
|
|
27
|
-
expect(doc.root.children.length).to eq(2)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|