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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2a6cac576f8bc4819d27a22825f7ab11fd35a004113c70d6c4986adf542d844
|
|
4
|
+
data.tar.gz: 8f7cbfa869296d65002a6d2b74399ede57b16c2bcc25f4c4c7e9ccab611f5be6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5b6bf4775a56f4d4fd18cea369d84463085c4df14bdef0ee0ad818ff734864e658bfc41c81bf0e6989cdff2b4ffdff69c74bfc512a7b2debecadd7a892432b6
|
|
7
|
+
data.tar.gz: daf1b85571460bbde0553a52603f255577918dd9eb2e9faf37cb156738277bef51d5bfadc207b74d17177d3f281d9ae4ce4b2db571e22ad112bb595291d124c8
|
|
@@ -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
|
|
@@ -222,6 +215,16 @@ namespace_validation_mode: :strict)
|
|
|
222
215
|
child_native
|
|
223
216
|
end
|
|
224
217
|
|
|
218
|
+
# Whether `Node.wrap` may safely memoize the wrapper for this
|
|
219
|
+
# native across calls. Adapters whose parser hands back the
|
|
220
|
+
# same Ruby object for the same logical node (nokogiri, ox,
|
|
221
|
+
# oga, rexml, leptris) opt in (default true). Adapters that
|
|
222
|
+
# mint a new Ruby object per access (libxml) opt out so the
|
|
223
|
+
# identity map does not accumulate dead entries.
|
|
224
|
+
def wrappers_recyclable?
|
|
225
|
+
true
|
|
226
|
+
end
|
|
227
|
+
|
|
225
228
|
# Returns all namespaces in scope for this element, including
|
|
226
229
|
# inherited from ancestors. Adapters with native support (Nokogiri)
|
|
227
230
|
# override this. Default walks the ancestor chain.
|
|
@@ -559,8 +559,16 @@ module Moxml
|
|
|
559
559
|
# @return [Array, Object, nil] native results, or nil when the
|
|
560
560
|
# query must run on the Ruby engine
|
|
561
561
|
def native_xpath(node, expression, namespaces, first_only: false)
|
|
562
|
-
return nil unless
|
|
562
|
+
return nil unless native_context_node?(node)
|
|
563
563
|
return nil unless native_expression?(expression)
|
|
564
|
+
# The binding reports the root element parentless while moxml
|
|
565
|
+
# roots it at the document, so parent-axis queries from the
|
|
566
|
+
# root element keep the Ruby engine.
|
|
567
|
+
if node.is_a?(::Leptris::XML::Element) &&
|
|
568
|
+
node.document&.root.equal?(node) &&
|
|
569
|
+
expression_uses_parent_axis?(expression)
|
|
570
|
+
return nil
|
|
571
|
+
end
|
|
564
572
|
|
|
565
573
|
compiled = NATIVE_XPATH_CACHE.get_or_set(expression) do
|
|
566
574
|
::Leptris::XML::XPath.compile(expression)
|
|
@@ -584,6 +592,27 @@ module Moxml
|
|
|
584
592
|
nil
|
|
585
593
|
end
|
|
586
594
|
|
|
595
|
+
def native_context_node?(node)
|
|
596
|
+
node.is_a?(::Leptris::XML::Document) ||
|
|
597
|
+
node.is_a?(::Leptris::XML::Element)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def expression_uses_parent_axis?(expression)
|
|
601
|
+
ast = XPath::Parser.parse_with_cache(expression)
|
|
602
|
+
contains_parent_axis?(ast)
|
|
603
|
+
rescue XPath::SyntaxError
|
|
604
|
+
true
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def contains_parent_axis?(ast)
|
|
608
|
+
return true if ast.type == :parent
|
|
609
|
+
return true if ast.type == :axis && ast.children.first == "parent"
|
|
610
|
+
|
|
611
|
+
ast.children.any? do |child|
|
|
612
|
+
child.is_a?(XPath::AST::Node) && contains_parent_axis?(child)
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
587
616
|
# Document-context queries without variable references,
|
|
588
617
|
# attribute-node results, or the nokogiri-compat xmlns:
|
|
589
618
|
# reserved prefix convention.
|
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
|
|
@@ -68,6 +67,13 @@ module Moxml
|
|
|
68
67
|
doc.root = element
|
|
69
68
|
end
|
|
70
69
|
|
|
70
|
+
# libxml-ruby mints a fresh Ruby object for the same C node on
|
|
71
|
+
# each access; the wrapper identity map would accumulate dead
|
|
72
|
+
# entries without bound at full mutation throughput.
|
|
73
|
+
def wrappers_recyclable?
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
71
77
|
def parse(xml, options = {}, _context = nil)
|
|
72
78
|
# LibXML doesn't preserve DOCTYPE during parsing, so we need to extract it manually
|
|
73
79
|
xml_string = if xml.is_a?(String)
|
|
@@ -121,10 +127,7 @@ module Moxml
|
|
|
121
127
|
|
|
122
128
|
ctx = _context || Context.new(:libxml)
|
|
123
129
|
# 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.
|
|
130
|
+
# directly (same pattern as the other adapters).
|
|
128
131
|
# Doctype/declaration/PI attachments set above remain on
|
|
129
132
|
# native_doc, so the serialize path still sees them.
|
|
130
133
|
#
|
|
@@ -134,7 +137,7 @@ module Moxml
|
|
|
134
137
|
# path; the restoration walk is just one of potentially several
|
|
135
138
|
# post-processing steps and doesn't fork the construction.
|
|
136
139
|
doc = Document.new(native_doc, ctx)
|
|
137
|
-
|
|
140
|
+
Entity::Restorer.new(doc).run if ctx.config.restore_entities
|
|
138
141
|
doc
|
|
139
142
|
end
|
|
140
143
|
|
|
@@ -219,7 +222,7 @@ module Moxml
|
|
|
219
222
|
return :unknown unless node
|
|
220
223
|
|
|
221
224
|
# Fast path: native libxml nodes are the vast majority during
|
|
222
|
-
# parse traversal (
|
|
225
|
+
# parse traversal (wrapping visits raw libxml children).
|
|
223
226
|
# Skip the wrapper checks below for them.
|
|
224
227
|
if node.is_a?(::LibXML::XML::Node)
|
|
225
228
|
return NATIVE_NODE_TYPE_MAP[node.node_type] || :unknown
|
|
@@ -1010,18 +1013,17 @@ module Moxml
|
|
|
1010
1013
|
|
|
1011
1014
|
if native_node.root
|
|
1012
1015
|
indent_size = options[:indent].is_a?(Integer) && options[:indent].positive? ? options[:indent] : 0
|
|
1013
|
-
# Custom serializer emits newlines AND indentation directly —
|
|
1014
|
-
# no separate add_newlines_to_xml / indent_xml passes.
|
|
1015
1016
|
# `eref_active` is computed once here and threaded through the
|
|
1016
1017
|
# recursion so that the per-element `attachments.key?` Monitor
|
|
1017
1018
|
# sync only fires for docs that actually have entity refs.
|
|
1018
1019
|
eref_active = entity_ref_registry(native_node).active?
|
|
1019
|
-
root_output =
|
|
1020
|
+
root_output = native_root_output(native_node, indent_size)
|
|
1021
|
+
root_output ||= serialize_element_with_namespaces(
|
|
1020
1022
|
native_node.root,
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1023
|
+
true,
|
|
1024
|
+
indent_size,
|
|
1025
|
+
0,
|
|
1026
|
+
eref_active: eref_active,
|
|
1025
1027
|
)
|
|
1026
1028
|
|
|
1027
1029
|
output << "\n" << root_output unless output.empty?
|
|
@@ -1034,8 +1036,48 @@ module Moxml
|
|
|
1034
1036
|
end
|
|
1035
1037
|
end
|
|
1036
1038
|
|
|
1039
|
+
# Possessive-quantifier form: no backtracking, so pathological
|
|
1040
|
+
# attribute content cannot blow up the expansion pass.
|
|
1041
|
+
EMPTY_ELEMENT_EXPANSION_RE = %r{
|
|
1042
|
+
<([A-Za-z_][\w.:-]*+)
|
|
1043
|
+
((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)
|
|
1044
|
+
/>
|
|
1045
|
+
}x
|
|
1046
|
+
private_constant :EMPTY_ELEMENT_EXPANSION_RE
|
|
1047
|
+
|
|
1048
|
+
# Serialize the root subtree with libxml's C serializer plus
|
|
1049
|
+
# moxml-canonical corrections — roughly 25x faster and ~2600x
|
|
1050
|
+
# fewer allocations than the Ruby walker. Returns nil whenever
|
|
1051
|
+
# a guard says the walker is still required.
|
|
1052
|
+
def native_root_output(native_doc, indent_size)
|
|
1053
|
+
return nil unless indent_size == 2
|
|
1054
|
+
return nil if entity_ref_registry(native_doc).active?
|
|
1055
|
+
|
|
1056
|
+
output = native_doc.root.to_s
|
|
1057
|
+
|
|
1058
|
+
# The walker strips prefixed names on parsed namespaced
|
|
1059
|
+
# children; that behavior is load-bearing, so namespaced
|
|
1060
|
+
# output keeps the walker.
|
|
1061
|
+
return nil if output.include?("xmlns")
|
|
1062
|
+
|
|
1063
|
+
# Layout: pull closing tags onto the last child's line
|
|
1064
|
+
output = output.gsub(/\n[ \t]*(<\/[\w.:-]+>)/, '\1')
|
|
1065
|
+
|
|
1066
|
+
if output.include?("/>")
|
|
1067
|
+
# A literal "/>" inside a comment or CDATA section would be
|
|
1068
|
+
# falsely expanded — the segment-aware walker is correct here
|
|
1069
|
+
return nil if output.include?("<!--") || output.include?("<![CDATA[")
|
|
1070
|
+
|
|
1071
|
+
output = output.gsub(EMPTY_ELEMENT_EXPANSION_RE, '<\1\2></\1>')
|
|
1072
|
+
end
|
|
1073
|
+
|
|
1074
|
+
# Native escapes attribute apostrophes; moxml keeps them literal
|
|
1075
|
+
output.gsub("'", "'")
|
|
1076
|
+
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1037
1079
|
# Shallow duplication: copies the node itself (name, attrs, namespaces)
|
|
1038
|
-
# but NOT its descendants
|
|
1080
|
+
# but NOT its descendants (deep duplication composes this).
|
|
1039
1081
|
# walks the source tree and re-adds children one at a time via
|
|
1040
1082
|
# add_child, so a deep copy here would be done only to be stripped
|
|
1041
1083
|
# by replace_children, then rebuilt — O(N²) waste on parse.
|
|
@@ -1061,7 +1103,8 @@ module Moxml
|
|
|
1061
1103
|
node
|
|
1062
1104
|
end
|
|
1063
1105
|
when :element
|
|
1064
|
-
|
|
1106
|
+
# Public dup contract: a full deep copy of the subtree
|
|
1107
|
+
deep_duplicate_node(native_node)
|
|
1065
1108
|
when :text
|
|
1066
1109
|
::LibXML::XML::Node.new_text(native_node.content)
|
|
1067
1110
|
when :cdata
|
|
@@ -1106,15 +1149,6 @@ module Moxml
|
|
|
1106
1149
|
end
|
|
1107
1150
|
end
|
|
1108
1151
|
|
|
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
1152
|
def has_declaration?(native_doc, wrapper)
|
|
1119
1153
|
decl = attachments.get(native_doc, :declaration)
|
|
1120
1154
|
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,30 @@ 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 + adapter opt-in. Adapters whose natives are
|
|
26
|
+
# recreated per access (libxml mints fresh Ruby objects for the
|
|
27
|
+
# same C node) opt out so the map does not accumulate dead
|
|
28
|
+
# entries; the default is opt-in.
|
|
29
|
+
return if @config&.adapter&.wrappers_recyclable? == false
|
|
30
|
+
|
|
31
|
+
@wrappers.clear if @wrappers.size >= 65_536
|
|
32
|
+
@wrappers[native] = wrapper
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unregister_wrapper(native)
|
|
36
|
+
@wrappers.delete(native)
|
|
13
37
|
end
|
|
14
38
|
|
|
15
39
|
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.
|