canon 0.3.5 → 0.3.7

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: 8912c851a2e2b0e250fcae120cda428d7705f9c6262b57ec7fe1218aaf186229
4
- data.tar.gz: 98a7b6c64818395e18c1de7b8918255368b6b4bf8766ecaa567b5502c7c084b4
3
+ metadata.gz: b1ce43e1e3cb461ac60687f0e9cdd815916f85cd40a87c8d70f0a071c56b1c04
4
+ data.tar.gz: 3eff7d2db22b5fe57674e64aa5f6b5ebb1d4b82d6f3afde5c45444f57f2c8e55
5
5
  SHA512:
6
- metadata.gz: 2fee687e2469d42193c4f58a294179dc8332aca3fefca5c3ad476cdc061e40c2370d2183790b20c1e43ae83b420ae78567cf7f27114c128f21b0ab8d76be46fe
7
- data.tar.gz: 61a58938d48f48ce066c93b09e734bc62a05cfccec1c7fea80ecdb4de97663e589940108f89f0d8f947737d096ca9c21b9f41edbe51569da5bcc2ad5c990fd0f
6
+ metadata.gz: 7428800d8125c5173b61338ce8929f1c3700684ccba7cae25847a1d5cc533c99eea946a46cd5f567841b978597670e26d546849b853b11bd88cb6f72c023c647
7
+ data.tar.gz: 77cdd82d177e2d476aece71503a70487ee31f6593f30d401e748199944fbc9230756c5bba08b73411dae62621c5d08c16c1fdb4ec427a5e96f0d43955cea0414
@@ -80,7 +80,7 @@ module Canon
80
80
  # fragment — a TreeBuilder walk like the XML extractors, with the
81
81
  # HTML whitespace policy and xmlns-free attributes.
82
82
  def self.build_from_nokogiri(nokogiri_doc)
83
- builder = Canon::Xml::TreeBuilder.new
83
+ builder = Canon::Xml::TreeBuilder::DEFAULT
84
84
  root = Canon::Xml::Nodes::RootNode.new
85
85
  skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
86
86
 
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.5"
4
+ VERSION = "0.3.7"
5
5
  end
@@ -134,8 +134,6 @@ module Canon
134
134
  # node kinds, document ordering) live in TreeBuilder and
135
135
  # WhitespacePolicy — one dialect, three feeds.
136
136
 
137
- TREE_BUILDER = TreeBuilder.new
138
-
139
137
  # -- Nokogiri walk: top-down, scope flows down the recursion --
140
138
 
141
139
  def self.build_from_nokogiri(nokogiri_doc, preserve_whitespace: false)
@@ -145,13 +143,13 @@ module Canon
145
143
  if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
146
144
  root.add_child(walk_nokogiri(nokogiri_doc.root,
147
145
  preserve_whitespace: preserve_whitespace))
148
- TREE_BUILDER.add_document_children(root, nokogiri_doc.children,
149
- nokogiri_doc.root, skip_types) do |child|
146
+ TreeBuilder::DEFAULT.add_document_children(root, nokogiri_doc.children,
147
+ nokogiri_doc.root, skip_types) do |child|
150
148
  walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
151
149
  end
152
150
  else
153
- TREE_BUILDER.add_document_children(root, nokogiri_doc.children,
154
- nil, skip_types) do |child|
151
+ TreeBuilder::DEFAULT.add_document_children(root, nokogiri_doc.children,
152
+ nil, skip_types) do |child|
155
153
  walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
156
154
  end
157
155
  end
@@ -163,11 +161,11 @@ module Canon
163
161
  inherited_namespaces: nil)
164
162
  case node
165
163
  when Nokogiri::XML::Element
