moxml 0.3.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/moxml/adapter/base.rb +10 -0
- data/lib/moxml/adapter/leptris.rb +30 -1
- data/lib/moxml/adapter/libxml.rb +53 -7
- data/lib/moxml/context.rb +6 -3
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +2 -1
- data/spec/moxml/adapter/leptris_spec.rb +9 -1
- metadata +1 -1
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
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -215,6 +215,16 @@ namespace_validation_mode: :strict)
|
|
|
215
215
|
child_native
|
|
216
216
|
end
|
|
217
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
|
+
|
|
218
228
|
# Returns all namespaces in scope for this element, including
|
|
219
229
|
# inherited from ancestors. Adapters with native support (Nokogiri)
|
|
220
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
|
@@ -67,6 +67,13 @@ module Moxml
|
|
|
67
67
|
doc.root = element
|
|
68
68
|
end
|
|
69
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
|
+
|
|
70
77
|
def parse(xml, options = {}, _context = nil)
|
|
71
78
|
# LibXML doesn't preserve DOCTYPE during parsing, so we need to extract it manually
|
|
72
79
|
xml_string = if xml.is_a?(String)
|
|
@@ -1006,18 +1013,17 @@ module Moxml
|
|
|
1006
1013
|
|
|
1007
1014
|
if native_node.root
|
|
1008
1015
|
indent_size = options[:indent].is_a?(Integer) && options[:indent].positive? ? options[:indent] : 0
|
|
1009
|
-
# Custom serializer emits newlines AND indentation directly —
|
|
1010
|
-
# no separate add_newlines_to_xml / indent_xml passes.
|
|
1011
1016
|
# `eref_active` is computed once here and threaded through the
|
|
1012
1017
|
# recursion so that the per-element `attachments.key?` Monitor
|
|
1013
1018
|
# sync only fires for docs that actually have entity refs.
|
|
1014
1019
|
eref_active = entity_ref_registry(native_node).active?
|
|
1015
|
-
root_output =
|
|
1020
|
+
root_output = native_root_output(native_node, indent_size)
|
|
1021
|
+
root_output ||= serialize_element_with_namespaces(
|
|
1016
1022
|
native_node.root,
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1023
|
+
true,
|
|
1024
|
+
indent_size,
|
|
1025
|
+
0,
|
|
1026
|
+
eref_active: eref_active,
|
|
1021
1027
|
)
|
|
1022
1028
|
|
|
1023
1029
|
output << "\n" << root_output unless output.empty?
|
|
@@ -1030,6 +1036,46 @@ module Moxml
|
|
|
1030
1036
|
end
|
|
1031
1037
|
end
|
|
1032
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
|
+
|
|
1033
1079
|
# Shallow duplication: copies the node itself (name, attrs, namespaces)
|
|
1034
1080
|
# but NOT its descendants (deep duplication composes this).
|
|
1035
1081
|
# walks the source tree and re-adds children one at a time via
|
data/lib/moxml/context.rb
CHANGED
|
@@ -22,9 +22,12 @@ module Moxml
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def register_wrapper(native, wrapper)
|
|
25
|
-
# Safety valve
|
|
26
|
-
# (
|
|
27
|
-
#
|
|
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
|
+
|
|
28
31
|
@wrappers.clear if @wrappers.size >= 65_536
|
|
29
32
|
@wrappers[native] = wrapper
|
|
30
33
|
end
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Moxml
|
|
|
22
22
|
class Compiler
|
|
23
23
|
# Shared context for compiled Procs
|
|
24
24
|
CONTEXT = Context.new
|
|
25
|
+
private_constant :CONTEXT
|
|
25
26
|
|
|
26
27
|
# Expression cache
|
|
27
28
|
CACHE = Cache.new
|
|
@@ -1834,7 +1835,7 @@ module Moxml
|
|
|
1834
1835
|
|
|
1835
1836
|
# Helper: Check if AST node is a number
|
|
1836
1837
|
def number?(ast)
|
|
1837
|
-
|
|
1838
|
+
ast.type == :number
|
|
1838
1839
|
end
|
|
1839
1840
|
|
|
1840
1841
|
# Helper: Check if AST contains a call node with given name
|
|
@@ -58,8 +58,16 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
58
58
|
expect(attrs.map(&:value)).to eq(%w[1 2])
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
it "
|
|
61
|
+
it "evaluates element-context queries on the native engine" do
|
|
62
62
|
expect(doc.root.xpath(".//item").size).to eq(2)
|
|
63
|
+
first_item = doc.root.children.find(&:element?)
|
|
64
|
+
expect(first_item.xpath("following-sibling::*").map(&:name)).to eq(%w[item])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "falls back to the Ruby engine for parent-axis queries from the root" do
|
|
68
|
+
# The binding reports the root element parentless; moxml roots
|
|
69
|
+
# it at the document
|
|
70
|
+
expect(doc.root.xpath("../*").size).to eq(1)
|
|
63
71
|
end
|
|
64
72
|
|
|
65
73
|
it "falls back to the Ruby engine for the xmlns: reserved prefix" do
|