multi_xml 0.6.0 → 0.9.1
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 +5 -5
- data/.mutant.yml +21 -0
- data/.rspec +2 -0
- data/.rubocop.yml +65 -0
- data/CHANGELOG.md +62 -0
- data/Gemfile +26 -0
- data/LICENSE.md +1 -1
- data/README.md +181 -57
- data/Rakefile +68 -0
- data/Steepfile +29 -0
- data/benchmark/overall_parser_benchmark.rb +5 -0
- data/benchmark.rb +1002 -0
- data/lib/multi_xml/concurrency.rb +31 -0
- data/lib/multi_xml/constants.rb +179 -0
- data/lib/multi_xml/deprecated.rb +35 -0
- data/lib/multi_xml/errors.rb +147 -0
- data/lib/multi_xml/file_like.rb +62 -0
- data/lib/multi_xml/helpers.rb +228 -0
- data/lib/multi_xml/options.rb +63 -0
- data/lib/multi_xml/options_normalization.rb +40 -0
- data/lib/multi_xml/parse_support.rb +113 -0
- data/lib/multi_xml/parser.rb +47 -0
- data/lib/multi_xml/parser_resolution.rb +150 -0
- data/lib/multi_xml/parsers/dom_parser.rb +190 -0
- data/lib/multi_xml/parsers/libxml.rb +57 -17
- data/lib/multi_xml/parsers/libxml_sax.rb +188 -0
- data/lib/multi_xml/parsers/nokogiri.rb +60 -19
- data/lib/multi_xml/parsers/nokogiri_sax.rb +130 -0
- data/lib/multi_xml/parsers/oga.rb +116 -49
- data/lib/multi_xml/parsers/ox.rb +191 -66
- data/lib/multi_xml/parsers/rexml.rb +169 -75
- data/lib/multi_xml/parsers/sax_handler.rb +169 -0
- data/lib/multi_xml/version.rb +6 -44
- data/lib/multi_xml.rb +187 -284
- data/sig/multi_xml.rbs +304 -0
- metadata +55 -23
- data/lib/multi_xml/parsers/libxml2_parser.rb +0 -72
- data/multi_xml.gemspec +0 -19
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
require "libxml-ruby"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require_relative "sax_handler"
|
|
4
|
+
require_relative "libxml"
|
|
5
|
+
|
|
6
|
+
module MultiXML
|
|
7
|
+
module Parsers
|
|
8
|
+
# SAX-based parser using LibXML (faster for large documents)
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module LibxmlSax
|
|
12
|
+
extend MultiXML::Parser
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Exception class raised on LibXML parse failure
|
|
17
|
+
# @api private
|
|
18
|
+
ParseError = ::LibXML::XML::Error
|
|
19
|
+
|
|
20
|
+
# Parse XML from a string or IO object
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
# @param xml [String, IO] XML content
|
|
24
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
25
|
+
# @return [Hash] Parsed XML as a hash
|
|
26
|
+
# @raise [LibXML::XML::Error] if XML is malformed
|
|
27
|
+
def parse(xml, namespaces: :strip)
|
|
28
|
+
source = xml.respond_to?(:read) ? xml.read : xml.to_s
|
|
29
|
+
return {} if source.empty?
|
|
30
|
+
|
|
31
|
+
return parse_with_dom(source, namespaces) if dom_fallback?(source, namespaces)
|
|
32
|
+
|
|
33
|
+
parse_with_sax(source, namespaces)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Detect whether a start tag has attributes that collide after stripping
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
# @param source [String] XML source
|
|
40
|
+
# @return [Boolean] true when stripped attribute locals collide
|
|
41
|
+
def stripped_attribute_collision?(source)
|
|
42
|
+
source.scan(%r{<(?![!?/])[^>]*>}m).any? do |tag|
|
|
43
|
+
local_names = attribute_names(tag).map { |name| name.split(":", 2).last }
|
|
44
|
+
local_names.uniq.length < local_names.length
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Extract non-xmlns attribute names from a start tag
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
# @param tag [String] Start tag source
|
|
52
|
+
# @return [Array<String>] attribute names
|
|
53
|
+
def attribute_names(tag)
|
|
54
|
+
tag.scan(/\s([a-zA-Z_][\w.-]*(?::[a-zA-Z_][\w.-]*)?)\s*=/).flatten.reject do |name|
|
|
55
|
+
name == "xmlns" || name.start_with?("xmlns:")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Determine whether libxml_sax must fall back to the DOM parser
|
|
60
|
+
#
|
|
61
|
+
# @api private
|
|
62
|
+
# @param source [String] XML source
|
|
63
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
64
|
+
# @return [Boolean] true when DOM parsing is required
|
|
65
|
+
def dom_fallback?(source, namespaces)
|
|
66
|
+
namespaces != :strip || stripped_attribute_collision?(source)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Parse via the DOM libxml backend
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
# @param source [String] XML source
|
|
73
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
74
|
+
# @return [Hash] Parsed XML as a hash
|
|
75
|
+
def parse_with_dom(source, namespaces)
|
|
76
|
+
Libxml.parse(StringIO.new(source), namespaces: namespaces)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Parse via libxml-ruby's SAX parser
|
|
80
|
+
#
|
|
81
|
+
# @api private
|
|
82
|
+
# @param source [String] XML source
|
|
83
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
84
|
+
# @return [Hash] Parsed XML as a hash
|
|
85
|
+
def parse_with_sax(source, namespaces)
|
|
86
|
+
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
|
|
87
|
+
handler = Handler.new(namespaces)
|
|
88
|
+
parser = ::LibXML::XML::SaxParser.io(StringIO.new(source))
|
|
89
|
+
parser.callbacks = handler
|
|
90
|
+
parser.parse
|
|
91
|
+
handler.result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# LibXML SAX handler.
|
|
95
|
+
#
|
|
96
|
+
# libxml-ruby's namespace-aware callback strips prefixes from the attrs
|
|
97
|
+
# hash, so we rely on the qname-preserving `on_start_element` callback
|
|
98
|
+
# and resolve namespaces via SaxHandler's scope stack.
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
class Handler
|
|
102
|
+
include ::LibXML::XML::SaxParser::Callbacks
|
|
103
|
+
include SaxHandler
|
|
104
|
+
|
|
105
|
+
# Create a new SAX handler
|
|
106
|
+
#
|
|
107
|
+
# @api private
|
|
108
|
+
# @param mode [Symbol] Namespace handling mode
|
|
109
|
+
# @return [Handler] new handler instance
|
|
110
|
+
def initialize(mode)
|
|
111
|
+
initialize_handler(mode)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Handle start of document (no-op)
|
|
115
|
+
#
|
|
116
|
+
# @api private
|
|
117
|
+
# @return [void]
|
|
118
|
+
def on_start_document
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Handle end of document (no-op)
|
|
122
|
+
#
|
|
123
|
+
# @api private
|
|
124
|
+
# @return [void]
|
|
125
|
+
def on_end_document
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Handle parse errors (no-op; libxml-ruby raises directly)
|
|
129
|
+
#
|
|
130
|
+
# @api private
|
|
131
|
+
# @param _error [String] Error message (unused)
|
|
132
|
+
# @return [void]
|
|
133
|
+
def on_error(_error)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Handle start of an element
|
|
137
|
+
#
|
|
138
|
+
# libxml-ruby strips xmlns declarations from attrs and passes through
|
|
139
|
+
# prefixed names for regular attributes. Since libxml_sax only uses
|
|
140
|
+
# this handler in :strip mode, we route through the namespace-aware
|
|
141
|
+
# entrypoint with empty ns_decls and treat attribute qnames as-if
|
|
142
|
+
# they had no namespace — matching the desired :strip output.
|
|
143
|
+
#
|
|
144
|
+
# @api private
|
|
145
|
+
# @param name [String] Element name (possibly prefixed)
|
|
146
|
+
# @param attrs [Hash] Attributes as name => value
|
|
147
|
+
# @return [void]
|
|
148
|
+
def on_start_element(name, attrs = {})
|
|
149
|
+
prefix, local = sax_split_qname(name.to_s)
|
|
150
|
+
tuples = attrs.map do |k, v|
|
|
151
|
+
ap, al = sax_split_qname(k.to_s)
|
|
152
|
+
[ap, al, v]
|
|
153
|
+
end
|
|
154
|
+
handle_start_element_ns(local, prefix, tuples, [])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Handle end of an element
|
|
158
|
+
#
|
|
159
|
+
# @api private
|
|
160
|
+
# @param _name [String] Element name (unused)
|
|
161
|
+
# @return [void]
|
|
162
|
+
def on_end_element(_name)
|
|
163
|
+
handle_end_element
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Split a prefixed name into [prefix, local]
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
# @param name [String] Prefixed or local name
|
|
172
|
+
# @return [Array<String, nil>] prefix and local name
|
|
173
|
+
def sax_split_qname(name)
|
|
174
|
+
p, l = name.split(":", 2)
|
|
175
|
+
l ? [p, l] : [nil, p]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Handle character data (also aliased as `on_cdata_block`)
|
|
179
|
+
#
|
|
180
|
+
# @api private
|
|
181
|
+
# @param text [String] Text content
|
|
182
|
+
# @return [void]
|
|
183
|
+
def on_characters(text) = append_text(text)
|
|
184
|
+
alias_method :on_cdata_block, :on_characters
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -1,34 +1,75 @@
|
|
|
1
|
-
require
|
|
2
|
-
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require_relative "dom_parser"
|
|
3
3
|
|
|
4
|
-
module
|
|
4
|
+
module MultiXML
|
|
5
5
|
module Parsers
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# XML parser using the Nokogiri library
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module Nokogiri
|
|
10
|
+
extend MultiXML::Parser
|
|
11
|
+
include DomParser
|
|
8
12
|
extend self
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
# Exception class raised on Nokogiri parse failure
|
|
15
|
+
# @api private
|
|
16
|
+
ParseError = ::Nokogiri::XML::SyntaxError
|
|
17
|
+
|
|
18
|
+
# Parse XML from an IO object
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
# @param io [IO] IO-like object containing XML
|
|
22
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
23
|
+
# @return [Hash] Parsed XML as a hash
|
|
24
|
+
# @raise [Nokogiri::XML::SyntaxError] if XML is malformed
|
|
25
|
+
def parse(io, namespaces: :strip)
|
|
26
|
+
doc = ::Nokogiri::XML(io)
|
|
27
|
+
raise doc.errors.first unless doc.errors.empty?
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
doc = ::Nokogiri::XML(xml)
|
|
16
|
-
raise(doc.errors.first) unless doc.errors.empty?
|
|
17
|
-
node_to_hash(doc.root)
|
|
29
|
+
node_to_hash(doc.root, mode: namespaces)
|
|
18
30
|
end
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Iterate over child nodes
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
# @param node [Nokogiri::XML::Node] Parent node
|
|
38
|
+
# @return [void]
|
|
39
|
+
def each_child(node, &) = node.children.each(&)
|
|
40
|
+
|
|
41
|
+
# Iterate over attribute nodes (excludes xmlns declarations)
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
44
|
+
# @param node [Nokogiri::XML::Node] Element node
|
|
45
|
+
# @return [void]
|
|
46
|
+
def each_element_attr(node, &) = node.attribute_nodes.each(&)
|
|
21
47
|
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
# Yield each xmlns declaration on this element
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
# @param node [Nokogiri::XML::Node] Element node
|
|
52
|
+
# @return [void]
|
|
53
|
+
def each_namespace_decl(node)
|
|
54
|
+
node.namespace_definitions.each { |ns| yield ns.prefix, ns.href }
|
|
24
55
|
end
|
|
25
56
|
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
# Return [prefix, local] for an element
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @param node [Nokogiri::XML::Node] Element node
|
|
61
|
+
# @return [Array<String, nil>] prefix and local name
|
|
62
|
+
def element_parts(node)
|
|
63
|
+
[node.namespace&.prefix, node.name]
|
|
28
64
|
end
|
|
29
65
|
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
# Return [prefix, local] for an attribute
|
|
67
|
+
#
|
|
68
|
+
# @api private
|
|
69
|
+
# @param attr [Nokogiri::XML::Attr] Attribute node
|
|
70
|
+
# @return [Array<String, nil>] prefix and local name
|
|
71
|
+
def attr_parts(attr)
|
|
72
|
+
[attr.namespace&.prefix, attr.name]
|
|
32
73
|
end
|
|
33
74
|
end
|
|
34
75
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require_relative "sax_handler"
|
|
4
|
+
|
|
5
|
+
module MultiXML
|
|
6
|
+
module Parsers
|
|
7
|
+
# SAX-based parser using Nokogiri (faster for large documents)
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module NokogiriSax
|
|
11
|
+
extend MultiXML::Parser
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Exception class raised on Nokogiri parse failure
|
|
16
|
+
# @api private
|
|
17
|
+
ParseError = ::Nokogiri::XML::SyntaxError
|
|
18
|
+
|
|
19
|
+
# Parse XML from a string or IO object
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
# @param xml [String, IO] XML content
|
|
23
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
24
|
+
# @return [Hash] Parsed XML as a hash
|
|
25
|
+
# @raise [Nokogiri::XML::SyntaxError] if XML is malformed
|
|
26
|
+
def parse(xml, namespaces: :strip)
|
|
27
|
+
io = xml.respond_to?(:read) ? xml : StringIO.new(xml)
|
|
28
|
+
return {} if io.eof?
|
|
29
|
+
|
|
30
|
+
handler = Handler.new(namespaces)
|
|
31
|
+
::Nokogiri::XML::SAX::Parser.new(handler).parse(io)
|
|
32
|
+
handler.result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Nokogiri SAX handler.
|
|
36
|
+
#
|
|
37
|
+
# Nokogiri always invokes `start_element_namespace` (even for documents
|
|
38
|
+
# without namespaces — prefix/uri come through as nil). We don't define
|
|
39
|
+
# `start_element` because it would never fire.
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
class Handler < ::Nokogiri::XML::SAX::Document
|
|
43
|
+
include SaxHandler
|
|
44
|
+
|
|
45
|
+
# Create a new SAX handler
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
# @param mode [Symbol] Namespace handling mode
|
|
49
|
+
# @return [Handler] new handler instance
|
|
50
|
+
def initialize(mode)
|
|
51
|
+
super()
|
|
52
|
+
initialize_handler(mode)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Handle start of document (no-op)
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# @return [void]
|
|
59
|
+
def start_document
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Handle end of document (no-op)
|
|
63
|
+
#
|
|
64
|
+
# @api private
|
|
65
|
+
# @return [void]
|
|
66
|
+
def end_document
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Handle parse errors
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
# @param message [String] Error message
|
|
73
|
+
# @return [void]
|
|
74
|
+
# @raise [Nokogiri::XML::SyntaxError] always
|
|
75
|
+
def error(message)
|
|
76
|
+
raise ::Nokogiri::XML::SyntaxError, message
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Handle start of a namespaced element
|
|
80
|
+
#
|
|
81
|
+
# Signature is fixed by the Nokogiri SAX protocol.
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
84
|
+
# @param local [String] Local element name
|
|
85
|
+
# @param attrs [Array<Nokogiri::XML::SAX::Parser::Attribute>] Attributes
|
|
86
|
+
# @param prefix [String, nil] Element namespace prefix
|
|
87
|
+
# @param _uri [String, nil] Element namespace URI (unused)
|
|
88
|
+
# @param ns [Array] Namespace declarations as [prefix, uri] pairs
|
|
89
|
+
# @return [void]
|
|
90
|
+
# rubocop:disable Metrics/ParameterLists, Naming/MethodParameterName
|
|
91
|
+
def start_element_namespace(local, attrs = [], prefix = nil, _uri = nil, ns = [])
|
|
92
|
+
ns_decls = ns.map { |p, u| [normalize(p), u] }
|
|
93
|
+
attr_tuples = attrs.map { |a| [normalize(a.prefix), a.localname, a.value] }
|
|
94
|
+
handle_start_element_ns(local, normalize(prefix), attr_tuples, ns_decls)
|
|
95
|
+
end
|
|
96
|
+
# rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
|
|
97
|
+
|
|
98
|
+
# Handle end of a namespaced element
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
# @param _local [String] Local element name (unused)
|
|
102
|
+
# @param _prefix [String, nil] Namespace prefix (unused)
|
|
103
|
+
# @param _uri [String, nil] Namespace URI (unused)
|
|
104
|
+
# @return [void]
|
|
105
|
+
def end_element_namespace(_local, _prefix = nil, _uri = nil)
|
|
106
|
+
handle_end_element
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Handle character data
|
|
110
|
+
#
|
|
111
|
+
# @api private
|
|
112
|
+
# @param text [String] Text content
|
|
113
|
+
# @return [void]
|
|
114
|
+
def characters(text) = append_text(text)
|
|
115
|
+
alias_method :cdata_block, :characters
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Normalize a value, returning nil for empty or nil input
|
|
120
|
+
#
|
|
121
|
+
# @api private
|
|
122
|
+
# @param value [String, nil] Value to normalize
|
|
123
|
+
# @return [String, nil] value or nil if empty
|
|
124
|
+
def normalize(value)
|
|
125
|
+
(value.nil? || value.to_s.empty?) ? nil : value
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -1,73 +1,140 @@
|
|
|
1
|
-
require
|
|
2
|
-
|
|
1
|
+
require "oga"
|
|
2
|
+
require_relative "dom_parser"
|
|
3
3
|
|
|
4
|
-
module
|
|
4
|
+
module MultiXML
|
|
5
5
|
module Parsers
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# XML parser using the Oga library
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module Oga
|
|
10
|
+
extend MultiXML::Parser
|
|
11
|
+
include DomParser
|
|
8
12
|
extend self
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
# Exception class raised on Oga parse failure
|
|
15
|
+
# @api private
|
|
16
|
+
ParseError = LL::ParserError
|
|
17
|
+
|
|
18
|
+
# Parse XML from an IO object
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
# @param io [IO] IO-like object containing XML
|
|
22
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
23
|
+
# @return [Hash] Parsed XML as a hash
|
|
24
|
+
# @raise [LL::ParserError] if XML is malformed
|
|
25
|
+
def parse(io, namespaces: :strip)
|
|
26
|
+
doc = ::Oga.parse_xml(io)
|
|
27
|
+
node_to_hash(doc.children.first, mode: namespaces)
|
|
12
28
|
end
|
|
13
29
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
# Collect child nodes into a hash (Oga-specific implementation)
|
|
31
|
+
#
|
|
32
|
+
# Oga uses different node types than Nokogiri/LibXML.
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
# @param node [Oga::XML::Element] Parent node
|
|
36
|
+
# @param node_hash [Hash] Hash to populate
|
|
37
|
+
# @param mode [Symbol] Namespace handling mode
|
|
38
|
+
# @return [void]
|
|
39
|
+
def collect_children(node, node_hash, mode)
|
|
40
|
+
each_child(node) do |child|
|
|
41
|
+
case child
|
|
42
|
+
when ::Oga::XML::Element then node_to_hash(child, node_hash, mode: mode)
|
|
43
|
+
when ::Oga::XML::Text, ::Oga::XML::Cdata then node_hash[TEXT_CONTENT_KEY] << child.text
|
|
44
|
+
end
|
|
45
|
+
end
|
|
17
46
|
end
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
node_hash = {MultiXml::CONTENT_ROOT => ''}
|
|
48
|
+
private
|
|
21
49
|
|
|
22
|
-
|
|
50
|
+
# Iterate over child nodes
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
53
|
+
# @param node [Oga::XML::Element] Parent node
|
|
54
|
+
# @return [void]
|
|
55
|
+
def each_child(node, &) = node.children.each(&)
|
|
23
56
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
57
|
+
# Iterate over attribute nodes (excludes xmlns declarations)
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @param node [Oga::XML::Element] Element node
|
|
61
|
+
# @return [void]
|
|
62
|
+
def each_element_attr(node)
|
|
63
|
+
node.attributes.each do |attr|
|
|
64
|
+
next if oga_xmlns_attr?(attr)
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
each_child(node) do |c|
|
|
36
|
-
if c.is_a?(::Oga::XML::Element)
|
|
37
|
-
node_to_hash(c, node_hash)
|
|
38
|
-
elsif c.is_a?(::Oga::XML::Text) || c.is_a?(::Oga::XML::Cdata)
|
|
39
|
-
node_hash[MultiXml::CONTENT_ROOT] << c.text
|
|
40
|
-
end
|
|
66
|
+
yield attr
|
|
41
67
|
end
|
|
68
|
+
end
|
|
42
69
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
# Yield each xmlns declaration on this element
|
|
71
|
+
#
|
|
72
|
+
# Oga stores only locally declared namespaces on each element
|
|
73
|
+
# (inherited ones are resolved via lookup, not merged into
|
|
74
|
+
# #namespaces), so we can yield them directly.
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
# @param node [Oga::XML::Element] Element node
|
|
78
|
+
# @return [void]
|
|
79
|
+
def each_namespace_decl(node)
|
|
80
|
+
namespace_scope(node).each do |key, ns|
|
|
81
|
+
prefix = (key == "xmlns") ? nil : key
|
|
82
|
+
yield prefix, ns.uri
|
|
46
83
|
end
|
|
84
|
+
end
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
# Return [prefix, local] for an element
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @param node [Oga::XML::Element] Element node
|
|
90
|
+
# @return [Array<String, nil>] prefix and local name
|
|
91
|
+
def element_parts(node)
|
|
92
|
+
[oga_prefix(node.namespace), node.name]
|
|
93
|
+
end
|
|
54
94
|
|
|
55
|
-
|
|
95
|
+
# Return [prefix, local] for an attribute
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
98
|
+
# @param attr [Oga::XML::Attribute] Attribute node
|
|
99
|
+
# @return [Array<String, nil>] prefix and local name
|
|
100
|
+
def attr_parts(attr)
|
|
101
|
+
[oga_prefix(attr.namespace), attr.name]
|
|
56
102
|
end
|
|
57
103
|
|
|
58
|
-
|
|
104
|
+
# Translate Oga's default-namespace sentinel to nil
|
|
105
|
+
#
|
|
106
|
+
# Oga represents the default namespace with the sentinel name "xmlns";
|
|
107
|
+
# we translate that to nil so it isn't emitted as a prefix.
|
|
108
|
+
#
|
|
109
|
+
# @api private
|
|
110
|
+
# @param namespace [Oga::XML::Namespace, nil] Namespace object
|
|
111
|
+
# @return [String, nil] prefix or nil
|
|
112
|
+
def oga_prefix(namespace)
|
|
113
|
+
return nil unless namespace
|
|
59
114
|
|
|
60
|
-
|
|
61
|
-
node.children.each(&block)
|
|
115
|
+
(namespace.name == "xmlns") ? nil : namespace.name
|
|
62
116
|
end
|
|
63
117
|
|
|
64
|
-
|
|
65
|
-
|
|
118
|
+
# Check whether an Oga attribute is actually an xmlns declaration
|
|
119
|
+
#
|
|
120
|
+
# Oga exposes xmlns declarations via Element#namespaces but may also
|
|
121
|
+
# surface them as raw attributes in some cases — filter either shape.
|
|
122
|
+
#
|
|
123
|
+
# @api private
|
|
124
|
+
# @param attr [Oga::XML::Attribute] Attribute node
|
|
125
|
+
# @return [Boolean] true if it's an xmlns declaration
|
|
126
|
+
def oga_xmlns_attr?(attr)
|
|
127
|
+
attr.name == "xmlns" || attr.namespace_name == "xmlns"
|
|
66
128
|
end
|
|
67
129
|
|
|
68
|
-
|
|
69
|
-
|
|
130
|
+
# Local namespace scope for a node
|
|
131
|
+
#
|
|
132
|
+
# @api private
|
|
133
|
+
# @param node [Oga::XML::Element] Element node
|
|
134
|
+
# @return [Hash{String => Oga::XML::Namespace}] scope
|
|
135
|
+
def namespace_scope(node)
|
|
136
|
+
node.namespaces || {}
|
|
70
137
|
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|