moxml 0.1.26 → 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/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +69 -6
- data/lib/compat/opal/moxml_boot.rb +6 -1
- data/lib/moxml/adapter/base.rb +53 -56
- 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 +137 -117
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +139 -55
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +64 -51
- 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/builder.rb +0 -18
- data/lib/moxml/config.rb +29 -5
- data/lib/moxml/context.rb +33 -0
- data/lib/moxml/document.rb +1 -1
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity/restorer.rb +95 -0
- data/lib/moxml/entity.rb +126 -0
- data/lib/moxml/entity_registry.rb +1 -1
- data/lib/moxml/error.rb +0 -42
- data/lib/moxml/node.rb +135 -10
- data/lib/moxml/node_set.rb +1 -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 +143 -8
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -3
- 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 +18 -1
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb} +1 -2
- 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 +15 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/libxml_internals_spec.rb +1 -1
- 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/config_spec.rb +0 -1
- data/spec/moxml/context_spec.rb +22 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/error_spec.rb +0 -50
- 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 +112 -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 +23 -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
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
|
|
|
@@ -40,7 +41,9 @@ module Moxml
|
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
ctx = _context || Context.new(:rexml)
|
|
43
|
-
|
|
44
|
+
doc = Document.new(native_doc, ctx)
|
|
45
|
+
Entity::Restorer.new(doc).run if ctx.config.restore_entities
|
|
46
|
+
doc
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def extract_encoding_from_xml(xml)
|
|
@@ -60,18 +63,21 @@ module Moxml
|
|
|
60
63
|
# @param handler [Moxml::SAX::Handler] Moxml SAX handler
|
|
61
64
|
# @return [void]
|
|
62
65
|
def sax_parse(xml, handler)
|
|
63
|
-
require "rexml/parsers/
|
|
66
|
+
require "rexml/parsers/streamparser"
|
|
64
67
|
require "rexml/source"
|
|
65
|
-
require "stringio"
|
|
66
68
|
|
|
67
|
-
bridge =
|
|
69
|
+
bridge = REXMLStreamBridge.new(handler)
|
|
68
70
|
|
|
69
71
|
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
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
# StreamParser (rather than SAX2Parser) is used because
|
|
74
|
+
# SAX2Parser normalizes plain "&#NN;" and "&#NN;" attribute
|
|
75
|
+
# literals to the same raw string, making spec-correct decoding
|
|
76
|
+
# impossible after the fact. StreamParser delivers values in
|
|
77
|
+
# the same decoded form as REXML's DOM.
|
|
78
|
+
handler.on_start_document
|
|
79
|
+
::REXML::Parsers::StreamParser.new(xml_string, bridge).parse
|
|
80
|
+
handler.on_end_document
|
|
75
81
|
rescue ::REXML::ParseException => e
|
|
76
82
|
error = Moxml::ParseError.new(e.message, line: e.line)
|
|
77
83
|
handler.on_error(error)
|
|
@@ -117,15 +123,12 @@ module Moxml
|
|
|
117
123
|
def create_native_doctype(name, external_id, system_id)
|
|
118
124
|
return nil unless name
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
::REXML::DocType.new(parts.join(" "))
|
|
126
|
+
external = if external_id
|
|
127
|
+
system_id ? %(PUBLIC "#{external_id}" "#{system_id}") : %(PUBLIC "#{external_id}")
|
|
128
|
+
elsif system_id
|
|
129
|
+
%(SYSTEM "#{system_id}")
|
|
130
|
+
end
|
|
131
|
+
external ? ::REXML::DocType.new(name.to_s, external) : ::REXML::DocType.new(name.to_s)
|
|
129
132
|
end
|
|
130
133
|
|
|
131
134
|
def set_root(doc, element)
|
|
@@ -250,9 +253,17 @@ module Moxml
|
|
|
250
253
|
def attributes(element)
|
|
251
254
|
return [] unless element.is_a?(::REXML::Element)
|
|
252
255
|
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
# Each real Attribute node, not Attributes#values: when two
|
|
257
|
+
# attributes share a local name across namespaces, REXML's
|
|
258
|
+
# values leaks its internal prefix-grouping Hash alongside
|
|
259
|
+
# the Attribute objects (issue #95).
|
|
260
|
+
result = []
|
|
261
|
+
element.attributes.each_attribute do |attr|
|
|
262
|
+
next if attr.prefix.to_s.start_with?("xmlns")
|
|
263
|
+
|
|
264
|
+
result << attr
|
|
265
|
+
end
|
|
266
|
+
result
|
|
256
267
|
end
|
|
257
268
|
|
|
258
269
|
def attribute_element(attribute)
|
|
@@ -271,10 +282,17 @@ module Moxml
|
|
|
271
282
|
element = attribute.element
|
|
272
283
|
element.attributes.delete(old_name)
|
|
273
284
|
element.attributes << attribute
|
|
285
|
+
attribute
|
|
274
286
|
end
|
|
275
287
|
|
|
276
288
|
def set_attribute_value(attribute, value)
|
|
277
289
|
attribute.normalized = value
|
|
290
|
+
attribute
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def remove_attribute_native(attribute)
|
|
294
|
+
attribute.element.attributes.delete(attribute.expanded_name)
|
|
295
|
+
attribute
|
|
278
296
|
end
|
|
279
297
|
|
|
280
298
|
def get_attribute(element, name)
|
|
@@ -468,9 +486,11 @@ module Moxml
|
|
|
468
486
|
element.add_namespace(prefix,
|
|
469
487
|
ns.value)
|
|
470
488
|
end
|
|
489
|
+
# Renames the node in place (works for elements and
|
|
490
|
+
# attributes alike); the caller keeps tracking the same
|
|
491
|
+
# native.
|
|
471
492
|
element.name = "#{prefix}:#{element.name}"
|
|
472
|
-
|
|
473
|
-
::REXML::Attribute.new(prefix, ns.value, owner)
|
|
493
|
+
element
|
|
474
494
|
end
|
|
475
495
|
|
|
476
496
|
def namespace_prefix(node)
|
|
@@ -518,11 +538,17 @@ module Moxml
|
|
|
518
538
|
end
|
|
519
539
|
|
|
520
540
|
def doctype_external_id(native)
|
|
521
|
-
|
|
541
|
+
# Constructed doctypes keep the identifier as a raw
|
|
542
|
+
# external_id string; parsed ones populate public directly
|
|
543
|
+
native.public ||
|
|
544
|
+
native.external_id&.match(/PUBLIC\s+"([^"]*)"/)&.[](1)
|
|
522
545
|
end
|
|
523
546
|
|
|
524
547
|
def doctype_system_id(native)
|
|
525
|
-
native.system
|
|
548
|
+
return native.system if native.system
|
|
549
|
+
|
|
550
|
+
quoted = native.external_id&.scan(/"([^"]*)"/)
|
|
551
|
+
quoted&.last&.first
|
|
526
552
|
end
|
|
527
553
|
|
|
528
554
|
# not used at the moment
|
|
@@ -642,29 +668,33 @@ module Moxml
|
|
|
642
668
|
end
|
|
643
669
|
end
|
|
644
670
|
|
|
645
|
-
# Bridge between REXML
|
|
671
|
+
# Bridge between REXML StreamParser and Moxml SAX
|
|
646
672
|
#
|
|
647
|
-
#
|
|
673
|
+
# StreamParser (rather than SAX2Parser) is used because SAX2Parser
|
|
674
|
+
# normalizes plain "&#NN;" and "&#NN;" attribute literals to
|
|
675
|
+
# the same raw string, which makes spec-correct decoding impossible
|
|
676
|
+
# after the fact. StreamParser delivers attribute values in the
|
|
677
|
+
# same decoded form as REXML's DOM.
|
|
648
678
|
#
|
|
649
679
|
# @private
|
|
650
|
-
class
|
|
680
|
+
class REXMLStreamBridge
|
|
681
|
+
include ::REXML::StreamListener
|
|
651
682
|
include Moxml::SAX::NamespaceSplitter
|
|
652
683
|
|
|
653
684
|
def initialize(handler)
|
|
654
685
|
@handler = handler
|
|
655
686
|
end
|
|
656
687
|
|
|
657
|
-
|
|
658
|
-
def start_element(_uri, _localname, qname, attributes)
|
|
688
|
+
def tag_start(name, attributes)
|
|
659
689
|
attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
|
|
660
|
-
@handler.on_start_element(
|
|
690
|
+
@handler.on_start_element(name, attr_hash, ns_hash)
|
|
661
691
|
end
|
|
662
692
|
|
|
663
|
-
def
|
|
664
|
-
@handler.on_end_element(
|
|
693
|
+
def tag_end(name)
|
|
694
|
+
@handler.on_end_element(name)
|
|
665
695
|
end
|
|
666
696
|
|
|
667
|
-
def
|
|
697
|
+
def text(text)
|
|
668
698
|
@handler.on_characters(text)
|
|
669
699
|
end
|
|
670
700
|
|
|
@@ -676,26 +706,9 @@ module Moxml
|
|
|
676
706
|
@handler.on_comment(text)
|
|
677
707
|
end
|
|
678
708
|
|
|
679
|
-
def
|
|
709
|
+
def instruction(target, data)
|
|
680
710
|
@handler.on_processing_instruction(target, data || "")
|
|
681
711
|
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
712
|
end
|
|
700
713
|
end
|
|
701
714
|
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/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
|
@@ -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
|
|
@@ -52,7 +78,6 @@ module Moxml
|
|
|
52
78
|
attr_reader :adapter_name, :default_line_ending
|
|
53
79
|
attr_accessor :strict_parsing,
|
|
54
80
|
:default_encoding,
|
|
55
|
-
:entity_encoding,
|
|
56
81
|
:default_indent,
|
|
57
82
|
:restore_entities,
|
|
58
83
|
:preload_entity_sets,
|
|
@@ -78,7 +103,6 @@ module Moxml
|
|
|
78
103
|
@default_encoding = default_encoding || Config.default.default_encoding
|
|
79
104
|
@default_indent = 2
|
|
80
105
|
@default_line_ending = LINE_ENDING_LF
|
|
81
|
-
@entity_encoding = :basic
|
|
82
106
|
@restore_entities = false
|
|
83
107
|
@preload_entity_sets = []
|
|
84
108
|
@entity_load_mode = :required
|
data/lib/moxml/context.rb
CHANGED
|
@@ -6,6 +6,39 @@ 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
|
+
# 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)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def namespace_scope_generation
|
|
37
|
+
@namespace_scope_generation
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def bump_namespace_scope_generation
|
|
41
|
+
@namespace_scope_generation += 1
|
|
9
42
|
end
|
|
10
43
|
|
|
11
44
|
def entity_registry
|
data/lib/moxml/document.rb
CHANGED
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
|