canon 0.3.1 → 0.3.3

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: 8180c4d10ad157f5fd7d401d7c4723a70d53daa90894a43c33016253d0355093
4
- data.tar.gz: 513538a92c8f209b2eeec7ecf2adedf9e2a86de8e2c6c0e807b45522afffa9b3
3
+ metadata.gz: 52174949c77e8f33286b2875ac5e4052e6f5671b64e53e48a3b0b3128bb598a1
4
+ data.tar.gz: 0b2efb1c4fa7e060f6d54f1c149bd775d45fab66e09ac7bbf306f0a6142360fa
5
5
  SHA512:
6
- metadata.gz: 15bbb37c41f2fe0809be3a1ae9e6e3bf438d7d509eb478b92eb1ace044d511017a257409024c0c7e042bf5dc146af0dbbfc9397c6ffea13a5d410fb9019c716d
7
- data.tar.gz: 81c275d67d47bfeb87a1c5969e90643da41e8816e882a40fa5affa8337fbd3c5bd0ee82428ef63b36bc5dff8338776e8df4da53394d3319663f3167433664785
6
+ metadata.gz: 4e78d8618af59c255979bf3987a1be3e7419e009354b73ca64df75600ce8c2f1cc90aa515eb14be97149fe5426243efacda4ff42eab63b5fc8f291407baa046e
7
+ data.tar.gz: 2596286ac94921e6021590fe6f1ece71d827145743cc993f703cfca079fb91472845c67f25a3e9ad03aecd471c6cc482cd78854a5e9ce85d16bd8f0ea5643636
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -154,11 +154,12 @@ module Canon
154
154
  end
155
155
 
156
156
  def self.build_node_from_nokogiri(nokogiri_node,
157
- preserve_whitespace: false)
157
+ preserve_whitespace: false, inherited_namespaces: nil)
158
158
  case nokogiri_node
159
159
  when Nokogiri::XML::Element
160
160
  build_element_node(nokogiri_node,
161
- preserve_whitespace: preserve_whitespace)
161
+ preserve_whitespace: preserve_whitespace,
162
+ inherited_namespaces: inherited_namespaces)
162
163
  when Nokogiri::XML::Text, Nokogiri::XML::CDATA
163
164
  build_text_node(nokogiri_node,
164
165
  preserve_whitespace: preserve_whitespace)
@@ -169,56 +170,44 @@ preserve_whitespace: false)
169
170
  end
170
171
  end
171
172
 
172
- def self.build_element_node(nokogiri_element, preserve_whitespace: false)
173
+ def self.build_element_node(nokogiri_element, preserve_whitespace: false,
174
+ inherited_namespaces: nil)
173
175
  element = Nodes::ElementNode.new(
174
176
  name: nokogiri_element.name,
175
177
  namespace_uri: nokogiri_element.namespace&.href,
176
178
  prefix: nokogiri_element.namespace&.prefix,
177
179
  )
178
180
 
179
- build_namespace_nodes(nokogiri_element, element)
181
+ scope = nokogiri_namespace_scope(nokogiri_element, inherited_namespaces)
182
+ scope.each do |prefix, uri|
183
+ element.add_namespace(Nodes::NamespaceNode.new(
184
+ prefix: prefix,
185
+ uri: uri,
186
+ ))
187
+ end
188
+
180
189
  build_attribute_nodes(nokogiri_element, element)
181
190
 
182
191
  nokogiri_element.children.each do |child|
183
192
  node = build_node_from_nokogiri(child,
184
- preserve_whitespace: preserve_whitespace)
193
+ preserve_whitespace: preserve_whitespace,
194
+ inherited_namespaces: scope)
185
195
  element.add_child(node) if node
186
196
  end
187
197
 
188
198
  element
189
199
  end
190
200
 
