canon 0.3.2 → 0.3.4

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: b000e9e89684d66d4f037a950a2121a88717dd6c72d975648742f32b5a83fb9f
4
- data.tar.gz: c5e8b5ecfdcb8435559be3e6c940e8e8bec99202054b90fa857f9e3afd3097e2
3
+ metadata.gz: 53a9382cf5977e8e1b65a847cd11f9f2c61a162c3913a64588e4d48c71cfbf94
4
+ data.tar.gz: 851f0227e56cffdd225fce23ec32b78b3b44f43e08137a39572a2db237d94aaf
5
5
  SHA512:
6
- metadata.gz: '0804e7a2a07ff2687e363ee2bd0a1e2d00a5fe826e45ae5bd69fe26749bf2e2b1517a6fb2fcd3518e41d7d9bf6bf0b76fd8e704c288293724d27cbc3613978f4'
7
- data.tar.gz: 6d9dca5aa3c29bbde5d067ca02e2a22c4d7bee098635e0fd8b48df16fbf08102fa713608f1965d198f3ace1c064743916b57d34d7a2cc2a6b845902e1553a045
6
+ metadata.gz: 67710ade1d18f99c23e4477540303f45867d145c7f92df59f2f1277cf442c576eca2a3d8562cb92972f56d80dbe111e6104d28353cb7c5091d690490605772ff
7
+ data.tar.gz: e8d2783baa8fba878cb3cbb7a602dff3563f23dfb15df95ca5c24c075475d53c8319f2579bf3ce3c533e1989b1c75b91b72913c57c30279278a59de68202aef0
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.2"
4
+ VERSION = "0.3.4"
5
5
  end
@@ -126,28 +126,49 @@ module Canon
126
126
  end
127
127
  end
128
128
 
129
+ # --- Shared construction kernels (identical semantics per engine) ---
130
+
131
+ # Attach document-level children (prolog/epilog PIs, comments,
132
+ # document-level text) to the canon root, in document order,
133
+ # skipping the document element and the doctype. `converter`
134
+ # converts one engine child to a canon node (or nil).
135
+ def self.add_document_children(root, children, document_element,
136
+ skip_types, converter)
137
+ children.each do |child|
138
+ next if child.equal?(document_element)
139
+ next if skip_types.any? { |type| child.is_a?(type) }
140
+
141
+ node = converter.call(child)
142
+ root.add_child(node) if node
143
+ end
144
+ end
145
+
146
+ # In-scope namespace bindings: the element's own declarations
147
+ # shadow inherited ones; xml is prebound at the base. Declaration
148
+ # pairs are [prefix-or-nil, uri]; nil and "" both mean the default
149
+ # namespace.
150
+ def self.merge_namespace_scope(inherited, declaration_pairs)
151
+ scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
152
+ declaration_pairs.each do |prefix, uri|
153
+ scope[prefix || ""] = uri
154
+ end
155
+ scope
156
+ end
157
+
129
158
  def self.build_from_nokogiri(nokogiri_doc, preserve_whitespace: false)
130
159
  root = Nodes::RootNode.new
131
160
 
161
+ converter = ->(child) { build_node_from_nokogiri(child, preserve_whitespace: preserve_whitespace) }
162
+ skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
163
+
132
164
  if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
133
165
  root.add_child(build_element_node(nokogiri_doc.root,
134
166
  preserve_whitespace: preserve_whitespace))
135
- nokogiri_doc.children.each do |child|
136
- next if child == nokogiri_doc.root
137
- next if child.is_a?(Nokogiri::XML::DTD)
138
-
139
- node = build_node_from_nokogiri(child,
140
- preserve_whitespace: preserve_whitespace)
141
- root.add_child(node) if node
142
- end
167
+ add_document_children(root, nokogiri_doc.children, nokogiri_doc.root,
168
+ skip_types, converter)
143
169
  else
144
- nokogiri_doc.children.each do |child|
145
- next if child.is_a?(Nokogiri::XML::DTD)
146
-
147
- node = build_node_from_nokogiri(child,
148
- preserve_whitespace: preserve_whitespace)
149
- root.add_child(node) if node
150
- end
170
+ add_document_children(root, nokogiri_doc.children, nil,
171
+ skip_types, converter)
151
172
  end
152
173
 
153
174
  root
@@ -198,16 +219,14 @@ inherited_namespaces: nil)
198
219
  element
199
220
  end
200
221
 
201
- # In-scope namespace bindings: the element's own declarations
202
- # shadow inherited ones, xml is prebound. The scope is passed down
222
+ # In-scope bindings via the shared kernel; the scope is passed down
203
223
  # the recursion — one definition fetch per element instead of an
204
224
  # ancestor walk per element (O(n) total, not O(n * depth)).
205
225
  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
209
- end
210
- scope
226
+ merge_namespace_scope(inherited,
227
+ nokogiri_element.namespace_definitions.map do |ns|
228
+ [ns.prefix, ns.href]
229
+ end)
211
230
  end
