nori 2.7.1 → 2.9.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 +45 -13
- data/README.md +136 -2
- 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 +152 -54
- data/lib/nori.rb +27 -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: d2ffd841cec28588977a4cf413ade2a33ceba13466046a1299e27016231db0ef
|
|
4
|
+
data.tar.gz: 51edbdf4ab6413ca7583bb02e05a88ad13fac97741797cf5dde6135edd99d5f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1700483ec9b91f559ab0fa138c5760fe237f7bf92f5b927bbde60dc437175af5e8c8cc12998195bd875ea9272f62d25727e3fe85a5954918cc0ea31aa5a413a3
|
|
7
|
+
data.tar.gz: 8713852dfe40adfe96ae00debff569d58f6d8c394fd48fe8df7ad34a767e886605df4163d15cd8573922926ad7abb9f3af1135ddd7180301172dfef5b6b718c6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
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.9.0] - 2026-07-05
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
* The `:standards` profile no longer applies schema-less typing. Under `Nori.new(standards: true)`, `:advanced_typecasting` now defaults to `false` (an explicit `advanced_typecasting: true` still wins), and the bare un-namespaced `type=` and `nil=` attributes become ordinary attributes instead of casting instructions. No more `type=` conversions (`integer`, `date`, `decimal`, `array`, `file`, ...), and only a prefixed `xsi:nil="true"` marks an element nil. These conventions come from Rails' `Hash.from_xml` (inherited via crack and merb), not from any XML spec. Without a schema, character data is just text. Knowing that `<id>123</id>` holds an integer is the business of a schema-aware layer, not a guess from string shape. Parsing without the profile is completely unchanged.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
* Add the `:serializable` profile option. `Nori.new(serializable: true)` returns plain, directly-serializable data with no custom value classes. A text node that also has attributes becomes a Hash in the XML JSON convention (`{"#text" => content}` merged with our existing `@`-prefixed attributes instead of a `Nori::StringWithAttributes`, and a text node without attributes becomes a plain `String`. A `type="file"` node is no longer decoded into a `Nori::StringIOFile`, it folds into text and attributes like any other node, leaving the base64 content untouched for the caller to decode. That `type=` decoding is a Rails/ActiveSupport `Hash.from_xml` convention Nori inherited via crack and merb, not part of any XML spec, which is why the plain-data profile drops it. With this, values survive `to_json`, `to_yaml`, and `Marshal`. Opt-in on the 2.x line, probably the default in Nori 3.0. Reported by @ArnoldMEDLINQ ([#107](https://github.com/savonrb/nori/issues/107)), thanks to @dub357 for pinpointing the cause and finding [#106](https://github.com/savonrb/nori/pull/106), and thanks to @ekzobrain whose hash-shape approach this builds on.
|
|
17
|
+
|
|
18
|
+
## [2.8.0] - 2026-07-04
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
* 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).
|
|
23
|
+
|
|
24
|
+
* [#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.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
* 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.
|
|
29
|
+
|
|
30
|
+
## 2.7.1 (2024-07-28)
|
|
2
31
|
|
|
3
32
|
* Stop monkey-patching String with #snakecase by @mchu in https://github.com/savonrb/nori/pull/102
|
|
4
33
|
|
|
5
|
-
|
|
34
|
+
## 2.7.0 (2024-02-13)
|
|
6
35
|
|
|
7
36
|
* Added support for ruby 3.1, 3.2, 3.3. Dropped support for ruby 2.7 and below.
|
|
8
37
|
* 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
38
|
* Fix: REXML parser changes `<` inside CDATA to `<` ([#94](https://github.com/savonrb/nori/pull/94))
|
|
10
39
|
* Change: `Object#blank?` is no longer patched in.
|
|
11
40
|
|
|
12
|
-
|
|
41
|
+
## 2.6.0 (2015-05-06)
|
|
13
42
|
|
|
14
43
|
* Feature: [#69](https://github.com/savonrb/nori/pull/69) Add option to convert empty tags to a value other than nil.
|
|
15
44
|
|
|
16
|
-
|
|
45
|
+
## 2.5.0 (2015-03-31)
|
|
17
46
|
|
|
18
47
|
* 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
48
|
* BREAKING CHANGE: Newlines are now preserved when present in the value of inner text nodes. See the example below:
|
|
@@ -31,7 +60,7 @@ Nori.new.parse("<outer>\n<embedded>\n<one></one>\n<two>&
|
|
|
31
60
|
=> {"outer"=>"<embedded>\n<one></one>\n<two></two>\n<embedded>\n"}
|
|
32
61
|
```
|
|
33
62
|
|
|
34
|
-
|
|
63
|
+
## 2.4.0 (2014-04-19)
|
|
35
64
|
|
|
36
65
|
* Change: Dropped support for ruby 1.8, rubinius and ree
|
|
37
66
|
|
|
@@ -39,42 +68,42 @@ Nori.new.parse("<outer>\n<embedded>\n<one></one>\n<two>&
|
|
|
39
68
|
|
|
40
69
|
* Feature: Added `:convert_dashes_to_underscore` option
|
|
41
70
|
|
|
42
|
-
|
|
71
|
+
## 2.3.0 (2013-07-26)
|
|
43
72
|
|
|
44
73
|
* Change: `Nori#find` now ignores namespace prefixes in Hash keys it is searching through.
|
|
45
74
|
|
|
46
75
|
* Fix: Limited Nokogiri to < 1.6, because v1.6 dropped support for Ruby 1.8.
|
|
47
76
|
|
|
48
|
-
|
|
77
|
+
## 2.2.0 (2013-04-25)
|
|
49
78
|
|
|
50
79
|
* Feature: [#42](https://github.com/savonrb/nori/pull/42) adds the `:delete_namespace_attributes`
|
|
51
80
|
option to remove namespace attributes like `xmlns:*` or `xsi:*`.
|
|
52
81
|
|
|
53
|
-
|
|
82
|
+
## 2.1.0 (2013-04-21)
|
|
54
83
|
|
|
55
84
|
* Feature: Added `Nori.hash_key` and `Nori#find` to work with Hash keys generated by Nori.
|
|
56
85
|
Original issue: [savonrb/savon#393](https://github.com/savonrb/savon/pull/393)
|
|
57
86
|
|
|
58
|
-
|
|
87
|
+
## 2.0.4 (2013-02-26)
|
|
59
88
|
|
|
60
89
|
* Fix: [#37](https://github.com/savonrb/nori/issues/37) special characters
|
|
61
90
|
problem on Ruby 1.9.3-p392.
|
|
62
91
|
|
|
63
|
-
|
|
92
|
+
## 2.0.3 (2013-01-10)
|
|
64
93
|
|
|
65
94
|
* Fix for remote code execution bug. For more in-depth information, read about the
|
|
66
95
|
recent [Rails hotfix](https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-security/61bkgvnSGTQ).
|
|
67
96
|
Please make sure to upgrade now!
|
|
68
97
|
|
|
69
|
-
|
|
98
|
+
## 2.0.2 (YANKED)
|
|
70
99
|
|
|
71
100
|
* Yanked because of a problem with XML that starts with an instruction tag.
|
|
72
101
|
|
|
73
|
-
|
|
102
|
+
## 2.0.1 (YANKED)
|
|
74
103
|
|
|
75
104
|
* Yanked because of a problem with XML that starts with an instruction tag.
|
|
76
105
|
|
|
77
|
-
|
|
106
|
+
## 2.0.0 (2012-12-12)
|
|
78
107
|
|
|
79
108
|
Please make sure to read the updated README for how to use the new version.
|
|
80
109
|
|
|
@@ -235,3 +264,6 @@ Please make sure to read the updated README for how to use the new version.
|
|
|
235
264
|
|
|
236
265
|
## 0.1.0 2009-03-28
|
|
237
266
|
* Initial release.
|
|
267
|
+
|
|
268
|
+
[2.9.0]: https://github.com/savonrb/nori/compare/v2.8.0...v2.9.0
|
|
269
|
+
[2.8.0]: https://github.com/savonrb/nori/compare/v2.7.1...v2.8.0
|
data/README.md
CHANGED
|
@@ -2,8 +2,8 @@ Nori
|
|
|
2
2
|
====
|
|
3
3
|
|
|
4
4
|
[](https://github.com/savonrb/nori/actions/workflows/test.yml)
|
|
5
|
-
[](https://rubygems.org/gems/nori)
|
|
6
|
+
[](https://coveralls.io/github/savonrb/nori?branch=main)
|
|
7
7
|
|
|
8
8
|
Really simple XML parsing ripped from Crack, which ripped it from Merb.
|
|
9
9
|
|
|
@@ -50,6 +50,11 @@ result["foo"].attributes
|
|
|
50
50
|
# => {"bar"=>"baz"}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
Because `StringWithAttributes` hides the attributes on an accessor, they are
|
|
54
|
+
lost on a plain `to_json` and callers have to type-check each value. The
|
|
55
|
+
[`serializable`](#serializable) profile returns a plain Hash instead and will
|
|
56
|
+
probably be the default in 3.0.
|
|
57
|
+
|
|
53
58
|
## advanced_typecasting
|
|
54
59
|
|
|
55
60
|
Nori can automatically convert string values to `TrueClass`, `FalseClass`, `Time`, `Date`, and `DateTime`:
|
|
@@ -107,3 +112,132 @@ parser = Nori.new(:convert_dashes_to_underscores => false)
|
|
|
107
112
|
parser.parse('<any-tag>foo bar</any-tag>')
|
|
108
113
|
# => {"any-tag"=>"foo bar"}
|
|
109
114
|
```
|
|
115
|
+
|
|
116
|
+
## empty_tag_value
|
|
117
|
+
|
|
118
|
+
By default, an empty tag becomes `nil`. The `empty_tag_value` option replaces that value.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
Nori.new.parse('<foo/>')
|
|
122
|
+
# => {"foo"=>nil}
|
|
123
|
+
|
|
124
|
+
Nori.new(:empty_tag_value => "").parse('<foo/>')
|
|
125
|
+
# => {"foo"=>""}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The option does not apply to empty tags with attributes.
|
|
129
|
+
They become a hash of their prefixed attributes instead.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
Nori.new(:empty_tag_value => "").parse('<foo bar="baz"/>')
|
|
133
|
+
# => {"foo"=>{"@bar"=>"baz"}}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## consistent_empty_tags
|
|
137
|
+
|
|
138
|
+
With `consistent_empty_tags`, every empty tag (with or without attributes) becomes the `empty_tag_value`.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Nori.new(:consistent_empty_tags => true).parse('<foo bar="baz"/>')
|
|
142
|
+
# => {"foo"=>nil}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A string `empty_tag_value` keeps the attributes accessible on the value,
|
|
146
|
+
and an explicit `xsi:nil="true"` always becomes `nil`.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
parser = Nori.new(:consistent_empty_tags => true, :empty_tag_value => "")
|
|
150
|
+
|
|
151
|
+
result = parser.parse('<foo bar="baz"/>')
|
|
152
|
+
# => {"foo"=>""}
|
|
153
|
+
|
|
154
|
+
result["foo"].attributes
|
|
155
|
+
# => {"bar"=>"baz"}
|
|
156
|
+
|
|
157
|
+
parser.parse('<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>')
|
|
158
|
+
# => {"foo"=>nil}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## standards
|
|
162
|
+
|
|
163
|
+
`standards` is a profile that turns on Nori's spec-correct parsing as a group,
|
|
164
|
+
rather than one flag at a time. It is opt-in on the 2.x line and becomes the
|
|
165
|
+
default in Nori 3.0.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
Nori.new(:standards => true)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Members of the profile:
|
|
172
|
+
|
|
173
|
+
- **xml:space honoring** ([XML 1.0 §2.10](https://www.w3.org/TR/xml/#sec-white-space)).
|
|
174
|
+
Whitespace-only text under `xml:space="preserve"` is kept instead of stripped.
|
|
175
|
+
The nearest ancestor that sets `xml:space` wins, and `xml:space="default"`
|
|
176
|
+
resets to the stripping behavior.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
Nori.new(:standards => true).parse('<name xml:space="preserve"> </name>')
|
|
180
|
+
# => {"name"=>" "}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- **the XML string-value model for empty elements.** The profile implies
|
|
184
|
+
`consistent_empty_tags: true` with `empty_tag_value: ""`. Both remain
|
|
185
|
+
overridable, so passing either option explicitly wins over the profile.
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Nori.new(:standards => true).parse('<foo bar="baz"/>')
|
|
189
|
+
# => {"foo"=>""}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- **no schema-less typing.** Without a schema, character data is just text, so
|
|
193
|
+
the profile implies `advanced_typecasting: false` (overridable) and ignores
|
|
194
|
+
the bare `type=` and `nil=` attribute conventions that Nori inherited from
|
|
195
|
+
Rails' `Hash.from_xml`. A `type="integer"` no longer casts, a `type="array"`
|
|
196
|
+
no longer folds children into an Array, and both stay visible as ordinary
|
|
197
|
+
attributes. Only the prefixed `xsi:nil="true"` still marks an element nil,
|
|
198
|
+
because that convention comes from XML Schema Instance itself.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
Nori.new(:standards => true).parse('<id type="integer">123</id>')
|
|
202
|
+
# => {"id"=>"123"} (with "type" accessible via #attributes)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## serializable
|
|
206
|
+
|
|
207
|
+
`serializable` is a profile that makes Nori return plain, directly-serializable
|
|
208
|
+
data with no custom value classes. It is opt-in on the 2.x line and will
|
|
209
|
+
probably become the default in 3.0.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
Nori.new(:serializable => true)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Members of the profile:
|
|
216
|
+
|
|
217
|
+
- **text nodes with attributes become a Hash.** A tag with both text content
|
|
218
|
+
and attributes maps to the XML JSON convention (`{"#text" => content}` merged
|
|
219
|
+
with the `@`-prefixed attributes) instead of a `Nori::StringWithAttributes`.
|
|
220
|
+
This is the same `@`-keyed shape element nodes already use, so the attributes
|
|
221
|
+
survive `to_json`, `to_yaml`, and `Marshal`. A text node without attributes
|
|
222
|
+
stays a plain `String`.
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
Nori.new(:serializable => true).parse('<foo bar="baz">Content</foo>')
|
|
226
|
+
# => {"foo"=>{"#text"=>"Content", "@bar"=>"baz"}}
|
|
227
|
+
|
|
228
|
+
Nori.new(:serializable => true).parse('<foo>Content</foo>')
|
|
229
|
+
# => {"foo"=>"Content"}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- **`type="file"` nodes are not decoded.** Without the profile, a node carrying
|
|
233
|
+
`type="file"` is base64-decoded into a `Nori::StringIOFile` with its filename
|
|
234
|
+
and content type. The profile skips that. The node folds into text and
|
|
235
|
+
attributes like any other, leaving the base64 content as a plain `String` for
|
|
236
|
+
the caller to decode. This `type=` decoding is a Rails/ActiveSupport
|
|
237
|
+
`Hash.from_xml` convention Nori inherited through crack and merb, not part of
|
|
238
|
+
any XML specification, so the plain-data profile leaves it behind.
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
Nori.new(:serializable => true).parse('<doc type="file" name="x.pdf">aGVsbG8=</doc>')
|
|
242
|
+
# => {"doc"=>{"#text"=>"aGVsbG8=", "@type"=>"file", "@name"=>"x.pdf"}}
|
|
243
|
+
```
|
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