166
- scope = TREE_BUILDER.merge_namespace_scope(
164
+ scope = TreeBuilder::DEFAULT.merge_namespace_scope(
167
165
  inherited_namespaces,
168
166
  node.namespace_definitions.map { |ns| [ns.prefix, ns.href] },
169
167
  )
170
- element = TREE_BUILDER.element(
168
+ element = TreeBuilder::DEFAULT.element(
171
169
  name: node.name,
172
170
  prefix: node.namespace&.prefix,
173
171
  namespace_uri: node.namespace&.href,
@@ -184,17 +182,17 @@ inherited_namespaces: nil)
184
182
  end
185
183
  element
186
184
  when Nokogiri::XML::Text, Nokogiri::XML::CDATA
187
- TREE_BUILDER.text(node.content,
188
- keep: WhitespacePolicy.keep_dom_text?(
189
- node.content,
190
- preserve_whitespace: preserve_whitespace,
191
- element_parent: node.parent.is_a?(Nokogiri::XML::Element),
192
- ),
193
- original: node.to_xml)
185
+ TreeBuilder::DEFAULT.text(node.content,
186
+ keep: WhitespacePolicy.keep_dom_text?(
187
+ node.content,
188
+ preserve_whitespace: preserve_whitespace,
189
+ element_parent: node.parent.is_a?(Nokogiri::XML::Element),
190
+ ),
191
+ original: node.to_xml)
194
192
  when Nokogiri::XML::Comment
195
- TREE_BUILDER.comment(node.content)
193
+ TreeBuilder::DEFAULT.comment(node.content)
196
194
  when Nokogiri::XML::ProcessingInstruction
197
- TREE_BUILDER.processing_instruction(node.name, node.content)
195
+ TreeBuilder::DEFAULT.processing_instruction(node.name, node.content)
198
196
  end
199
197
  end
200
198
 
@@ -207,9 +205,16 @@ inherited_namespaces: nil)
207
205
  # scopes cannot flow down during the stream).
208
206
 
209
207
  def self.from_moxml_xml(xml_string, preserve_whitespace:)
210
- doc = Canon::XmlParsing.moxml_context.parse(xml_string, readonly: true)
208
+ # strict: false keeps the Nokogiri-engine contract — malformed
209
+ # input yields a recovered document plus diagnostics instead of
210
+ # an exception (moxml 0.5.15 defaults strict to true; issue
211
+ # #147 rides recover errors on Document#parse_errors).
212
+ doc = Canon::XmlParsing.moxml_context.parse(xml_string,
213
+ readonly: true,
214
+ strict: false)
211
215
  check_moxml_relative_namespace_uris(doc)
212
216
  result = build_from_moxml(doc, preserve_whitespace: preserve_whitespace)
217
+ result.parse_errors = doc.parse_errors if doc.parse_errors.any?
213
218
  # Canon owns this document's full lifecycle: the canon tree holds
214
219
  # no engine references once built, so release the native tree now
215
220
  # instead of waiting for the GC finalizer (moxml#134; no-op on
@@ -243,13 +248,13 @@ inherited_namespaces: nil)
243
248
  element = build_moxml_subtree_from_records(moxml_doc.root,
244
249
  preserve_whitespace: preserve_whitespace)
245
250
  root.add_child(element) if element
246
- TREE_BUILDER.add_document_children(root, moxml_doc.children,
247
- moxml_doc.root, skip_types) do |child|
251
+ TreeBuilder::DEFAULT.add_document_children(root, moxml_doc.children,
252
+ moxml_doc.root, skip_types) do |child|
248
253
  walk_moxml(child, preserve_whitespace: preserve_whitespace)
249
254
  end
250
255
  else
251
- TREE_BUILDER.add_document_children(root, moxml_doc.children,
252
- nil, skip_types) do |child|
256
+ TreeBuilder::DEFAULT.add_document_children(root, moxml_doc.children,
257
+ nil, skip_types) do |child|
253
258
  walk_moxml(child, preserve_whitespace: preserve_whitespace)
254
259
  end
255
260
  end
@@ -276,15 +281,15 @@ preserve_whitespace: false)
276
281
  own_namespaces)
277
282
  when :text, :cdata
278
283
  content = record[:text].to_s
279
- TREE_BUILDER.text(content,
280
- keep: WhitespacePolicy.keep_dom_text?(
281
- content, preserve_whitespace: preserve_whitespace
282
- ))
284
+ TreeBuilder::DEFAULT.text(content,
285
+ keep: WhitespacePolicy.keep_dom_text?(
286
+ content, preserve_whitespace: preserve_whitespace
287
+ ))
283
288
  when :comment
284
- TREE_BUILDER.comment(record[:text])
289
+ TreeBuilder::DEFAULT.comment(record[:text])
285
290
  when :processing_instruction
286
- TREE_BUILDER.processing_instruction(record[:qname],
287
- record[:text] || "")
291
+ TreeBuilder::DEFAULT.processing_instruction(record[:qname],
292
+ record[:text] || "")
288
293
  end
289
294
 
290
295
  frames.push([record[:depth], node]) if node
@@ -298,7 +303,7 @@ preserve_whitespace: false)
298
303
  end
299
304
 
300
305
  def self.build_moxml_element_from_record(record, frames, own_namespaces)
