nori 2.7.1 → 2.8.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/CHANGELOG.md +34 -13
- data/README.md +76 -0
- data/lib/nori/parser/nokogiri.rb +53 -6
- data/lib/nori/parser/rexml.rb +31 -2
- data/lib/nori/string_io_file.rb +2 -0
- data/lib/nori/version.rb +1 -1
- data/lib/nori/xml_utility_node.rb +89 -51
- data/lib/nori.rb +21 -1
- metadata +19 -23
- data/.devcontainer/devcontainer.json +0 -19
- data/.github/dependabot.yml +0 -12
- data/.github/workflows/test.yml +0 -26
- data/.gitignore +0 -8
- data/.rspec +0 -1
- data/Gemfile +0 -6
- data/Rakefile +0 -12
- data/benchmark/benchmark.rb +0 -19
- data/benchmark/soap_response.xml +0 -266
- data/nori.gemspec +0 -29
- data/spec/nori/api_spec.rb +0 -177
- data/spec/nori/core_ext/hash_spec.rb +0 -30
- data/spec/nori/nori_spec.rb +0 -650
- data/spec/nori/string_utils_spec.rb +0 -33
- data/spec/spec_helper.rb +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac639b2fdf06325300d7a65de3e8de963d6b6deed94eba8a5c5560ec99e29e28
|
|
4
|
+
data.tar.gz: 5837f3e91314f284171ac6b97ebd21d8b6d2bcd769aea32c854c4818523a15da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b315e0f90e75a5d225d83b0248b7d5e554ad1eab8e06b37e7cd25c5dacce146bfff6ef3e40e7ff37d4e07eb142b255b6d7a026512cd1f0d94622dcc6669d0ff0
|
|
7
|
+
data.tar.gz: 5fa332d507687ea25c1f2517944aff167916275f149ed590d097bc04aec39b3cf57532eeab5cee37167df4c69e68267908f80ef3c223dd84bf3c1baf881e3815
|
data/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nori changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.8.0] - 2026-07-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* Add the `:standards` profile option. `Nori.new(standards: true)` turns on Nori's spec-correct parsing as a group rather than one flag at a time. Its first members are xml:space honoring (whitespace-only text under `xml:space="preserve"` is kept instead of stripped, with inheritance and `xml:space="default"` reset per [XML 1.0 §2.10](https://www.w3.org/TR/xml/#sec-white-space)) and the XML string-value model for empty elements (implies `:consistent_empty_tags` with an empty-string `:empty_tag_value`, both still overridable). Opt-in on the 2.x line, default in 3.0. Thanks to @md5 for raising the `xml:space="preserve"` case on [#97](https://github.com/savonrb/nori/issues/97).
|
|
13
|
+
|
|
14
|
+
* [#97](https://github.com/savonrb/nori/issues/97) Add the `:consistent_empty_tags` option ([#109](https://github.com/savonrb/nori/pull/109)). When enabled, every empty tag becomes the `:empty_tag_value`, whether it has attributes or not. A string value keeps the attributes accessible via `#attributes`, and an explicit `xsi:nil="true"` always becomes `nil`. Reported by @lukasbischof, who also proposed a fix in [#98](https://github.com/savonrb/nori/pull/98). Thanks to @md5 for surfacing another instance of it and pinpointing the cause.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* Whitespace-only CDATA content is no longer stripped. A `<![CDATA[ ]]>` section is the author's explicit literal-data marker, so its content is now preserved as text instead of being dropped, on both the Nokogiri and REXML parsers. Previously such a tag parsed to `nil` (or, with attributes, dropped the content and kept only the attributes). Non-whitespace CDATA is unchanged, and ordinary whitespace-only text is still stripped as before. Surfaced while auditing whitespace handling for [#97](https://github.com/savonrb/nori/issues/97), the same empty-tag/whitespace spec-compliance work as the `:consistent_empty_tags` option above.
|
|
19
|
+
|
|
20
|
+
## 2.7.1 (2024-07-28)
|
|
2
21
|
|
|
3
22
|
* Stop monkey-patching String with #snakecase by @mchu in https://github.com/savonrb/nori/pull/102
|
|
4
23
|
|
|
5
|
-
|
|
24
|
+
## 2.7.0 (2024-02-13)
|
|
6
25
|
|
|
7
26
|
* Added support for ruby 3.1, 3.2, 3.3. Dropped support for ruby 2.7 and below.
|
|
8
27
|
* Feature: `Nori::Parser` has a new option, `:scrub_xml`, which defaults to true, for scrubbing invalid characters ([#72](https://github.com/savonrb/nori/pull/72)). This should allow documents containing invalid characters to still be parsed.
|
|
9
28
|
* Fix: REXML parser changes `<` inside CDATA to `<` ([#94](https://github.com/savonrb/nori/pull/94))
|
|
10
29
|
* Change: `Object#blank?` is no longer patched in.
|
|
11
30
|
|
|
12
|
-
|
|
31
|
+
## 2.6.0 (2015-05-06)
|
|
13
32
|
|
|
14
33
|
* Feature: [#69](https://github.com/savonrb/nori/pull/69) Add option to convert empty tags to a value other than nil.
|
|
15
34
|
|
|
16
|
-
|
|
35
|
+
## 2.5.0 (2015-03-31)
|
|
17
36
|
|
|
18
37
|
* Formally drop support for ruby 1.8.7. Installing Nori from rubygems for that version should no longer attempt to install versions that will not work.
|
|
19
38
|
* BREAKING CHANGE: Newlines are now preserved when present in the value of inner text nodes. See the example below:
|
|
@@ -31,7 +50,7 @@ Nori.new.parse("<outer>\n<embedded>\n<one></one>\n<two>&
|
|
|
31
50
|
=> {"outer"=>"<embedded>\n<one></one>\n<two></two>\n<embedded>\n"}
|
|
32
51
|
```
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
## 2.4.0 (2014-04-19)
|
|
35
54
|
|
|
36
55
|
* Change: Dropped support for ruby 1.8, rubinius and ree
|
|
37
56
|
|
|
@@ -39,42 +58,42 @@ Nori.new.parse("<outer>\n<embedded>\n<one></one>\n<two>&
|
|
|
39
58
|
|
|
40
59
|
* Feature: Added `:convert_dashes_to_underscore` option
|
|
41
60
|
|
|
42
|
-
|
|
61
|
+
## 2.3.0 (2013-07-26)
|
|
43
62
|
|
|
44
63
|
* Change: `Nori#find` now ignores namespace prefixes in Hash keys it is searching through.
|
|
45
64
|
|
|
46
65
|
* Fix: Limited Nokogiri to < 1.6, because v1.6 dropped support for Ruby 1.8.
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
## 2.2.0 (2013-04-25)
|
|
49
68
|
|
|
50
69
|
* Feature: [#42](https://github.com/savonrb/nori/pull/42) adds the `:delete_namespace_attributes`
|
|
51
70
|
option to remove namespace attributes like `xmlns:*` or `xsi:*`.
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
## 2.1.0 (2013-04-21)
|
|
54
73
|
|
|
55
74
|
* Feature: Added `Nori.hash_key` and `Nori#find` to work with Hash keys generated by Nori.
|
|
56
75
|
Original issue: [savonrb/savon#393](https://github.com/savonrb/savon/pull/393)
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
## 2.0.4 (2013-02-26)
|
|
59
78
|
|
|
60
79
|
* Fix: [#37](https://github.com/savonrb/nori/issues/37) special characters
|
|
61
80
|
problem on Ruby 1.9.3-p392.
|
|
62
81
|
|
|
63
|
-
|
|
82
|
+
## 2.0.3 (2013-01-10)
|
|
64
83
|
|
|
65
84
|
* Fix for remote code execution bug. For more in-depth information, read about the
|
|
66
85
|
recent [Rails hotfix](https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-security/61bkgvnSGTQ).
|
|
67
86
|
Please make sure to upgrade now!
|
|
68
87
|
|
|
69
|
-
|
|
88
|
+
## 2.0.2 (YANKED)
|
|
70
89
|
|
|
71
90
|
* Yanked because of a problem with XML that starts with an instruction tag.
|
|
72
91
|
|
|
73
|
-
|
|
92
|
+
## 2.0.1 (YANKED)
|
|
74
93
|
|
|
75
94
|
* Yanked because of a problem with XML that starts with an instruction tag.
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
## 2.0.0 (2012-12-12)
|
|
78
97
|
|
|
79
98
|
Please make sure to read the updated README for how to use the new version.
|
|
80
99
|
|
|
@@ -235,3 +254,5 @@ Please make sure to read the updated README for how to use the new version.
|
|
|
235
254
|
|
|
236
255
|
## 0.1.0 2009-03-28
|
|
237
256
|
* Initial release.
|
|
257
|
+
|
|
258
|
+
[2.8.0]: https://github.com/savonrb/nori/compare/v2.7.1...2.8.0
|
data/README.md
CHANGED
|
@@ -107,3 +107,79 @@ parser = Nori.new(:convert_dashes_to_underscores => false)
|
|
|
107
107
|
parser.parse('<any-tag>foo bar</any-tag>')
|
|
108
108
|
# => {"any-tag"=>"foo bar"}
|
|
109
109
|
```
|
|
110
|
+
|
|
111
|
+
## empty_tag_value
|
|
112
|
+
|
|
113
|
+
By default, an empty tag becomes `nil`. The `empty_tag_value` option replaces that value.
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Nori.new.parse('<foo/>')
|
|
117
|
+
# => {"foo"=>nil}
|
|
118
|
+
|
|
119
|
+
Nori.new(:empty_tag_value => "").parse('<foo/>')
|
|
120
|
+
# => {"foo"=>""}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The option does not apply to empty tags with attributes.
|
|
124
|
+
They become a hash of their prefixed attributes instead.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
Nori.new(:empty_tag_value => "").parse('<foo bar="baz"/>')
|
|
128
|
+
# => {"foo"=>{"@bar"=>"baz"}}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## consistent_empty_tags
|
|
132
|
+
|
|
133
|
+
With `consistent_empty_tags`, every empty tag (with or without attributes) becomes the `empty_tag_value`.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
Nori.new(:consistent_empty_tags => true).parse('<foo bar="baz"/>')
|
|
137
|
+
# => {"foo"=>nil}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A string `empty_tag_value` keeps the attributes accessible on the value,
|
|
141
|
+
and an explicit `xsi:nil="true"` always becomes `nil`.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
parser = Nori.new(:consistent_empty_tags => true, :empty_tag_value => "")
|
|
145
|
+
|
|
146
|
+
result = parser.parse('<foo bar="baz"/>')
|
|
147
|
+
# => {"foo"=>""}
|
|
148
|
+
|
|
149
|
+
result["foo"].attributes
|
|
150
|
+
# => {"bar"=>"baz"}
|
|
151
|
+
|
|
152
|
+
parser.parse('<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>')
|
|
153
|
+
# => {"foo"=>nil}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## standards
|
|
157
|
+
|
|
158
|
+
`standards` is a profile that turns on Nori's spec-correct parsing as a group,
|
|
159
|
+
rather than one flag at a time. It is opt-in on the 2.x line and becomes the
|
|
160
|
+
default in Nori 3.0.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
Nori.new(:standards => true)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Members of the profile:
|
|
167
|
+
|
|
168
|
+
- **xml:space honoring** ([XML 1.0 §2.10](https://www.w3.org/TR/xml/#sec-white-space)).
|
|
169
|
+
Whitespace-only text under `xml:space="preserve"` is kept instead of stripped.
|
|
170
|
+
The nearest ancestor that sets `xml:space` wins, and `xml:space="default"`
|
|
171
|
+
resets to the stripping behavior.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
Nori.new(:standards => true).parse('<name xml:space="preserve"> </name>')
|
|
175
|
+
# => {"name"=>" "}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- **the XML string-value model for empty elements.** The profile implies
|
|
179
|
+
`consistent_empty_tags: true` with `empty_tag_value: ""`. Both remain
|
|
180
|
+
overridable, so passing either option explicitly wins over the profile.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
Nori.new(:standards => true).parse('<foo bar="baz"/>')
|
|
184
|
+
# => {"foo"=>""}
|
|
185
|
+
```
|
data/lib/nori/parser/nokogiri.rb
CHANGED
|
@@ -9,23 +9,44 @@ class Nori
|
|
|
9
9
|
module Nokogiri
|
|
10
10
|
|
|
11
11
|
class Document < ::Nokogiri::XML::SAX::Document
|
|
12
|
+
# Marks a text node that came from a CDATA section so the whitespace
|
|
13
|
+
# stripping in {#end_element} and {#characters} leaves it alone.
|
|
14
|
+
# A CDATA section is the author's explicit literal-data marker
|
|
15
|
+
# (XML 1.0 §2.7), so its content is real even when it is only
|
|
16
|
+
# whitespace and is never trimmed. The marker never reaches the
|
|
17
|
+
# output because {XMLUtilityNode#inner_html} joins children into
|
|
18
|
+
# a plain string.
|
|
19
|
+
class CDataText < ::String; end
|
|
20
|
+
|
|
12
21
|
attr_accessor :options
|
|
13
22
|
|
|
14
23
|
def stack
|
|
15
24
|
@stack ||= []
|
|
16
25
|
end
|
|
17
26
|
|
|
27
|
+
# Tracks the xml:space value in effect for each open element so
|
|
28
|
+
# {#preserve_whitespace?} can honor XML 1.0 §2.10 inheritance.
|
|
29
|
+
def space_stack
|
|
30
|
+
@space_stack ||= []
|
|
31
|
+
end
|
|
32
|
+
|
|
18
33
|
def start_element(name, attrs = [])
|
|
19
|
-
|
|
34
|
+
attributes = Hash[*attrs.flatten]
|
|
35
|
+
space_stack.push xml_space(attributes)
|
|
36
|
+
stack.push Nori::XMLUtilityNode.new(options, name, attributes)
|
|
20
37
|
end
|
|
21
38
|
|
|
22
39
|
# To keep backward behaviour compatibility
|
|
23
|
-
# delete last child if it is a space-only text node
|
|
40
|
+
# delete last child if it is a space-only text node.
|
|
41
|
+
# CDATA content is literal and is never stripped, and under
|
|
42
|
+
# xml:space="preserve" whitespace-only text is kept too.
|
|
24
43
|
def end_element(name)
|
|
44
|
+
preserve = preserve_whitespace?
|
|
45
|
+
space_stack.pop
|
|
25
46
|
if stack.size > 1
|
|
26
47
|
last = stack.pop
|
|
27
48
|
maybe_string = last.children.last
|
|
28
|
-
if maybe_string.is_a?(String) and maybe_string.strip.empty?
|
|
49
|
+
if !preserve and maybe_string.is_a?(String) and !maybe_string.is_a?(CDataText) and maybe_string.strip.empty?
|
|
29
50
|
last.children.pop
|
|
30
51
|
end
|
|
31
52
|
stack.last.add_node last
|
|
@@ -34,15 +55,41 @@ class Nori
|
|
|
34
55
|
|
|
35
56
|
# If this node is a successive character then add it as is.
|
|
36
57
|
# First child being a space-only text node will not be added
|
|
37
|
-
# because there is no previous characters
|
|
58
|
+
# because there is no previous characters, unless xml:space="preserve"
|
|
59
|
+
# is in effect.
|
|
38
60
|
def characters(string)
|
|
39
61
|
last = stack.last
|
|
40
|
-
if last and last.children.last.is_a?(String) or string.strip.size > 0
|
|
62
|
+
if last and (preserve_whitespace? or last.children.last.is_a?(String) or string.strip.size > 0)
|
|
41
63
|
last.add_node(string)
|
|
42
64
|
end
|
|
43
65
|
end
|
|
44
66
|
|
|
45
|
-
|
|
67
|
+
# Adds the CDATA section content verbatim.
|
|
68
|
+
def cdata_block(string)
|
|
69
|
+
last = stack.last
|
|
70
|
+
last.add_node(CDataText.new(string)) if last
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# The xml:space value for an element. Its own declaration if it has one,
|
|
76
|
+
# otherwise the value inherited from the nearest ancestor.
|
|
77
|
+
# Defaults to "default". See XML 1.0 §2.10.
|
|
78
|
+
#
|
|
79
|
+
# @param attributes [Hash{String => String}] the element's attributes
|
|
80
|
+
# @return [String] "preserve", "default", or an inherited value
|
|
81
|
+
def xml_space(attributes)
|
|
82
|
+
attributes["xml:space"] || space_stack.last || "default"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Whether whitespace-only text must be kept for the current element.
|
|
86
|
+
# Only the +:standards+ profile honors xml:space. Otherwise
|
|
87
|
+
# insignificant whitespace is stripped as before.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def preserve_whitespace?
|
|
91
|
+
options[:standards] && space_stack.last == "preserve"
|
|
92
|
+
end
|
|
46
93
|
|
|
47
94
|
end
|
|
48
95
|
|
data/lib/nori/parser/rexml.rb
CHANGED
|
@@ -12,6 +12,9 @@ class Nori
|
|
|
12
12
|
|
|
13
13
|
def self.parse(xml, options)
|
|
14
14
|
stack = []
|
|
15
|
+
# Tracks the xml:space value in effect for each open element so
|
|
16
|
+
# whitespace-only text can be kept under xml:space="preserve".
|
|
17
|
+
space_stack = []
|
|
15
18
|
parser = ::REXML::Parsers::BaseParser.new(xml)
|
|
16
19
|
|
|
17
20
|
while true
|
|
@@ -23,21 +26,47 @@ class Nori
|
|
|
23
26
|
when :end_doctype, :start_doctype
|
|
24
27
|
# do nothing
|
|
25
28
|
when :start_element
|
|
29
|
+
space_stack.push xml_space(event[2], space_stack)
|
|
26
30
|
stack.push Nori::XMLUtilityNode.new(options, event[1], event[2])
|
|
27
31
|
when :end_element
|
|
32
|
+
space_stack.pop
|
|
28
33
|
if stack.size > 1
|
|
29
34
|
temp = stack.pop
|
|
30
35
|
stack.last.add_node(temp)
|
|
31
36
|
end
|
|
32
37
|
when :text
|
|
33
|
-
|
|
38
|
+
# Whitespace-only text is insignificant and stripped, unless
|
|
39
|
+
# xml:space="preserve" is in effect under the :standards profile.
|
|
40
|
+
keep = preserve_whitespace?(options, space_stack) || event[1].strip.length > 0
|
|
41
|
+
stack.last.add_node(event[1]) if keep && !stack.empty?
|
|
34
42
|
when :cdata
|
|
35
|
-
|
|
43
|
+
# CDATA is the author's explicit literal-data marker (XML 1.0 §2.7),
|
|
44
|
+
# so whitespace-only CDATA is kept rather than stripped like text.
|
|
45
|
+
stack.last.add_node(raw_data[1]) unless stack.empty?
|
|
36
46
|
end
|
|
37
47
|
end
|
|
38
48
|
stack.length > 0 ? stack.pop.to_hash : {}
|
|
39
49
|
end
|
|
40
50
|
|
|
51
|
+
# The xml:space value for an element: its own declaration if it has
|
|
52
|
+
# one, otherwise the value inherited from the nearest ancestor.
|
|
53
|
+
# Defaults to "default". See XML 1.0 §2.10.
|
|
54
|
+
#
|
|
55
|
+
# @param attributes [Hash{String => String}] the element's attributes
|
|
56
|
+
# @param space_stack [Array<String>] the ancestor xml:space values
|
|
57
|
+
# @return [String] "preserve", "default", or an inherited value
|
|
58
|
+
def self.xml_space(attributes, space_stack)
|
|
59
|
+
(attributes && attributes["xml:space"]) || space_stack.last || "default"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Whether whitespace-only text must be kept for the current element.
|
|
63
|
+
# Only the +:standards+ profile honors xml:space.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def self.preserve_whitespace?(options, space_stack)
|
|
67
|
+
options[:standards] && space_stack.last == "preserve"
|
|
68
|
+
end
|
|
69
|
+
|
|
41
70
|
def self.unnormalize(event)
|
|
42
71
|
event.map do |el|
|
|
43
72
|
if el.is_a?(String)
|
data/lib/nori/string_io_file.rb
CHANGED
data/lib/nori/version.rb
CHANGED
|
@@ -127,59 +127,21 @@ class Nori
|
|
|
127
127
|
@children << node
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# Converts the node into a hash with the node name as its single key.
|
|
131
|
+
#
|
|
132
|
+
# The value depends on the shape of the node. A node typed as "file"
|
|
133
|
+
# becomes a {StringIOFile}. A node with text content becomes a typecast
|
|
134
|
+
# scalar. Every other node folds its children into an array or a hash.
|
|
135
|
+
#
|
|
136
|
+
# @return [Hash{String => Object}] the node name mapped to its value
|
|
130
137
|
def to_hash
|
|
131
|
-
if @type == "file"
|
|
132
|
-
|
|
133
|
-
f.original_filename = attributes['name'] || 'untitled'
|
|
134
|
-
f.content_type = attributes['content_type'] || 'application/octet-stream'
|
|
135
|
-
return { name => f }
|
|
136
|
-
end
|
|
138
|
+
return { name => file_value } if @type == "file"
|
|
139
|
+
return { name => text_value } if @text
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if t.is_a?(String)
|
|
143
|
-
t = StringWithAttributes.new(t)
|
|
144
|
-
t.attributes = attributes
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
return { name => t }
|
|
148
|
-
else
|
|
149
|
-
#change repeating groups into an array
|
|
150
|
-
groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
|
|
151
|
-
|
|
152
|
-
out = nil
|
|
153
|
-
if @type == "array"
|
|
154
|
-
out = []
|
|
155
|
-
groups.each do |k, v|
|
|
156
|
-
if v.size == 1
|
|
157
|
-
out << v.first.to_hash.entries.first.last
|
|
158
|
-
else
|
|
159
|
-
out << v.map{|e| e.to_hash[k]}
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
out = out.flatten
|
|
163
|
-
|
|
164
|
-
else # If Hash
|
|
165
|
-
out = {}
|
|
166
|
-
groups.each do |k,v|
|
|
167
|
-
if v.size == 1
|
|
168
|
-
out.merge!(v.first)
|
|
169
|
-
else
|
|
170
|
-
out.merge!( k => v.map{|e| e.to_hash[k]})
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
out.merge! prefixed_attributes unless attributes.empty?
|
|
174
|
-
out = out.empty? ? @options[:empty_tag_value] : out
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
if @type && out.nil?
|
|
178
|
-
{ name => typecast_value(out) }
|
|
179
|
-
else
|
|
180
|
-
{ name => out }
|
|
181
|
-
end
|
|
182
|
-
end
|
|
141
|
+
groups = group_children
|
|
142
|
+
value = @type == "array" ? array_value(groups) : hash_value(groups)
|
|
143
|
+
value = typecast_value(value) if @type && value.nil?
|
|
144
|
+
{ name => value }
|
|
183
145
|
end
|
|
184
146
|
|
|
185
147
|
# Typecasts a value based upon its type. For instance, if
|
|
@@ -248,6 +210,82 @@ class Nori
|
|
|
248
210
|
alias to_s to_html
|
|
249
211
|
|
|
250
212
|
private
|
|
213
|
+
|
|
214
|
+
# Decodes the base64 content of a node typed as "file" into a
|
|
215
|
+
# {StringIOFile} carrying the filename and content type attributes.
|
|
216
|
+
def file_value
|
|
217
|
+
file = StringIOFile.new((@children.first || '').unpack('m').first)
|
|
218
|
+
file.original_filename = attributes['name'] || 'untitled'
|
|
219
|
+
file.content_type = attributes['content_type'] || 'application/octet-stream'
|
|
220
|
+
file
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Typecasts the text content of the node. String results are wrapped
|
|
224
|
+
# so the node's attributes stay accessible on the value.
|
|
225
|
+
def text_value
|
|
226
|
+
value = typecast_value(inner_html)
|
|
227
|
+
value = advanced_typecasting(value) if value.is_a?(String) && @options[:advanced_typecasting]
|
|
228
|
+
value.is_a?(String) ? string_with_attributes(value) : value
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Groups the child nodes by their name so repeating siblings can be
|
|
232
|
+
# folded into arrays.
|
|
233
|
+
#
|
|
234
|
+
# @return [Hash{String => Array<XMLUtilityNode>}]
|
|
235
|
+
def group_children
|
|
236
|
+
@children.inject({}) { |hash, child| (hash[child.name] ||= []) << child; hash }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Collects the values of all child nodes for a node typed as "array".
|
|
240
|
+
def array_value(groups)
|
|
241
|
+
values = []
|
|
242
|
+
groups.each do |child_name, nodes|
|
|
243
|
+
if nodes.size == 1
|
|
244
|
+
values << nodes.first.to_hash.entries.first.last
|
|
245
|
+
else
|
|
246
|
+
values << nodes.map { |node| node.to_hash[child_name] }
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
values.flatten
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Folds the child nodes and the prefixed attributes into a hash.
|
|
253
|
+
# An empty result becomes the :empty_tag_value option.
|
|
254
|
+
def hash_value(groups)
|
|
255
|
+
return consistent_empty_value if @options[:consistent_empty_tags] && groups.empty?
|
|
256
|
+
|
|
257
|
+
value = {}
|
|
258
|
+
groups.each do |child_name, nodes|
|
|
259
|
+
if nodes.size == 1
|
|
260
|
+
value.merge!(nodes.first.to_hash)
|
|
261
|
+
else
|
|
262
|
+
value.merge!(child_name => nodes.map { |node| node.to_hash[child_name] })
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
value.merge!(prefixed_attributes) unless attributes.empty?
|
|
266
|
+
value.empty? ? @options[:empty_tag_value] : value
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Resolves an element without children and without text when the
|
|
270
|
+
# :consistent_empty_tags option is set. The element becomes the
|
|
271
|
+
# :empty_tag_value option no matter which attributes it carries.
|
|
272
|
+
# A string value keeps the attributes accessible on the value.
|
|
273
|
+
# An explicit xsi:nil="true" wins over the option and becomes nil.
|
|
274
|
+
def consistent_empty_value
|
|
275
|
+
return nil if @nil_element
|
|
276
|
+
|
|
277
|
+
value = @options[:empty_tag_value]
|
|
278
|
+
value.is_a?(String) ? string_with_attributes(value) : value
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Wraps a string value so the node's attributes stay accessible
|
|
282
|
+
# through {StringWithAttributes#attributes}.
|
|
283
|
+
def string_with_attributes(value)
|
|
284
|
+
string = StringWithAttributes.new(value)
|
|
285
|
+
string.attributes = attributes
|
|
286
|
+
string
|
|
287
|
+
end
|
|
288
|
+
|
|
251
289
|
def try_to_convert(value, &block)
|
|
252
290
|
block.call(value)
|
|
253
291
|
rescue ArgumentError
|
data/lib/nori.rb
CHANGED
|
@@ -20,14 +20,16 @@ class Nori
|
|
|
20
20
|
:convert_tags_to => nil,
|
|
21
21
|
:convert_attributes_to => nil,
|
|
22
22
|
:empty_tag_value => nil,
|
|
23
|
+
:consistent_empty_tags => false,
|
|
23
24
|
:advanced_typecasting => true,
|
|
24
25
|
:convert_dashes_to_underscores => true,
|
|
25
26
|
:scrub_xml => true,
|
|
27
|
+
:standards => false,
|
|
26
28
|
:parser => :nokogiri
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
validate_options! defaults.keys, options.keys
|
|
30
|
-
@options = defaults.merge(options)
|
|
32
|
+
@options = defaults.merge(standards_defaults(options)).merge(options)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def find(hash, *path)
|
|
@@ -49,6 +51,24 @@ class Nori
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
private
|
|
54
|
+
|
|
55
|
+
# The defaults implied by the +:standards+ profile.
|
|
56
|
+
#
|
|
57
|
+
# The profile groups the spec-correct behaviors under a single opt-in.
|
|
58
|
+
# It turns on the XML string-value model for empty elements
|
|
59
|
+
# (+:consistent_empty_tags+ with an empty-string +:empty_tag_value+) and,
|
|
60
|
+
# in the parsers, xml:space honoring. These are defaults, so an explicit
|
|
61
|
+
# +:consistent_empty_tags+ or +:empty_tag_value+ passed by the caller
|
|
62
|
+
# still wins. When the profile is off the hash is empty and parsing is
|
|
63
|
+
# unchanged.
|
|
64
|
+
#
|
|
65
|
+
# @param options [Hash] the options passed to {#initialize}
|
|
66
|
+
# @return [Hash] the implied defaults, or +{}+ when the profile is off
|
|
67
|
+
def standards_defaults(options)
|
|
68
|
+
return {} unless options[:standards]
|
|
69
|
+
{ :consistent_empty_tags => true, :empty_tag_value => "" }
|
|
70
|
+
end
|
|
71
|
+
|
|
52
72
|
def load_parser(parser)
|
|
53
73
|
require "nori/parser/#{parser}"
|
|
54
74
|
Parser.const_get PARSERS[parser]
|
metadata
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nori
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Harrington
|
|
8
8
|
- John Nunemaker
|
|
9
9
|
- Wynn Netherland
|
|
10
|
-
autorequire:
|
|
11
10
|
bindir: bin
|
|
12
11
|
cert_chain: []
|
|
13
|
-
date:
|
|
12
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
14
13
|
dependencies:
|
|
15
14
|
- !ruby/object:Gem::Dependency
|
|
16
15
|
name: bigdecimal
|
|
@@ -26,20 +25,34 @@ dependencies:
|
|
|
26
25
|
- - ">="
|
|
27
26
|
- !ruby/object:Gem::Version
|
|
28
27
|
version: '0'
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: stringio
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0'
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
29
42
|
- !ruby/object:Gem::Dependency
|
|
30
43
|
name: rake
|
|
31
44
|
requirement: !ruby/object:Gem::Requirement
|
|
32
45
|
requirements:
|
|
33
46
|
- - "~>"
|
|
34
47
|
- !ruby/object:Gem::Version
|
|
35
|
-
version:
|
|
48
|
+
version: '13.3'
|
|
36
49
|
type: :development
|
|
37
50
|
prerelease: false
|
|
38
51
|
version_requirements: !ruby/object:Gem::Requirement
|
|
39
52
|
requirements:
|
|
40
53
|
- - "~>"
|
|
41
54
|
- !ruby/object:Gem::Version
|
|
42
|
-
version:
|
|
55
|
+
version: '13.3'
|
|
43
56
|
- !ruby/object:Gem::Dependency
|
|
44
57
|
name: nokogiri
|
|
45
58
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -74,18 +87,9 @@ executables: []
|
|
|
74
87
|
extensions: []
|
|
75
88
|
extra_rdoc_files: []
|
|
76
89
|
files:
|
|
77
|
-
- ".devcontainer/devcontainer.json"
|
|
78
|
-
- ".github/dependabot.yml"
|
|
79
|
-
- ".github/workflows/test.yml"
|
|
80
|
-
- ".gitignore"
|
|
81
|
-
- ".rspec"
|
|
82
90
|
- CHANGELOG.md
|
|
83
|
-
- Gemfile
|
|
84
91
|
- LICENSE
|
|
85
92
|
- README.md
|
|
86
|
-
- Rakefile
|
|
87
|
-
- benchmark/benchmark.rb
|
|
88
|
-
- benchmark/soap_response.xml
|
|
89
93
|
- lib/nori.rb
|
|
90
94
|
- lib/nori/core_ext.rb
|
|
91
95
|
- lib/nori/core_ext/hash.rb
|
|
@@ -96,18 +100,11 @@ files:
|
|
|
96
100
|
- lib/nori/string_with_attributes.rb
|
|
97
101
|
- lib/nori/version.rb
|
|
98
102
|
- lib/nori/xml_utility_node.rb
|
|
99
|
-
- nori.gemspec
|
|
100
|
-
- spec/nori/api_spec.rb
|
|
101
|
-
- spec/nori/core_ext/hash_spec.rb
|
|
102
|
-
- spec/nori/nori_spec.rb
|
|
103
|
-
- spec/nori/string_utils_spec.rb
|
|
104
|
-
- spec/spec_helper.rb
|
|
105
103
|
homepage: https://github.com/savonrb/nori
|
|
106
104
|
licenses:
|
|
107
105
|
- MIT
|
|
108
106
|
metadata:
|
|
109
107
|
rubygems_mfa_required: 'true'
|
|
110
|
-
post_install_message:
|
|
111
108
|
rdoc_options: []
|
|
112
109
|
require_paths:
|
|
113
110
|
- lib
|
|
@@ -122,8 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
122
119
|
- !ruby/object:Gem::Version
|
|
123
120
|
version: '0'
|
|
124
121
|
requirements: []
|
|
125
|
-
rubygems_version: 3.
|
|
126
|
-
signing_key:
|
|
122
|
+
rubygems_version: 3.6.9
|
|
127
123
|
specification_version: 4
|
|
128
124
|
summary: XML to Hash translator
|
|
129
125
|
test_files: []
|