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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e562b494afc72df387f136e51af0c5e0445b528b5515eedad7f60b3d49d207a5
4
- data.tar.gz: 5eabad83b50392e39973058c70d6307ddaaeea47934c292fbd472ee0f93680f9
3
+ metadata.gz: ac639b2fdf06325300d7a65de3e8de963d6b6deed94eba8a5c5560ec99e29e28
4
+ data.tar.gz: 5837f3e91314f284171ac6b97ebd21d8b6d2bcd769aea32c854c4818523a15da
5
5
  SHA512:
6
- metadata.gz: 6366a2ac0d9dd99e4fd7b0f32d95dfb21f76885ab18fc7f3f7767c5e257b61e3817d2bf13f9a113feb2482fdb6f5a912956ef5d17a3f868a9139e9b55683402f
7
- data.tar.gz: cf295850b06961ed67644a8c06c61152cecf5c8512f8a74a03115e47fe8f9a567300d9af8d91504013777400882869cb7016c80563015e6423e86718c7b828bd
6
+ metadata.gz: b315e0f90e75a5d225d83b0248b7d5e554ad1eab8e06b37e7cd25c5dacce146bfff6ef3e40e7ff37d4e07eb142b255b6d7a026512cd1f0d94622dcc6669d0ff0
7
+ data.tar.gz: 5fa332d507687ea25c1f2517944aff167916275f149ed590d097bc04aec39b3cf57532eeab5cee37167df4c69e68267908f80ef3c223dd84bf3c1baf881e3815
data/CHANGELOG.md CHANGED
@@ -1,19 +1,38 @@
1
- # 2.7.1 (2024-07-28)
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
- # 2.7.0 (2024-02-13)
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 `&lt;` inside CDATA to `<` ([#94](https://github.com/savonrb/nori/pull/94))
10
29
  * Change: `Object#blank?` is no longer patched in.
11
30
 
12
- # 2.6.0 (2015-05-06)
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
- # 2.5.0 (2015-03-31)
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&lt;embedded&gt;\n&lt;one&gt;&lt;/one&gt;\n&lt;two&gt;&
31
50
  => {"outer"=>"<embedded>\n<one></one>\n<two></two>\n<embedded>\n"}
32
51
  ```
33
52
 
34
- # 2.4.0 (2014-04-19)
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&lt;embedded&gt;\n&lt;one&gt;&lt;/one&gt;\n&lt;two&gt;&
39
58
 
40
59
  * Feature: Added `:convert_dashes_to_underscore` option
41
60
 
42
- # 2.3.0 (2013-07-26)
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
- # 2.2.0 (2013-04-25)
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
- # 2.1.0 (2013-04-21)
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
- # 2.0.4 (2013-02-26)
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
- # 2.0.3 (2013-01-10)
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
- # 2.0.2 (YANKED)
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
- # 2.0.1 (YANKED)
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
- # 2.0.0 (2012-12-12)
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
+ ```
@@ -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
- stack.push Nori::XMLUtilityNode.new(options, name, Hash[*attrs.flatten])
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
- alias cdata_block characters
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
 
@@ -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
- stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
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
- stack.last.add_node(raw_data[1]) unless raw_data[1].strip.length == 0 || stack.empty?
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)
@@ -1,3 +1,5 @@
1
+ require "stringio"
2
+
1
3
  class Nori
2
4
  class StringIOFile < StringIO
3
5
 
data/lib/nori/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Nori
2
- VERSION = '2.7.1'
2
+ VERSION = '2.8.0'
3
3
  end
@@ -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
- f = StringIOFile.new((@children.first || '').unpack('m').first)
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
- if @text
139
- t = typecast_value(inner_html)
140
- t = advanced_typecasting(t) if t.is_a?(String) && @options[:advanced_typecasting]
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.7.1
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: 2024-07-28 00:00:00.000000000 Z
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: 12.3.3
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: 12.3.3
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.4.10
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: []