301
- element = TREE_BUILDER.element(
306
+ element = TreeBuilder::DEFAULT.element(
302
307
  name: record[:qname],
303
308
  prefix: record[:prefix],
304
309
  namespace_uri: record[:namespace_uri],
@@ -316,9 +321,9 @@ preserve_whitespace: false)
316
321
  end
317
322
 
318
323
  def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
319
- scope = TREE_BUILDER.merge_namespace_scope(inherited,
320
- own_namespaces[element].to_a)
321
- TREE_BUILDER.attach_namespace_scope(element, scope)
324
+ scope = TreeBuilder::DEFAULT.merge_namespace_scope(inherited,
325
+ own_namespaces[element].to_a)
326
+ TreeBuilder::DEFAULT.attach_namespace_scope(element, scope)
322
327
 
323
328
  element.children.each do |child|
324
329
  assign_moxml_namespace_scopes(child, scope, own_namespaces) if child.is_a?(Nodes::ElementNode)
@@ -331,11 +336,11 @@ preserve_whitespace: false)
331
336
  inherited_namespaces: nil)
332
337
  case node
333
338
  when Moxml::Element
334
- scope = TREE_BUILDER.merge_namespace_scope(
339
+ scope = TreeBuilder::DEFAULT.merge_namespace_scope(
335
340
  inherited_namespaces,
336
341
  node.namespace_definitions.map { |ns| [ns.prefix, ns.uri] },
337
342
  )
338
- element = TREE_BUILDER.element(
343
+ element = TreeBuilder::DEFAULT.element(
339
344
  name: node.name,
340
345
  prefix: node.namespace&.prefix,
341
346
  namespace_uri: node.namespace&.uri,
@@ -352,16 +357,16 @@ inherited_namespaces: nil)
352
357
  end
353
358
  element
354
359
  when Moxml::Text, Moxml::Cdata
355
- TREE_BUILDER.text(node.content,
356
- keep: WhitespacePolicy.keep_dom_text?(
357
- node.content,
358
- preserve_whitespace: preserve_whitespace,
359
- element_parent: node.parent.is_a?(Moxml::Element),
360
- ))
360
+ TreeBuilder::DEFAULT.text(node.content,
361
+ keep: WhitespacePolicy.keep_dom_text?(
362
+ node.content,
363
+ preserve_whitespace: preserve_whitespace,
364
+ element_parent: node.parent.is_a?(Moxml::Element),
365
+ ))
361
366
  when Moxml::Comment
362
- TREE_BUILDER.comment(node.content)
367
+ TreeBuilder::DEFAULT.comment(node.content)
363
368
  when Moxml::ProcessingInstruction
364
- TREE_BUILDER.processing_instruction(node.target, node.content)
369
+ TreeBuilder::DEFAULT.processing_instruction(node.target, node.content)
365
370
  end
366
371
  end
367
372
  end
data/lib/canon/xml/sax.rb CHANGED
@@ -10,16 +10,13 @@ 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: 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.
13
+ # Engine choice follows the resolved adapter independent of the
14
+ # DOM engine default: leptris SAX (corruption leptris#625 and the
15
+ # missing xmlns reporting leptris-ruby#99 both fixed) builds canon
16
+ # trees at parity with or faster than Nokogiri SAX, CI-gated. The
17
+ # DOM engine default stays Nokogiri until the record conversion
18
+ # passes the same gate (see XmlBackend). With leptris absent the
19
+ # driver calls Nokogiri directly.
23
20
  module Sax
24
21
  autoload :NokogiriDriver, "canon/xml/sax/nokogiri_driver"
25
22
  autoload :MoxmlDriver, "canon/xml/sax/moxml_driver"
@@ -27,7 +24,7 @@ module Canon
27
24
  class << self
28
25
  # Drive `builder` with the runtime's SAX engine.
29
26
  def parse(xml_string, builder)
30
- if RUBY_ENGINE == "opal"
27
+ if RUBY_ENGINE == "opal" || Canon::XmlParsing.moxml_adapter_name != :nokogiri
31
28
  MoxmlDriver.new(builder).parse(xml_string)
32
29
  else
33
30
  NokogiriDriver.new(builder).parse(xml_string)
@@ -9,6 +9,11 @@ module Canon
9
9
  # selects the driver. Much faster than DOM parsing + conversion —
10
10
  # no intermediate engine DOM tree, no traversal conversion pass.
11
11
  #
12
+ # Construction goes through TreeBuilder like every other feed:
13
+ # this class owns only what is SAX-specific — qname parsing, xmlns
14
+ # separation, character-reference decoding, adjacency combining,
15
+ # the namespace stack, and document-level reordering.
16
+ #
12
17
  # Usage:
13
18
  # root = SaxBuilder.parse(xml_string, preserve_whitespace: false)
14
19
  # # root is a Canon::Xml::Nodes::RootNode
@@ -123,26 +128,23 @@ strip_doctype: false)
123
128
  end
124
129
  end
125
130
 
126
- # Push new namespace scope with declarations
131
+ # Push new namespace scope with declarations (own shadows
132
+ # inherited — the same merge the TreeBuilder scope kernel applies)
127
133
  new_scope = @namespace_stack.last.merge(ns_hash)
128
134
  @namespace_stack.push(new_scope)
129
135
 
130
- # Find namespace URI from current scope
131
- ns_uri = new_scope[prefix.to_s]
132
-
133
- # Create element node
134
- element = Nodes::ElementNode.new(
136
+ element = TreeBuilder::DEFAULT.element(
135
137
  name: local_name,
136
- namespace_uri: ns_uri,
137
138
  prefix: prefix,
139
+ namespace_uri: new_scope[prefix.to_s],
140
+ namespace_scope: new_scope,
141
+ attributes: regular_attrs.map do |attr_name, attr_value|
142
+ attr_prefix, attr_local = parse_qname(attr_name)
143
+ attr_ns_uri = attr_prefix ? new_scope[attr_prefix] : nil
144
+ [attr_local, decode_character_references(attr_value || ""), attr_ns_uri, attr_prefix]
145
+ end,
138
146
  )
139
147
 
140
- # Add namespace nodes from current scope
141
- add_namespace_nodes(element, new_scope)
142
-
143
- # Build and add attribute nodes (excluding xmlns declarations)
144
- add_attribute_nodes(element, regular_attrs)
145
-
146
148
  parent.add_child(element)
147
149
  @stack.push(element)
148
150
  end
@@ -206,8 +208,9 @@ strip_doctype: false)
206
208
  preserve_whitespace: @preserve_whitespace,
207
209
  element_parent: parent.node_type == :element)
