moxml 0.1.26 → 0.2.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/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +69 -6
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +21 -2
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rexml/document"
|
|
4
4
|
require "rexml/xpath"
|
|
5
|
+
require "rexml/streamlistener"
|
|
5
6
|
require "set" unless RUBY_ENGINE == "opal"
|
|
6
7
|
require "stringio" if RUBY_ENGINE == "opal"
|
|
7
8
|
|
|
@@ -60,18 +61,21 @@ module Moxml
|
|
|
60
61
|
# @param handler [Moxml::SAX::Handler] Moxml SAX handler
|
|
61
62
|
# @return [void]
|
|
62
63
|
def sax_parse(xml, handler)
|
|
63
|
-
require "rexml/parsers/
|
|
64
|
+
require "rexml/parsers/streamparser"
|
|
64
65
|
require "rexml/source"
|
|
65
|
-
require "stringio"
|
|
66
66
|
|
|
67
|
-
bridge =
|
|
67
|
+
bridge = REXMLStreamBridge.new(handler)
|
|
68
68
|
|
|
69
69
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
70
|
-
source = ::REXML::IOSource.new(StringIO.new(xml_string))
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
# StreamParser (rather than SAX2Parser) is used because
|
|
72
|
+
# SAX2Parser normalizes plain "&#NN;" and "&#NN;" attribute
|
|
73
|
+
# literals to the same raw string, making spec-correct decoding
|
|
74
|
+
# impossible after the fact. StreamParser delivers values in
|
|
75
|
+
# the same decoded form as REXML's DOM.
|
|
76
|
+
handler.on_start_document
|
|
77
|
+
::REXML::Parsers::StreamParser.new(xml_string, bridge).parse
|
|
78
|
+
handler.on_end_document
|
|
75
79
|
rescue ::REXML::ParseException => e
|
|
76
80
|
error = Moxml::ParseError.new(e.message, line: e.line)
|
|
77
81
|
handler.on_error(error)
|
|
@@ -117,15 +121,12 @@ module Moxml
|
|
|
117
121
|
def create_native_doctype(name, external_id, system_id)
|
|
118
122
|
return nil unless name
|
|
119
123
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
::REXML::DocType.new(parts.join(" "))
|
|
124
|
+
external = if external_id
|
|
125
|
+
system_id ? %(PUBLIC "#{external_id}" "#{system_id}") : %(PUBLIC "#{external_id}")
|
|
126
|
+
elsif system_id
|
|
127
|
+
%(SYSTEM "#{system_id}")
|
|
128
|
+
end
|
|
129
|
+
external ? ::REXML::DocType.new(name.to_s, external) : ::REXML::DocType.new(name.to_s)
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def set_root(doc, element)
|
|
@@ -250,9 +251,17 @@ module Moxml
|
|
|
250
251
|
def attributes(element)
|
|
251
252
|
return [] unless element.is_a?(::REXML::Element)
|
|
252
253
|
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
# Each real Attribute node, not Attributes#values: when two
|
|
255
|
+
# attributes share a local name across namespaces, REXML's
|
|
256
|
+
# values leaks its internal prefix-grouping Hash alongside
|
|
257
|
+
# the Attribute objects (issue #95).
|
|
258
|
+
result = []
|
|
259
|
+
element.attributes.each_attribute do |attr|
|
|
260
|
+
next if attr.prefix.to_s.start_with?("xmlns")
|
|
261
|
+
|
|
262
|
+
result << attr
|
|
263
|
+
end
|
|
264
|
+
result
|
|
256
265
|
end
|
|
257
266
|
|
|
258
267
|
def attribute_element(attribute)
|
|
@@ -271,10 +280,17 @@ module Moxml
|
|
|
271
280
|
element = attribute.element
|
|
272
281
|
element.attributes.delete(old_name)
|
|
273
282
|
element.attributes << attribute
|
|
283
|
+
attribute
|
|
274
284
|
end
|
|
275
285
|
|
|
276
286
|
def set_attribute_value(attribute, value)
|
|
277
287
|
attribute.normalized = value
|
|
288
|
+
attribute
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def remove_attribute_native(attribute)
|
|
292
|
+
attribute.element.attributes.delete(attribute.expanded_name)
|
|
293
|
+
attribute
|
|
278
294
|
end
|
|
279
295
|
|
|
280
296
|
def get_attribute(element, name)
|
|
@@ -468,9 +484,11 @@ module Moxml
|
|
|
468
484
|
element.add_namespace(prefix,
|
|
469
485
|
ns.value)
|
|
470
486
|
end
|
|
487
|
+
# Renames the node in place (works for elements and
|
|
488
|
+
# attributes alike); the caller keeps tracking the same
|
|
489
|
+
# native.
|
|
471
490
|
element.name = "#{prefix}:#{element.name}"
|
|
472
|
-
|
|
473
|
-
::REXML::Attribute.new(prefix, ns.value, owner)
|
|
491
|
+
element
|
|
474
492
|
end
|
|
475
493
|
|
|
476
494
|
def namespace_prefix(node)
|
|
@@ -518,11 +536,17 @@ module Moxml
|
|
|
518
536
|
end
|
|
519
537
|
|
|
520
538
|
def doctype_external_id(native)
|
|
521
|
-
|
|
539
|
+
# Constructed doctypes keep the identifier as a raw
|
|
540
|
+
# external_id string; parsed ones populate public directly
|
|
541
|
+
native.public ||
|
|
542
|
+
native.external_id&.match(/PUBLIC\s+"([^"]*)"/)&.[](1)
|
|
522
543
|
end
|
|
523
544
|
|
|
524
545
|
def doctype_system_id(native)
|
|
525
|
-
native.system
|
|
546
|
+
return native.system if native.system
|
|
547
|
+
|
|
548
|
+
quoted = native.external_id&.scan(/"([^"]*)"/)
|
|
549
|
+
quoted&.last&.first
|
|
526
550
|
end
|
|
527
551
|
|
|
528
552
|
# not used at the moment
|
|
@@ -642,29 +666,33 @@ module Moxml
|
|
|
642
666
|
end
|
|
643
667
|
end
|
|
644
668
|
|
|
645
|
-
# Bridge between REXML
|
|
669
|
+
# Bridge between REXML StreamParser and Moxml SAX
|
|
646
670
|
#
|
|
647
|
-
#
|
|
671
|
+
# StreamParser (rather than SAX2Parser) is used because SAX2Parser
|
|
672
|
+
# normalizes plain "&#NN;" and "&#NN;" attribute literals to
|
|
673
|
+
# the same raw string, which makes spec-correct decoding impossible
|
|
674
|
+
# after the fact. StreamParser delivers attribute values in the
|
|
675
|
+
# same decoded form as REXML's DOM.
|
|
648
676
|
#
|
|
649
677
|
# @private
|
|
650
|
-
class
|
|
678
|
+
class REXMLStreamBridge
|
|
679
|
+
include ::REXML::StreamListener
|
|
651
680
|
include Moxml::SAX::NamespaceSplitter
|
|
652
681
|
|
|
653
682
|
def initialize(handler)
|
|
654
683
|
@handler = handler
|
|
655
684
|
end
|
|
656
685
|
|
|
657
|
-
|
|
658
|
-
def start_element(_uri, _localname, qname, attributes)
|
|
686
|
+
def tag_start(name, attributes)
|
|
659
687
|
attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
|
|
660
|
-
@handler.on_start_element(
|
|
688
|
+
@handler.on_start_element(name, attr_hash, ns_hash)
|
|
661
689
|
end
|
|
662
690
|
|
|
663
|
-
def
|
|
664
|
-
@handler.on_end_element(
|
|
691
|
+
def tag_end(name)
|
|
692
|
+
@handler.on_end_element(name)
|
|
665
693
|
end
|
|
666
694
|
|
|
667
|
-
def
|
|
695
|
+
def text(text)
|
|
668
696
|
@handler.on_characters(text)
|
|
669
697
|
end
|
|
670
698
|
|
|
@@ -676,26 +704,9 @@ module Moxml
|
|
|
676
704
|
@handler.on_comment(text)
|
|
677
705
|
end
|
|
678
706
|
|
|
679
|
-
def
|
|
707
|
+
def instruction(target, data)
|
|
680
708
|
@handler.on_processing_instruction(target, data || "")
|
|
681
709
|
end
|
|
682
|
-
|
|
683
|
-
def start_document
|
|
684
|
-
@handler.on_start_document
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
def end_document
|
|
688
|
-
@handler.on_end_document
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
# REXML calls these but we don't need to handle them
|
|
692
|
-
def xmldecl(version, encoding, standalone)
|
|
693
|
-
# XML declaration - we don't need to do anything
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
def progress(position)
|
|
697
|
-
# Progress callback - we don't need to do anything
|
|
698
|
-
end
|
|
699
710
|
end
|
|
700
711
|
end
|
|
701
712
|
end
|
data/lib/moxml/adapter.rb
CHANGED
|
@@ -7,8 +7,9 @@ module Moxml
|
|
|
7
7
|
autoload :CustomizedOx, "moxml/adapter/customized_ox"
|
|
8
8
|
autoload :CustomizedRexml, "moxml/adapter/customized_rexml"
|
|
9
9
|
autoload :CustomizedLibxml, "moxml/adapter/customized_libxml"
|
|
10
|
+
autoload :CustomizedLeptris, "moxml/adapter/customized_leptris"
|
|
10
11
|
|
|
11
|
-
AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
|
|
12
|
+
AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml leptris].freeze
|
|
12
13
|
|
|
13
14
|
# Adapters that work under the Opal (JavaScript) runtime.
|
|
14
15
|
# Oga is pure Ruby and designed with Opal compatibility in mind — it is the
|
data/lib/moxml/attribute.rb
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
module Moxml
|
|
4
4
|
class Attribute < Node
|
|
5
5
|
def name
|
|
6
|
-
@native
|
|
6
|
+
adapter.attribute_name(@native)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def name=(new_name)
|
|
10
|
-
|
|
10
|
+
# Mutation return contract (Adapter::Base): returns the native
|
|
11
|
+
# to keep tracking — same object for in-place adapters, fresh
|
|
12
|
+
# object for value-object adapters (leptris).
|
|
13
|
+
@native = adapter.set_attribute_name(@native, new_name)
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
# Returns the primary identifier for this attribute (its name)
|
|
@@ -44,7 +47,8 @@ module Moxml
|
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def namespace=(ns)
|
|
47
|
-
|
|
50
|
+
# See name= for the mutation return contract.
|
|
51
|
+
@native = adapter.set_namespace(@native, ns&.native)
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def element
|
|
@@ -53,7 +57,7 @@ module Moxml
|
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
def remove
|
|
56
|
-
adapter.
|
|
60
|
+
adapter.remove_attribute_native(@native)
|
|
57
61
|
if @parent_node.is_a?(Moxml::Element)
|
|
58
62
|
@parent_node.invalidate_attribute_cache!
|
|
59
63
|
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Canonical, adapter-independent attribute-name resolution.
|
|
5
|
+
#
|
|
6
|
+
# XML Namespaces 1.0 semantics, implemented once on the Moxml object
|
|
7
|
+
# model (the unified interface); adapters provide raw access only:
|
|
8
|
+
#
|
|
9
|
+
# - An attribute's expanded name is (namespace URI or none) + local
|
|
10
|
+
# name. Unprefixed attributes are never in a namespace — the
|
|
11
|
+
# default namespace declaration does not apply to attributes
|
|
12
|
+
# (Namespaces 1.0 §5.2).
|
|
13
|
+
# - "prefix:local" resolves the prefix against the element's
|
|
14
|
+
# in-scope declarations and matches by expanded name: an attribute
|
|
15
|
+
# written as q:type matches a lookup as p:type whenever both
|
|
16
|
+
# prefixes are bound to the same URI.
|
|
17
|
+
# - "xml" is prebound to the XML namespace URI; it never needs a
|
|
18
|
+
# declaration (Namespaces 1.0 §4).
|
|
19
|
+
# - "xmlns"/"xmlns:*" are declarations, not attributes: never match.
|
|
20
|
+
# - An undeclared prefix names nothing: nil. No silent prefix-string
|
|
21
|
+
# or bare-name fallback.
|
|
22
|
+
#
|
|
23
|
+
# When several attributes match (only possible in namespace-ill-formed
|
|
24
|
+
# documents), the first in document order wins, matching every
|
|
25
|
+
# adapter's attributes() ordering contract.
|
|
26
|
+
module AttributeResolver
|
|
27
|
+
XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# @return [Moxml::Attribute, nil] the matching attribute wrapper
|
|
32
|
+
def resolve(element, name)
|
|
33
|
+
name = name.to_s
|
|
34
|
+
if name.include?(":")
|
|
35
|
+
prefix, local = name.split(":", 2)
|
|
36
|
+
return nil if prefix == "xmlns"
|
|
37
|
+
|
|
38
|
+
uri = prefix_uri(element, prefix)
|
|
39
|
+
return nil if uri.nil?
|
|
40
|
+
|
|
41
|
+
element.attributes.find do |attr|
|
|
42
|
+
local_name(attr.name) == local && attribute_uri(element, attr) == uri
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
element.attributes.find do |attr|
|
|
46
|
+
local_name(attr.name) == name && attribute_uri(element, attr).nil?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# URI a prefix is bound to for lookups on this element, or nil when
|
|
52
|
+
# the prefix is undeclared.
|
|
53
|
+
def prefix_uri(element, prefix)
|
|
54
|
+
return XML_NAMESPACE_URI if prefix == "xml"
|
|
55
|
+
|
|
56
|
+
element.in_scope_namespaces
|
|
57
|
+
.find { |candidate| candidate.prefix == prefix }&.uri
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Assign a value to the attribute named by XML-correct resolution,
|
|
61
|
+
# mirroring #resolve:
|
|
62
|
+
#
|
|
63
|
+
# - "xmlns"/"xmlns:*" are namespace declarations, not attributes:
|
|
64
|
+
# delegated verbatim to the adapter (declaration creation is
|
|
65
|
+
# adapter territory).
|
|
66
|
+
# - A bare name replaces the no-namespace attribute only — a
|
|
67
|
+
# namespaced attribute sharing the local name is left alone.
|
|
68
|
+
# - A prefixed name replaces by expanded name: assigning p:x
|
|
69
|
+
# overwrites an attribute spelled q:x when both prefixes are
|
|
70
|
+
# bound to the same URI.
|
|
71
|
+
#
|
|
72
|
+
# When nothing matches, the attribute is created with the given
|
|
73
|
+
# spelling. A prefix not yet in scope is stored raw — builders
|
|
74
|
+
# legitimately write detached children before attaching them under
|
|
75
|
+
# the declaring ancestor, and reads resolve by expanded name once
|
|
76
|
+
# the prefix is in scope (matching nokogiri/libxml creation
|
|
77
|
+
# behavior; namespace validity is a property of the serialized
|
|
78
|
+
# document).
|
|
79
|
+
#
|
|
80
|
+
# @return [String] the assigned value
|
|
81
|
+
def assign(element, name, value)
|
|
82
|
+
name = name.to_s
|
|
83
|
+
adapter = element.context.config.adapter
|
|
84
|
+
if name == "xmlns" || name.start_with?("xmlns:")
|
|
85
|
+
adapter.set_attribute(element.native, name, value)
|
|
86
|
+
element.invalidate_attribute_cache!
|
|
87
|
+
return value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
existing = resolve(element, name)
|
|
91
|
+
if existing
|
|
92
|
+
existing.value = value
|
|
93
|
+
return value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
adapter.set_attribute(element.native, name, value)
|
|
97
|
+
element.invalidate_attribute_cache!
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Remove the attribute named by XML-correct resolution (see
|
|
102
|
+
# #assign for the name semantics).
|
|
103
|
+
#
|
|
104
|
+
# @return [Moxml::Attribute, nil] the removed attribute
|
|
105
|
+
def remove(element, name)
|
|
106
|
+
name = name.to_s
|
|
107
|
+
adapter = element.context.config.adapter
|
|
108
|
+
if name == "xmlns" || name.start_with?("xmlns:")
|
|
109
|
+
adapter.remove_attribute(element.native, name)
|
|
110
|
+
element.invalidate_attribute_cache!
|
|
111
|
+
return nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
attr = resolve(element, name)
|
|
115
|
+
return nil unless attr
|
|
116
|
+
|
|
117
|
+
adapter.remove_attribute_native(attr.native)
|
|
118
|
+
element.invalidate_attribute_cache!
|
|
119
|
+
attr
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# XPath 1.0 attribute node test, sharing the resolver's
|
|
123
|
+
# expanded-name semantics:
|
|
124
|
+
#
|
|
125
|
+
# @param prefix [String, Symbol, nil]
|
|
126
|
+
# - String: match by namespace URI resolved against the
|
|
127
|
+
# element's in-scope declarations; the prefix spelling is
|
|
128
|
+
# irrelevant (q:x matches a p:x test when URIs agree)
|
|
129
|
+
# - :any: any namespace, including none (*:local)
|
|
130
|
+
# - nil: bare name — no-namespace attributes only (Namespaces
|
|
131
|
+
# 1.0 §5.2)
|
|
132
|
+
# @param local [String, nil] local name, or nil for any (p:*)
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def attribute_test?(element, attr, prefix, local)
|
|
135
|
+
return false if local && local_name(attr.name) != local
|
|
136
|
+
|
|
137
|
+
case prefix
|
|
138
|
+
when :any then true
|
|
139
|
+
when nil then attribute_uri(element, attr).nil?
|
|
140
|
+
else
|
|
141
|
+
uri = prefix_uri(element, prefix)
|
|
142
|
+
!uri.nil? && uri == attribute_uri(element, attr)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Attribute node test with a URI resolved at XPath compile time
|
|
147
|
+
# (the static context's prefix mapping wins over the document's
|
|
148
|
+
# own declarations). See #attribute_test? for local semantics.
|
|
149
|
+
#
|
|
150
|
+
# @return [Boolean]
|
|
151
|
+
def attribute_test_uri?(element, attr, uri, local)
|
|
152
|
+
return false if local && local_name(attr.name) != local
|
|
153
|
+
|
|
154
|
+
attribute_uri(element, attr) == uri
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Local part of a wrapper attribute name. Local names cannot
|
|
158
|
+
# contain ':' in namespace-well-formed documents; splitting is for
|
|
159
|
+
# adapters whose natives carry the qualified name (leptris).
|
|
160
|
+
def local_name(name)
|
|
161
|
+
name.include?(":") ? name.split(":", 2)[1] : name
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# URI of an attribute's namespace, nil for the no-namespace case.
|
|
165
|
+
#
|
|
166
|
+
# Uses the attribute's own namespace when the native binding
|
|
167
|
+
# exposes one with a URI (nokogiri/oga/libxml, and rexml/ox for
|
|
168
|
+
# declared prefixes). Otherwise reconstructs it from the attribute
|
|
169
|
+
# prefix via the element's in-scope declarations — covering
|
|
170
|
+
# adapters that report a prefix without resolving it (rexml/ox for
|
|
171
|
+
# the prebound xml prefix) and adapters that expose neither
|
|
172
|
+
# namespace nor separate prefix (leptris qualified names).
|
|
173
|
+
def attribute_uri(element, attr)
|
|
174
|
+
ns = attr.namespace
|
|
175
|
+
return ns.uri unless ns.nil? || ns.uri.to_s.empty?
|
|
176
|
+
|
|
177
|
+
prefix = ns&.prefix || prefix_part(attr.name)
|
|
178
|
+
return nil if prefix.nil? || prefix.empty?
|
|
179
|
+
return XML_NAMESPACE_URI if prefix == "xml"
|
|
180
|
+
|
|
181
|
+
element.in_scope_namespaces
|
|
182
|
+
.find { |candidate| candidate.prefix == prefix }&.uri
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def prefix_part(name)
|
|
186
|
+
name.include?(":") ? name.split(":", 2)[0] : nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
data/lib/moxml/config.rb
CHANGED
|
@@ -5,8 +5,15 @@ module Moxml
|
|
|
5
5
|
LINE_ENDING_LF = "\n"
|
|
6
6
|
LINE_ENDING_CRLF = "\r\n"
|
|
7
7
|
VALID_LINE_ENDINGS = [LINE_ENDING_LF, LINE_ENDING_CRLF].freeze
|
|
8
|
-
VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
|
|
9
|
-
|
|
8
|
+
VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml leptris].freeze
|
|
9
|
+
# Preferred CRuby default (issue #96): fastest on every measured
|
|
10
|
+
# operation and the cleanest deployment story. Only takes effect
|
|
11
|
+
# when the installed leptris supports programmatic document
|
|
12
|
+
# construction (Leptris::XML::Document.create); otherwise moxml
|
|
13
|
+
# falls back to FALLBACK_ADAPTER so released-gem environments keep
|
|
14
|
+
# working. Oga remains the Opal default (pure Ruby).
|
|
15
|
+
PREFERRED_ADAPTER = :leptris
|
|
16
|
+
FALLBACK_ADAPTER = :nokogiri
|
|
10
17
|
OPAL_DEFAULT_ADAPTER = :oga
|
|
11
18
|
|
|
12
19
|
# Entity loading modes:
|
|
@@ -30,7 +37,26 @@ module Moxml
|
|
|
30
37
|
def runtime_default_adapter
|
|
31
38
|
return OPAL_DEFAULT_ADAPTER if RUBY_ENGINE == "opal"
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
return PREFERRED_ADAPTER if leptris_preferred_available?
|
|
41
|
+
|
|
42
|
+
detect_loaded_adapter || FALLBACK_ADAPTER
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# True when the leptris gem is installed AND new enough for the
|
|
46
|
+
# adapter (it needs programmatic document construction, which
|
|
47
|
+
# released 1.1.x lacks). Memoized: the require probe runs once.
|
|
48
|
+
def leptris_preferred_available?
|
|
49
|
+
return @leptris_preferred_available if defined?(@leptris_preferred_available)
|
|
50
|
+
|
|
51
|
+
@leptris_preferred_available = begin
|
|
52
|
+
require "leptris"
|
|
53
|
+
# leptris-ruby 1.9.0 regression (leptris-ruby#53): the XML
|
|
54
|
+
# autoload manifest is shadowed; load it explicitly.
|
|
55
|
+
require "leptris/xml"
|
|
56
|
+
::Leptris::XML::Document.respond_to?(:create)
|
|
57
|
+
rescue LoadError, NameError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
34
60
|
end
|
|
35
61
|
|
|
36
62
|
def detect_loaded_adapter
|
data/lib/moxml/context.rb
CHANGED
|
@@ -6,6 +6,18 @@ module Moxml
|
|
|
6
6
|
|
|
7
7
|
def initialize(adapter = nil)
|
|
8
8
|
@config = Config.new(adapter)
|
|
9
|
+
# Bumped on any namespace-scope mutation; Element scope caches
|
|
10
|
+
# compare against it so every wrapper sees changes without
|
|
11
|
+
# needing wrapper identity.
|
|
12
|
+
@namespace_scope_generation = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def namespace_scope_generation
|
|
16
|
+
@namespace_scope_generation
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bump_namespace_scope_generation
|
|
20
|
+
@namespace_scope_generation += 1
|
|
9
21
|
end
|
|
10
22
|
|
|
11
23
|
def entity_registry
|
data/lib/moxml/element.rb
CHANGED
|
@@ -37,19 +37,23 @@ module Moxml
|
|
|
37
37
|
ns&.uri
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Write path shares the resolver's expanded-name semantics (see
|
|
41
|
+
# AttributeResolver.assign): bare names target no-namespace
|
|
42
|
+
# attributes only; prefixed names replace by namespace URI and
|
|
43
|
+
# require a declared prefix. Cache coherence is the resolver's.
|
|
40
44
|
def []=(name, value)
|
|
41
|
-
|
|
42
|
-
@attributes = nil
|
|
45
|
+
Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
|
|
43
46
|
end
|
|
44
47
|
|
|
48
|
+
# Unified XML-correct name resolution (see AttributeResolver):
|
|
49
|
+
# bare names match only no-namespace attributes; prefixed names
|
|
50
|
+
# resolve through in-scope declarations to expanded names.
|
|
45
51
|
def [](name)
|
|
46
|
-
|
|
47
|
-
val ? adapter.restore_entities(val) : val
|
|
52
|
+
Moxml::AttributeResolver.resolve(self, name)&.value
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def attribute(name)
|
|
51
|
-
|
|
52
|
-
native_attr && Attribute.new(native_attr, context)
|
|
56
|
+
Moxml::AttributeResolver.resolve(self, name)
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
# Returns attribute value by name (used by XPath engine)
|
|
@@ -66,15 +70,14 @@ module Moxml
|
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def remove_attribute(name)
|
|
69
|
-
|
|
70
|
-
@attributes = nil
|
|
73
|
+
Moxml::AttributeResolver.remove(self, name)
|
|
71
74
|
self
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
def add_namespace(prefix, uri)
|
|
75
78
|
adapter.create_namespace(@native, prefix, uri,
|
|
76
79
|
namespace_validation_mode: context.config.namespace_validation_mode)
|
|
77
|
-
|
|
80
|
+
invalidate_namespace_cache!
|
|
78
81
|
self
|
|
79
82
|
rescue ValidationError => e
|
|
80
83
|
# Re-raise as NamespaceError, provide attributes for error context
|
|
@@ -106,7 +109,7 @@ module Moxml
|
|
|
106
109
|
else
|
|
107
110
|
adapter.set_namespace(@native, ns_or_hash&.native)
|
|
108
111
|
end
|
|
109
|
-
|
|
112
|
+
invalidate_namespace_cache!
|
|
110
113
|
end
|
|
111
114
|
|
|
112
115
|
def namespaces
|
|
@@ -119,9 +122,14 @@ module Moxml
|
|
|
119
122
|
# Returns all namespaces in scope for this element,
|
|
120
123
|
# including those inherited from ancestor elements.
|
|
121
124
|
def in_scope_namespaces
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
generation = context.namespace_scope_generation
|
|
126
|
+
if @in_scope_namespaces.nil? || @in_scope_generation != generation
|
|
127
|
+
@in_scope_namespaces = adapter.in_scope_namespaces(@native).map do |ns|
|
|
128
|
+
Namespace.new(ns, context)
|
|
129
|
+
end
|
|
130
|
+
@in_scope_generation = generation
|
|
124
131
|
end
|
|
132
|
+
@in_scope_namespaces
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
# Returns the namespace URI of this element (alias for namespace_uri)
|
|
@@ -209,5 +217,14 @@ module Moxml
|
|
|
209
217
|
def invalidate_attribute_cache!
|
|
210
218
|
@attributes = nil
|
|
211
219
|
end
|
|
220
|
+
|
|
221
|
+
# Clear the namespace caches and bump the context's scope
|
|
222
|
+
# generation so every wrapper — including ones not reachable from
|
|
223
|
+
# any children cache — recomputes on next read.
|
|
224
|
+
def invalidate_namespace_cache!
|
|
225
|
+
@namespaces = nil
|
|
226
|
+
@in_scope_namespaces = nil
|
|
227
|
+
context.bump_namespace_scope_generation
|
|
228
|
+
end
|
|
212
229
|
end
|
|
213
230
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Entity
|
|
5
|
+
# A named entity reference (&name;) as a value object, for
|
|
6
|
+
# adapters whose native trees cannot represent entity references
|
|
7
|
+
# (ox stores them in the tree; rexml and leptris carry them
|
|
8
|
+
# alongside). Adapter-specific namespaces alias this class so
|
|
9
|
+
# node_type dispatch keeps working.
|
|
10
|
+
class Reference
|
|
11
|
+
attr_reader :name
|
|
12
|
+
attr_accessor :parent
|
|
13
|
+
|
|
14
|
+
def initialize(name)
|
|
15
|
+
@name = name
|
|
16
|
+
@parent = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_xml
|
|
20
|
+
"&#{name};"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(Reference) && name == other.name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|