191
- def self.build_namespace_nodes(nokogiri_element, element)
192
- namespaces = collect_in_scope_namespaces(nokogiri_element)
193
-
194
- namespaces.each do |prefix, uri|
195
- ns_node = Nodes::NamespaceNode.new(
196
- prefix: prefix,
197
- uri: uri,
198
- )
199
- element.add_namespace(ns_node)
200
- end
201
- end
202
-
203
- def self.collect_in_scope_namespaces(nokogiri_element)
204
- namespaces = {}
205
-
206
- current = nokogiri_element
207
- while current && !current.is_a?(Nokogiri::XML::Document)
208
- if current.is_a?(Nokogiri::XML::Element)
209
- current.namespace_definitions.each do |ns|
210
- prefix = ns.prefix || ""
211
- unless namespaces.key?(prefix)
212
- namespaces[prefix] = ns.href
213
- end
214
- end
215
- end
216
- current = current.parent
201
+ # In-scope namespace bindings: the element's own declarations
202
+ # shadow inherited ones, xml is prebound. The scope is passed down
203
+ # the recursion — one definition fetch per element instead of an
204
+ # ancestor walk per element (O(n) total, not O(n * depth)).
205
+ def self.nokogiri_namespace_scope(nokogiri_element, inherited)
206
+ scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
207
+ nokogiri_element.namespace_definitions.each do |ns|
208
+ scope[ns.prefix || ""] = ns.href
217
209
  end
218
-
219
- namespaces["xml"] ||= "http://www.w3.org/XML/1998/namespace"
220
-
221
- namespaces
210
+ scope
222
211
  end
223
212
 
224
213
  def self.build_attribute_nodes(nokogiri_element, element)
@@ -239,7 +228,9 @@ preserve_whitespace: false)
239
228
  def self.build_text_node(nokogiri_text, preserve_whitespace: false)
240
229
  content = nokogiri_text.content
241
230
 
242
- if !preserve_whitespace && content.strip.empty? && nokogiri_text.parent.is_a?(Nokogiri::XML::Element)
231
+ unless WhitespacePolicy.keep_dom_text?(content,
232
+ preserve_whitespace: preserve_whitespace,
233
+ element_parent: nokogiri_text.parent.is_a?(Nokogiri::XML::Element))
243
234
  return nil
244
235
  end
245
236
 
@@ -405,7 +396,7 @@ preserve_whitespace: false)
405
396
  # whitespace-only skip rule matches the wrapper path exactly.
406
397
  def self.build_moxml_text_from_record(record, preserve_whitespace)
407
398
  content = record[:text].to_s
408
- return nil if !preserve_whitespace && content.strip.empty?
399
+ return nil unless WhitespacePolicy.keep_dom_text?(content, preserve_whitespace: preserve_whitespace)
409
400
 
410
401
  Nodes::TextNode.new(value: content, original: content)
411
402
  end
@@ -513,7 +504,9 @@ inherited_namespaces: nil)
513
504
  def self.build_moxml_text_node(moxml_text, preserve_whitespace: false)
514
505
  content = moxml_text.content
515
506
 
516
- if !preserve_whitespace && content.strip.empty? && moxml_text.parent.is_a?(Moxml::Element)
507
+ unless WhitespacePolicy.keep_dom_text?(content,
508
+ preserve_whitespace: preserve_whitespace,
509
+ element_parent: moxml_text.parent.is_a?(Moxml::Element))
517
510
  return nil
518
511
  end
519
512
 
data/lib/canon/xml/sax.rb CHANGED
@@ -10,13 +10,16 @@ module Canon
10
10
  # API to that protocol. OCP: a new engine is a new driver plus one line
11
11
  # here; the builder never changes.
12
12
  #
13
- # Engine choice: leptris SAX outperforms Nokogiri SAX (1.10x min-of-N
14
- # on canon's builder, 53KB doc) through moxml 0.5.12's
15
- # interest-proportional callbacksBUT the batch-attribute transport
16
- # reads uninitialized memory at certain buffer offsets, corrupting the
17
- # first N attributes of an element (leptris-ruby#95). Until that is
18
- # fixed, CRuby keeps the Nokogiri driver; flipping is the one-line
19
- # change below.
13
+ # Engine choice: the leptris SAX flip is armed but blocked twice over.
14
+ # The batch-attribute corruption (leptris-ruby#95) is answered by a
15
+ # correctness-first path in leptris 1.9.36 attributes are correct,
16
+ # but that path DROPS namespace declarations from SAX events
17
+ # entirely (raw `["<root xmlns:a=.../>", []]` xmlns pairs vanish),
18
+ # blinding namespace comparison. Until xmlns reporting is restored
19
+ # (and then the fast path returns with the C fix), CRuby keeps the
20
+ # Nokogiri driver; the flip is the one-line change below. With
21
+ # leptris absent (moxml resolving nokogiri) the driver calls Nokogiri
22
+ # directly — the wrapper layer only adds overhead there.
20
23
  module Sax
21
24
  autoload :NokogiriDriver, "canon/xml/sax/nokogiri_driver"
22
25
  autoload :MoxmlDriver, "canon/xml/sax/moxml_driver"
@@ -198,22 +198,12 @@ strip_doctype: false)
198
198
  return
199
199
  end
200
200
 
201
- # Skip whitespace-only text nodes unless:
202
- # 1. preserve_whitespace is true, OR
203
- # 2. The content contains CR (from &#xD; entities) which must be preserved for C14N, OR
204
- # 3. The content contains non-ASCII whitespace (NBSP U+00A0, ideographic
205
- # space U+3000, etc.) — those are semantically meaningful content,
206
- # not pretty-print indentation, and must survive parsing so the
207
- # comparator can detect Unicode whitespace-type differences.
208
- #
209
- # Strip only when the node is pure ASCII whitespace (space, tab, CR, LF).
210
- # This lets pretty-printed fixtures work (indent nodes stripped) while
211
- # preserving NBSP-only text nodes.
212
- if !@preserve_whitespace && decoded_string.gsub(/[ \t\r\n]/,
213
- "").empty? && parent.node_type == :element && !decoded_string.include?("\r")
214
- # Only skip if parent is an element (not root)
215
- return
216
- end
201
+ # Skip whitespace-only text nodes unless preserving, per the SAX
202
+ # policy (CR-bearing content always survives for C14N;
203
+ # non-ASCII whitespace is meaningful content).
204
+ return unless WhitespacePolicy.keep_sax_text?(decoded_string,
205
+ preserve_whitespace: @preserve_whitespace,
206
+ element_parent: parent.node_type == :element)
217
207
 
218
208
  text = Nodes::TextNode.new(value: decoded_string, original: raw_string)
219
209
  parent.add_child(text)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Xml
5
+ # Parse-time whitespace policy: whether a character-data node
6
+ # survives conversion. One home for the keep/strip rules so the
7
+ # DOM/SAX differences are visible here instead of implied by
8
+ # copy-paste across conversion sites.
9
+ module WhitespacePolicy
10
+ module_function
11
+
12
+ # DOM conversion rule: whitespace-only text is dropped unless
13
+ # preserving. Non-ASCII whitespace (NBSP, U+3000) survives —
14
+ # String#strip only removes ASCII whitespace.
15
+ #
16
+ # NOTE: CR-only nodes are dropped on this path; the SAX rule keeps
17
+ # them (character references must survive for C14N).
18
+ def keep_dom_text?(content, preserve_whitespace:, element_parent: true)
19
+ return true if preserve_whitespace
20
+ return true unless element_parent
21
+
22
+ !content.strip.empty?
23
+ end
24
+
25
+ # SAX rule: same shape, plus CR-bearing content is always kept
26
+ # (&#xD; must survive parsing for C14N) — only runs of pure ASCII
27
+ # whitespace (space, tab, CR, LF) are dropped when not preserving.
28
+ def keep_sax_text?(content, preserve_whitespace:, element_parent: true)
29
+ return true if preserve_whitespace
30
+ return true unless element_parent
31
+ return true if content.include?("\r")
32
+
33
+ !content.gsub(/[ \t\r\n]/, "").empty?
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/canon/xml.rb CHANGED
@@ -28,6 +28,7 @@ module Canon
28
28
  autoload :Sax, "canon/xml/sax"
29
29
  autoload :SaxBuilder, "canon/xml/sax_builder"
30
30
  autoload :WhitespaceNormalizer, "canon/xml/whitespace_normalizer"
31
+ autoload :WhitespacePolicy, "canon/xml/whitespace_policy"
31
32
  autoload :XmlBaseHandler, "canon/xml/xml_base_handler"
32
33
  autoload :XPathEngine, "canon/xml/xpath_engine"
33
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -401,6 +401,7 @@ files:
401
401
  - lib/canon/xml/sax/nokogiri_driver.rb
402
402
  - lib/canon/xml/sax_builder.rb
403
403
  - lib/canon/xml/whitespace_normalizer.rb
404
+ - lib/canon/xml/whitespace_policy.rb
404
405
  - lib/canon/xml/xml_base_handler.rb
405
406
  - lib/canon/xml/xpath_engine.rb
406
407
  - lib/canon/xml_backend.rb