208
210
 
209
- text = Nodes::TextNode.new(value: decoded_string, original: raw_string)
210
- parent.add_child(text)
211
+ parent.add_child(
212
+ TreeBuilder::DEFAULT.text(decoded_string, keep: true, original: raw_string),
213
+ )
211
214
  end
212
215
  private :append_text
213
216
 
@@ -215,9 +218,7 @@ strip_doctype: false)
215
218
  #
216
219
  # @param string [String] Comment content
217
220
  def comment(string)
218
- parent = @stack.last
219
- comment_node = Nodes::CommentNode.new(value: string)
220
- parent.add_child(comment_node)
221
+ @stack.last.add_child(TreeBuilder::DEFAULT.comment(string))
221
222
  end
222
223
 
223
224
  # Called for processing instructions
@@ -225,10 +226,9 @@ strip_doctype: false)
225
226
  # @param name [String] PI target
226
227
  # @param content [String] PI content
227
228
  def processing_instruction(name, content)
228
- parent = @stack.last
229
- pi = Nodes::ProcessingInstructionNode.new(target: name,
230
- data: content || "")
231
- parent.add_child(pi)
229
+ @stack.last.add_child(
230
+ TreeBuilder::DEFAULT.processing_instruction(name, content || ""),
231
+ )
232
232
  end
233
233
 
234
234
  # Return the built tree
@@ -314,44 +314,7 @@ strip_doctype: false)
314
314
  [ns_decls, regular_attrs]
315
315
  end
316
316
 
317
- # Add namespace nodes to element
318
- #
319
- # @param element [Nodes::ElementNode] Element to add namespaces to
320
- # @param scope [Hash] Current namespace scope
321
- def add_namespace_nodes(element, scope)
322
- scope.each do |prefix, uri|
323
- ns_node = Nodes::NamespaceNode.new(prefix: prefix, uri: uri)
324
- element.add_namespace(ns_node)
325
- end
326
- end
327
-
328
- # Add attribute nodes to element
329
- #
330
- # @param element [Nodes::ElementNode] Element to add attributes to
331
- # @param attrs [Array] Array of [name, value] pairs
332
- def add_attribute_nodes(element, attrs)
333
- attrs.each do |attr_name, attr_value|
334
- attr_prefix, attr_local = parse_qname(attr_name)
335
-
336
- # Find namespace for attribute (if prefixed)
337
- attr_ns_uri = attr_prefix ? @namespace_stack.last[attr_prefix] : nil
338
-
339
- # Decode numeric character references (e.g., &#38; → &)
340
- # Nokogiri SAX leaves these as-is, but we need them decoded for C14N
341
- decoded_value = decode_character_references(attr_value || "")
342
-
343
- attr_node = Nodes::AttributeNode.new(
344
- name: attr_local,
345
- value: decoded_value,
346
- namespace_uri: attr_ns_uri,
347
- prefix: attr_prefix,
348
- )
349
- element.add_attribute(attr_node)
350
- end
351
- end
352
-
353
- # Decode numeric character references in a string
354
- # Handles both &#decimal; and &#xhex; forms
317
+ # Decode numeric character references (e.g., &#38; → &)
355
318
  #
