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
data/lib/multi_xml/parsers/ox.rb
CHANGED
|
@@ -1,91 +1,216 @@
|
|
|
1
|
-
require
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# conversion rules are:
|
|
5
|
-
#
|
|
6
|
-
# - Each document starts out as an empty Hash.
|
|
7
|
-
#
|
|
8
|
-
# - Reading an element created an entry in the parent Hash that has a key of
|
|
9
|
-
# the element name and a value of a Hash with attributes as key value
|
|
10
|
-
# pairs. Children are added as described by this rule.
|
|
11
|
-
#
|
|
12
|
-
# - Text and CDATE is stored in the parent element Hash with a key of
|
|
13
|
-
# MultiXml::CONTENT_ROOT and a value of the text itself.
|
|
14
|
-
#
|
|
15
|
-
# - If a key already exists in the Hash then the value associated with the key
|
|
16
|
-
# is converted to an Array with the old and new value in it.
|
|
17
|
-
#
|
|
18
|
-
# - Other elements such as the xml prolog, doctype, and comments are ignored.
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
module MultiXml
|
|
1
|
+
require "ox"
|
|
2
|
+
|
|
3
|
+
module MultiXML
|
|
22
4
|
module Parsers
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
# XML parser using the Ox library (fastest pure-Ruby parser)
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Ox
|
|
9
|
+
extend MultiXML::Parser
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
Exception
|
|
28
|
-
end
|
|
11
|
+
module_function
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
# Exception class raised on Ox parse failure
|
|
14
|
+
# @api private
|
|
15
|
+
ParseError = ::Ox::ParseError
|
|
16
|
+
|
|
17
|
+
# Parse XML from an IO object
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
# @param io [IO] IO-like object containing XML
|
|
21
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
22
|
+
# @return [Hash] Parsed XML as a hash
|
|
23
|
+
def parse(io, namespaces: :strip)
|
|
24
|
+
handler = Handler.new(namespaces)
|
|
25
|
+
::Ox.sax_parse(handler, io, convert_special: true, skip: :skip_return)
|
|
26
|
+
handler.result
|
|
34
27
|
end
|
|
35
28
|
|
|
29
|
+
# SAX event handler that builds a hash tree while parsing.
|
|
30
|
+
#
|
|
31
|
+
# Ox's SAX callbacks expose element and attribute names in prefixed
|
|
32
|
+
# form (e.g. "atom:feed"). Under :preserve we keep the source form
|
|
33
|
+
# verbatim; under :strip we drop the prefix and filter xmlns
|
|
34
|
+
# declarations out of the attribute stream.
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
36
37
|
class Handler
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# Create a new SAX handler
|
|
39
|
+
#
|
|
40
|
+
# @api private
|
|
41
|
+
# @param mode [Symbol] Namespace handling mode
|
|
42
|
+
# @return [Handler] new handler instance
|
|
43
|
+
def initialize(mode)
|
|
44
|
+
@mode = mode
|
|
45
|
+
@stack = [{}]
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
# Get the parsed result
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
# @return [Hash] the parsed hash
|
|
52
|
+
def result = @stack.first
|
|
53
|
+
|
|
54
|
+
# Handle start of an element
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
57
|
+
# @param name [Symbol, String] Element name
|
|
58
|
+
# @return [void]
|
|
59
|
+
def start_element(name)
|
|
60
|
+
child = {}
|
|
61
|
+
add_value(current, format_name(name.to_s), child)
|
|
62
|
+
@stack << child
|
|
45
63
|
end
|
|
46
64
|
|
|
65
|
+
# Handle an attribute
|
|
66
|
+
#
|
|
67
|
+
# Ignored outside an element (e.g. attributes on the XML declaration
|
|
68
|
+
# such as `<?xml version="1.0"?>`, which fire before any `start_element`).
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
# @param name [Symbol, String] Attribute name
|
|
72
|
+
# @param value [String] Attribute value
|
|
73
|
+
# @return [void]
|
|
47
74
|
def attr(name, value)
|
|
48
|
-
|
|
75
|
+
return if @stack.size < 2
|
|
76
|
+
|
|
77
|
+
name = name.to_s
|
|
78
|
+
return if xmlns_decl?(name) && @mode != :preserve
|
|
79
|
+
|
|
80
|
+
add_attribute_value(current, format_name(name), value)
|
|
49
81
|
end
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
# Handle text content (also aliased as `cdata`)
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
# @param value [String] Text content
|
|
87
|
+
# @return [void]
|
|
88
|
+
def text(value) = append_text(current, value)
|
|
89
|
+
alias_method :cdata, :text
|
|
90
|
+
|
|
91
|
+
# Handle end of an element
|
|
92
|
+
#
|
|
93
|
+
# @api private
|
|
94
|
+
# @param _name [Symbol, String] Element name (unused)
|
|
95
|
+
# @return [void]
|
|
96
|
+
def end_element(_name)
|
|
97
|
+
strip_whitespace_content if current.key?(TEXT_CONTENT_KEY)
|
|
98
|
+
@stack.pop
|
|
53
99
|
end
|
|
54
100
|
|
|
55
|
-
|
|
56
|
-
|
|
101
|
+
# Handle parse errors
|
|
102
|
+
#
|
|
103
|
+
# @api private
|
|
104
|
+
# @param message [String] Error message
|
|
105
|
+
# @param line [Integer] Line number
|
|
106
|
+
# @param column [Integer] Column number
|
|
107
|
+
# @return [void]
|
|
108
|
+
# @raise [Ox::ParseError] always
|
|
109
|
+
def error(message, line, column)
|
|
110
|
+
raise ::Ox::ParseError, "#{message} at #{line}:#{column}"
|
|
57
111
|
end
|
|
58
112
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Current element hash on top of the stack
|
|
116
|
+
#
|
|
117
|
+
# @api private
|
|
118
|
+
# @return [Hash] current hash being built
|
|
119
|
+
def current = @stack.last
|
|
120
|
+
|
|
121
|
+
# Format a prefixed-or-local name according to the namespace mode
|
|
122
|
+
#
|
|
123
|
+
# @api private
|
|
124
|
+
# @param name [String] Prefixed or local name
|
|
125
|
+
# @return [String] formatted name
|
|
126
|
+
def format_name(name)
|
|
127
|
+
(@mode == :preserve) ? name : name.split(":", 2).last
|
|
64
128
|
end
|
|
65
129
|
|
|
66
|
-
|
|
67
|
-
|
|
130
|
+
# Check whether an attribute name is an xmlns declaration
|
|
131
|
+
#
|
|
132
|
+
# @api private
|
|
133
|
+
# @param name [String] Attribute name
|
|
134
|
+
# @return [Boolean] true if xmlns or xmlns:*
|
|
135
|
+
def xmlns_decl?(name)
|
|
136
|
+
name == "xmlns" || name.start_with?("xmlns:")
|
|
68
137
|
end
|
|
69
138
|
|
|
70
|
-
|
|
71
|
-
|
|
139
|
+
# Add a value to a hash, folding into an array on collision
|
|
140
|
+
#
|
|
141
|
+
# @api private
|
|
142
|
+
# @param hash [Hash] Target hash
|
|
143
|
+
# @param key [String] Key to add
|
|
144
|
+
# @param value [Object] Value to add
|
|
145
|
+
# @return [void]
|
|
146
|
+
def add_value(hash, key, value)
|
|
147
|
+
existing = hash[key]
|
|
148
|
+
hash[key] = existing ? merge_values(existing, value) : value
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Append a text fragment to the current node's content
|
|
152
|
+
#
|
|
153
|
+
# SAX parsers may deliver element text in multiple callbacks when
|
|
154
|
+
# inline elements split the text stream. MultiXML represents that
|
|
155
|
+
# as one concatenated ``__content__`` string, not an array.
|
|
156
|
+
#
|
|
157
|
+
# @api private
|
|
158
|
+
# @param hash [Hash] Target hash
|
|
159
|
+
# @param value [String] Text fragment
|
|
160
|
+
# @return [void]
|
|
161
|
+
def append_text(hash, value)
|
|
162
|
+
existing = hash[TEXT_CONTENT_KEY]
|
|
163
|
+
hash[TEXT_CONTENT_KEY] = existing ? "#{existing}#{value}" : value
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Merge a value with an existing value, creating an array if needed
|
|
167
|
+
#
|
|
168
|
+
# @api private
|
|
169
|
+
# @param existing [Object] Existing value
|
|
170
|
+
# @param value [Object] Value to append
|
|
171
|
+
# @return [Array] array with both values
|
|
172
|
+
def merge_values(existing, value)
|
|
173
|
+
existing.is_a?(Array) ? existing << value : [existing, value]
|
|
72
174
|
end
|
|
73
175
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
176
|
+
# Add an attribute value while keeping document order on collisions
|
|
177
|
+
#
|
|
178
|
+
# @api private
|
|
179
|
+
# @param hash [Hash] Target hash
|
|
180
|
+
# @param key [String] Attribute key
|
|
181
|
+
# @param value [String] Attribute value
|
|
182
|
+
# @return [void]
|
|
183
|
+
def add_attribute_value(hash, key, value)
|
|
184
|
+
existing = hash[key]
|
|
185
|
+
hash[key] = case existing
|
|
186
|
+
when nil then value
|
|
187
|
+
when Array then insert_attribute_before_children(existing, value)
|
|
188
|
+
when Hash then [value, existing]
|
|
189
|
+
else [existing, value]
|
|
86
190
|
end
|
|
87
191
|
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
192
|
+
|
|
193
|
+
# Insert a later attribute before any child-element entries
|
|
194
|
+
#
|
|
195
|
+
# @api private
|
|
196
|
+
# @param values [Array] Existing colliding values
|
|
197
|
+
# @param value [String] Attribute value to insert
|
|
198
|
+
# @return [Array] Updated value list
|
|
199
|
+
def insert_attribute_before_children(values, value)
|
|
200
|
+
child_index = values.index { |entry| entry.is_a?(Hash) } || values.length
|
|
201
|
+
values.dup.insert(child_index, value)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Remove empty or whitespace-only text content from the current hash
|
|
205
|
+
#
|
|
206
|
+
# @api private
|
|
207
|
+
# @return [void]
|
|
208
|
+
def strip_whitespace_content
|
|
209
|
+
content = current[TEXT_CONTENT_KEY]
|
|
210
|
+
should_remove = content.empty? || (current.size > 1 && content.strip.empty?)
|
|
211
|
+
current.delete(TEXT_CONTENT_KEY) if should_remove
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -1,111 +1,205 @@
|
|
|
1
|
-
require
|
|
1
|
+
require "rexml/document"
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module MultiXML
|
|
4
4
|
module Parsers
|
|
5
|
-
|
|
5
|
+
# XML parser using Ruby's built-in REXML library
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Rexml
|
|
9
|
+
extend MultiXML::Parser
|
|
6
10
|
extend self
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
# Exception class raised on REXML parse failure
|
|
13
|
+
# @api private
|
|
14
|
+
ParseError = ::REXML::ParseException
|
|
15
|
+
|
|
16
|
+
# Parse XML from an IO object
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
# @param io [IO] IO-like object containing XML
|
|
20
|
+
# @param namespaces [Symbol] Namespace handling mode
|
|
21
|
+
# @return [Hash] Parsed XML as a hash
|
|
22
|
+
# @raise [REXML::ParseException] if XML is malformed
|
|
23
|
+
def parse(io, namespaces: :strip)
|
|
24
|
+
doc = REXML::Document.new(io)
|
|
25
|
+
element_to_hash({}, doc.root, namespaces)
|
|
10
26
|
end
|
|
11
27
|
|
|
12
|
-
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Convert an element to hash format
|
|
13
31
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
# @api private
|
|
33
|
+
# @param hash [Hash] Accumulator hash
|
|
34
|
+
# @param element [REXML::Element] Element to convert
|
|
35
|
+
# @param mode [Symbol] Namespace handling mode
|
|
36
|
+
# @return [Hash] Updated hash
|
|
37
|
+
def element_to_hash(hash, element, mode)
|
|
38
|
+
add_to_hash(hash, format_element_name(element, mode), collapse_element(element, mode))
|
|
20
39
|
end
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
# Format element name using prefix/local and namespace mode
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
44
|
+
# @param element [REXML::Element] Element node
|
|
45
|
+
# @param mode [Symbol] Namespace handling mode
|
|
46
|
+
# @return [String] formatted element name
|
|
47
|
+
def format_element_name(element, mode)
|
|
48
|
+
format_name(element.prefix, element.name, mode)
|
|
49
|
+
end
|
|
23
50
|
|
|
24
|
-
#
|
|
51
|
+
# Format attribute name using prefix/local and namespace mode
|
|
25
52
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
def
|
|
31
|
-
|
|
53
|
+
# @api private
|
|
54
|
+
# @param attr [REXML::Attribute] Attribute node
|
|
55
|
+
# @param mode [Symbol] Namespace handling mode
|
|
56
|
+
# @return [String] formatted attribute name
|
|
57
|
+
def format_attr_name(attr, mode)
|
|
58
|
+
format_name(attr.prefix, attr.name, mode)
|
|
32
59
|
end
|
|
33
60
|
|
|
34
|
-
#
|
|
61
|
+
# Produce a name string for a given [prefix, local] tuple
|
|
35
62
|
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
# @api private
|
|
64
|
+
# @param prefix [String, nil] Namespace prefix
|
|
65
|
+
# @param local [String] Local part of the name
|
|
66
|
+
# @param mode [Symbol] Namespace handling mode
|
|
67
|
+
# @return [String] formatted name
|
|
68
|
+
def format_name(prefix, local, mode)
|
|
69
|
+
(mode == :preserve && prefix && !prefix.empty?) ? "#{prefix}:#{local}" : local
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Collapse an element into a hash with attributes and content
|
|
73
|
+
#
|
|
74
|
+
# @api private
|
|
75
|
+
# @param element [REXML::Element] Element to collapse
|
|
76
|
+
# @param mode [Symbol] Namespace handling mode
|
|
77
|
+
# @return [Hash] Hash representation
|
|
78
|
+
def collapse_element(element, mode)
|
|
79
|
+
node_hash = collect_attributes(element, mode)
|
|
40
80
|
|
|
41
81
|
if element.has_elements?
|
|
42
|
-
element
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
merge_texts!(hash, element)
|
|
82
|
+
collect_child_elements(element, node_hash, mode)
|
|
83
|
+
add_text_content(node_hash, element) unless whitespace_only?(element)
|
|
84
|
+
elsif node_hash.empty? || !whitespace_only?(element)
|
|
85
|
+
add_text_content(node_hash, element)
|
|
47
86
|
end
|
|
87
|
+
|
|
88
|
+
node_hash
|
|
48
89
|
end
|
|
49
90
|
|
|
50
|
-
#
|
|
91
|
+
# Collect all attributes from an element into a hash
|
|
51
92
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
hash
|
|
93
|
+
# @api private
|
|
94
|
+
# @param element [REXML::Element] Element with attributes
|
|
95
|
+
# @param mode [Symbol] Namespace handling mode
|
|
96
|
+
# @return [Hash] Hash of attribute name-value pairs
|
|
97
|
+
def collect_attributes(element, mode)
|
|
98
|
+
element.attributes.each_attribute.with_object({}) do |attr, hash|
|
|
99
|
+
if xmlns_decl?(attr)
|
|
100
|
+
add_attribute_value(hash, xmlns_decl_key(attr), attr.value) if mode == :preserve
|
|
101
|
+
else
|
|
102
|
+
add_attribute_value(hash, format_attr_name(attr, mode), attr.value)
|
|
103
|
+
end
|
|
64
104
|
end
|
|
65
105
|
end
|
|
66
106
|
|
|
67
|
-
#
|
|
68
|
-
# already exists and the existing value associated with key is not
|
|
69
|
-
# an Array, it will be wrapped in an Array. Then the new value is
|
|
70
|
-
# appended to that Array.
|
|
107
|
+
# Check whether an attribute represents an xmlns declaration
|
|
71
108
|
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
# @api private
|
|
110
|
+
# @param attr [REXML::Attribute] Attribute to inspect
|
|
111
|
+
# @return [Boolean] true if xmlns declaration
|
|
112
|
+
def xmlns_decl?(attr)
|
|
113
|
+
attr.prefix == "xmlns" || ((attr.prefix.nil? || attr.prefix.empty?) && attr.name == "xmlns")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Build the key for an xmlns declaration under :preserve
|
|
117
|
+
#
|
|
118
|
+
# @api private
|
|
119
|
+
# @param attr [REXML::Attribute] Declaration attribute
|
|
120
|
+
# @return [String] key such as "xmlns" or "xmlns:atom"
|
|
121
|
+
def xmlns_decl_key(attr)
|
|
122
|
+
(attr.prefix == "xmlns") ? "xmlns:#{attr.name}" : "xmlns"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Collect all child elements into a hash
|
|
126
|
+
#
|
|
127
|
+
# @api private
|
|
128
|
+
# @param element [REXML::Element] Parent element
|
|
129
|
+
# @param node_hash [Hash] Hash to populate
|
|
130
|
+
# @param mode [Symbol] Namespace handling mode
|
|
131
|
+
# @return [void]
|
|
132
|
+
def collect_child_elements(element, node_hash, mode)
|
|
133
|
+
element.each_element { |child| element_to_hash(node_hash, child, mode) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Add text content from an element to a hash
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
139
|
+
# @param hash [Hash] Target hash
|
|
140
|
+
# @param element [REXML::Element] Element with text
|
|
141
|
+
# @return [Hash] Updated hash
|
|
142
|
+
def add_text_content(hash, element)
|
|
143
|
+
return hash unless element.has_text?
|
|
144
|
+
|
|
145
|
+
text = element.texts.map(&:value).join
|
|
146
|
+
add_to_hash(hash, TEXT_CONTENT_KEY, text)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Add a value to a hash, handling duplicates as arrays
|
|
150
|
+
#
|
|
151
|
+
# @api private
|
|
152
|
+
# @param hash [Hash] Target hash
|
|
153
|
+
# @param key [String] Key to add
|
|
154
|
+
# @param value [Object] Value to add
|
|
155
|
+
# @return [Hash] Updated hash
|
|
156
|
+
def add_to_hash(hash, key, value)
|
|
157
|
+
existing = hash[key]
|
|
158
|
+
hash[key] = if existing
|
|
159
|
+
existing.is_a?(Array) ? existing << value : [existing, value]
|
|
160
|
+
elsif value.is_a?(Array)
|
|
161
|
+
[value]
|
|
87
162
|
else
|
|
88
|
-
|
|
163
|
+
value
|
|
164
|
+
end
|
|
165
|
+
hash
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Add an attribute value while keeping document order on collisions
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
# @param hash [Hash] Target hash
|
|
172
|
+
# @param key [String] Attribute key
|
|
173
|
+
# @param value [String] Attribute value
|
|
174
|
+
# @return [Hash] Updated hash
|
|
175
|
+
def add_attribute_value(hash, key, value)
|
|
176
|
+
existing = hash[key]
|
|
177
|
+
hash[key] = case existing
|
|
178
|
+
when nil then value
|
|
179
|
+
when Array then insert_attribute_before_children(existing, value)
|
|
180
|
+
when Hash then [value, existing]
|
|
181
|
+
else [existing, value]
|
|
89
182
|
end
|
|
90
183
|
hash
|
|
91
184
|
end
|
|
92
185
|
|
|
93
|
-
#
|
|
94
|
-
# Returns an empty Hash if node has no attributes.
|
|
186
|
+
# Insert a later attribute before any child-element entries
|
|
95
187
|
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
188
|
+
# @api private
|
|
189
|
+
# @param values [Array] Existing colliding values
|
|
190
|
+
# @param value [String] Attribute value to insert
|
|
191
|
+
# @return [Array] Updated value list
|
|
192
|
+
def insert_attribute_before_children(values, value)
|
|
193
|
+
child_index = values.index { |entry| entry.is_a?(Hash) } || values.length
|
|
194
|
+
values.dup.insert(child_index, value)
|
|
102
195
|
end
|
|
103
196
|
|
|
104
|
-
#
|
|
197
|
+
# Check if element contains only whitespace text
|
|
105
198
|
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
199
|
+
# @api private
|
|
200
|
+
# @param element [REXML::Element] Element to check
|
|
201
|
+
# @return [Boolean] true if whitespace only
|
|
202
|
+
def whitespace_only?(element)
|
|
109
203
|
element.texts.join.strip.empty?
|
|
110
204
|
end
|
|
111
205
|
end
|