212
231
 
213
232
  def self.build_attribute_nodes(nokogiri_element, element)
@@ -228,7 +247,9 @@ inherited_namespaces: nil)
228
247
  def self.build_text_node(nokogiri_text, preserve_whitespace: false)
229
248
  content = nokogiri_text.content
230
249
 
231
- if !preserve_whitespace && content.strip.empty? && nokogiri_text.parent.is_a?(Nokogiri::XML::Element)
250
+ unless WhitespacePolicy.keep_dom_text?(content,
251
+ preserve_whitespace: preserve_whitespace,
252
+ element_parent: nokogiri_text.parent.is_a?(Nokogiri::XML::Element))
232
253
  return nil
233
254
  end
234
255
 
@@ -281,26 +302,18 @@ inherited_namespaces: nil)
281
302
  def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
282
303
  root = Nodes::RootNode.new
283
304
 
305
+ converter = ->(child) { build_moxml_node(child, preserve_whitespace: preserve_whitespace) }
306
+ skip_types = [Moxml::Doctype]
307
+
284
308
  if moxml_doc.is_a?(Moxml::Document) && moxml_doc.root
285
309
  element = build_moxml_subtree_from_records(moxml_doc.root,
286
310
  preserve_whitespace: preserve_whitespace)
287
311
  root.add_child(element) if element
288
- moxml_doc.children.each do |child|
289
- next if child.equal?(moxml_doc.root)
290
- next if child.is_a?(Moxml::Doctype)
291
-
292
- node = build_moxml_node(child,
293
- preserve_whitespace: preserve_whitespace)
294
- root.add_child(node) if node
295
- end
312
+ add_document_children(root, moxml_doc.children, moxml_doc.root,
313
+ skip_types, converter)
296
314
  else
297
- moxml_doc.children.each do |child|
298
- next if child.is_a?(Moxml::Doctype)
299
-
300
- node = build_moxml_node(child,
301
- preserve_whitespace: preserve_whitespace)
302
- root.add_child(node) if node
303
- end
315
+ add_document_children(root, moxml_doc.children, nil,
316
+ skip_types, converter)
304
317
  end
305
318
 
306
319
  root
@@ -394,19 +407,16 @@ preserve_whitespace: false)
394
407
  # whitespace-only skip rule matches the wrapper path exactly.
395
408
  def self.build_moxml_text_from_record(record, preserve_whitespace)
396
409
  content = record[:text].to_s
397
- return nil if !preserve_whitespace && content.strip.empty?
410
+ return nil unless WhitespacePolicy.keep_dom_text?(content, preserve_whitespace: preserve_whitespace)
398
411
 
399
412
  Nodes::TextNode.new(value: content, original: content)
400
413
  end
401
414
 
402
- # Expand each element's own declarations to in-scope bindings:
403
- # the element's declarations shadow inherited ones; xml is
404
- # prebound. Scope flows down the canon tree — no engine calls.
415
+ # Expand each element's own declarations to in-scope bindings via
416
+ # the shared kernel. Scope flows down the canon tree — no engine
417
+ # calls.
405
418
  def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
406
- scope = inherited.dup
407
- own_namespaces[element].to_a.each do |prefix, uri|
408
- scope[prefix || ""] = uri
409
- end
419
+ scope = merge_namespace_scope(inherited, own_namespaces[element].to_a)
410
420
 
411
421
  scope.each do |prefix, uri|
412
422
  element.add_namespace(Nodes::NamespaceNode.new(
@@ -472,11 +482,10 @@ inherited_namespaces: nil)
472
482
  # of an ancestor walk through the adapter layer (the Nokogiri
473
483
  # collector walks ancestors because those calls are cheap there).
474
484
  def self.moxml_namespace_scope(moxml_element, inherited)
475
- scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
476
- moxml_element.namespace_definitions.each do |decl|
477
- scope[decl.prefix || ""] = decl.uri
478
- end
479
- scope
485
+ merge_namespace_scope(inherited,
486
+ moxml_element.namespace_definitions.map do |decl|
487
+ [decl.prefix, decl.uri]
488
+ end)
480
489
  end
481
490
 
482
491
  def self.build_moxml_attribute_nodes(moxml_element, element)
@@ -502,7 +511,9 @@ inherited_namespaces: nil)
502
511
  def self.build_moxml_text_node(moxml_text, preserve_whitespace: false)
503
512
  content = moxml_text.content
504
513
 
505
- if !preserve_whitespace && content.strip.empty? && moxml_text.parent.is_a?(Moxml::Element)
514
+ unless WhitespacePolicy.keep_dom_text?(content,
515
+ preserve_whitespace: preserve_whitespace,
516
+ element_parent: moxml_text.parent.is_a?(Moxml::Element))
506
517
  return nil
507
518
  end
508
519
 
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.2
4
+ version: 0.3.4
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