356
319
  # @param value [String] String potentially containing character references
357
320
  # @return [String] String with character references decoded
@@ -16,6 +16,9 @@ module Canon
16
16
  # repeats, libleptris deduplicates), so the builder resolves them
17
17
  # once — first occurrence wins.
18
18
  class TreeBuilder
19
+ # Stateless module: one shared instance serves every feed.
20
+ DEFAULT = new
21
+
19
22
  NO_ATTRIBUTES = [].freeze
20
23
  XML_NAMESPACE_PREFIX = "xml"
21
24
  XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"
@@ -54,12 +57,16 @@ module Canon
54
57
  )
55
58
  attach_namespace_scope(element, namespace_scope) if namespace_scope
56
59
 
57
- seen = {}
58
- attributes.each do |attr_name, value, attr_namespace_uri, attr_prefix|
59
- key = [attr_name, attr_namespace_uri]
60
- next if seen.key?(key)
60
+ # Duplicate (name, namespace) pairs are invalid XML — first
61
+ # occurrence wins. Attributes per element are few, so a pairwise
62
+ # scan beats a per-element seen-hash on allocation (this is the
63
+ # hot SAX path).
64
+ attributes.each_with_index do |(attr_name, value, attr_namespace_uri, attr_prefix), index|
65
+ duplicate = attributes[0...index].any? do |prior_name, _v, prior_ns, _p|
66
+ prior_name == attr_name && prior_ns == attr_namespace_uri
67
+ end
68
+ next if duplicate
61
69
 
62
- seen[key] = true
63
70
  element.add_attribute(Nodes::AttributeNode.new(
64
71
  name: attr_name,
65
72
  value: value,
@@ -9,16 +9,14 @@ module Canon
9
9
  # - HTML support — always Nokogiri on CRuby; moxml has no HTML adapter and
10
10
  # leptris no HTML parser (see Canon::Html::NokogiriSupport).
11
11
  #
12
- # SSOT: moxml owns adapter preference (Moxml::Config prefers leptris when
13
- # installed and capable; see XmlParsing.moxml_adapter_name). Canon's
14
- # default engine is Nokogiri on CRuby until leptris WINS somewhere
15
- # measurably: conformance parity is complete (engine_parity_spec 12/12),
16
- # but the leptris SAX driver is blocked by a data-corruption bug
17
- # (leptris-ruby#95) and the record-based conversion runs ~0.55x Nokogiri
18
- # (CI performance gate: -42..45% on from_xml) pending lighter record
19
- # emission upstream. Flipping the default is the one-line change in
20
- # #detect; CANON_XML_BACKEND=moxml opts in today. Under Opal the engine
21
- # is moxml (rexml adapter).
12
+ # SSOT: moxml owns adapter preference (Moxml::Config prefers leptris
13
+ # when installed; see XmlParsing.moxml_adapter_name). The SAX engine
14
+ # follows the resolved adapter leptris (correct since leptris#625
15
+ # and leptris-ruby#99; CI perf-gated). The DOM engine stays Nokogiri
16
+ # until the record conversion passes the same gate (moxml#143's fix
17
+ # landed; still -40%/-37% from_xml on the CI runner). Conformance
18
+ # parity is complete (engine_parity_spec 12/12, zero pendings).
19
+ # CANON_XML_BACKEND=moxml opts into the leptris DOM engine today.
22
20
  #
23
21
  # HTML stays on Nokogiri on CRuby (Canon::Html::NokogiriSupport) and the
24
22
  # pretty-printers keep the Nokogiri pipeline — pretty-printed bytes are
@@ -63,6 +61,11 @@ module Canon
63
61
  def detect
64
62
  return :moxml if RUBY_ENGINE == "opal"
65
63
 
64
+ # The SAX engine flipped to leptris (leptris#625/#99 fixed,
65
+ # CI-gated — see Canon::Xml::Sax), but the DOM conversion still
66
+ # trails Nokogiri on the CI runner (-40%/-37% from_xml after
67
+ # moxml#143's emission fix landed). Nokogiri stays the DOM
68
+ # default until that gate passes; flipping is this one line.
66
69
  :nokogiri
67
70
  end
68
71
  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.5
4
+ version: 0.3.7
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-29 00:00:00.000000000 Z
11
+ date: 2026-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs