canon 0.3.6 → 0.3.8

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: 04012234db38cae77206ba33f856aca1262a0ad0111e7d233d5b814e90c22187
4
- data.tar.gz: 3700758afcb5619f59ba7e2f896963bc63695d534f60af7d922dfce8c26076ec
3
+ metadata.gz: 7e6a28999cdd57c9fb642e7f334546ef2ebe4802057481ef9c9bcf93252b8590
4
+ data.tar.gz: 072ae76310a6f936b81a73f2305bac61cb0b1100f36509724f67c5a763fb718f
5
5
  SHA512:
6
- metadata.gz: c6452b462325e65c3f38f31438656b1cf60790646413314d8e835f6a27f78026bc10de2474065627b26a602962c424fd6858e1bc97ed3bca43e8a009b1779291
7
- data.tar.gz: 749b78e282b4b2c7d440f9dff4a2361ee8d7742afcdab54b490bf836ae9417afc5f677e2039143020a6e50eceaf12d42950372c7a5f62dfcd335223243962fef
6
+ metadata.gz: '049a48f66a243b20993f677e8789eff4b08ed72f0b22fa9288453d3806718f2a9f7414c7774e51de30290d19d5b012419f50eb8cdf75762944b4f5c28ed6d54b'
7
+ data.tar.gz: b57afbf6f649b27c8a4833742673f2ae7dea0a4608610795d8ae38c76398ed66e833d4a5d29f832e45ecb1f5ced1e23125afc6b383f45ffc545b6257aca986bb
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.6"
4
+ VERSION = "0.3.8"
5
5
  end
@@ -205,9 +205,15 @@ inherited_namespaces: nil)
205
205
  # scopes cannot flow down during the stream).
206
206
 
207
207
  def self.from_moxml_xml(xml_string, preserve_whitespace:)
208
- doc = Canon::XmlParsing.moxml_context.parse(xml_string, readonly: true)
209
- check_moxml_relative_namespace_uris(doc)
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)
210
215
  result = build_from_moxml(doc, preserve_whitespace: preserve_whitespace)
216
+ result.parse_errors = doc.parse_errors if doc.parse_errors.any?
211
217
  # Canon owns this document's full lifecycle: the canon tree holds
212
218
  # no engine references once built, so release the native tree now
213
219
  # instead of waiting for the GC finalizer (moxml#134; no-op on
@@ -216,23 +222,6 @@ inherited_namespaces: nil)
216
222
  result
217
223
  end
218
224
 
219
- def self.check_moxml_relative_namespace_uris(node)
220
- case node
221
- when Moxml::Element
222
- node.namespace_definitions.each do |ns|
223
- href = ns.uri
224
- next if href.nil? || href.empty?
225
- if relative_uri?(href)
226
- raise Canon::Error,
227
- "Relative namespace URI not allowed: #{href}"
228
- end
229
- end
230
- node.children.each { |child| check_moxml_relative_namespace_uris(child) }
231
- when Moxml::Document
232
- node.children.each { |child| check_moxml_relative_namespace_uris(child) }
233
- end
234
- end
235
-
236
225
  def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
237
226
  root = Nodes::RootNode.new
238
227
  skip_types = [Moxml::Doctype]
@@ -260,32 +249,52 @@ preserve_whitespace: false)
260
249
  frames = []
261
250
  own_namespaces = {}.compare_by_identity
262
251
 
263
- moxml_element.materialize do |record|
252
+ # materialize_fields: the zero-allocation hot path (moxml#143).
253
+ # Flat reused buffers — attributes stride 4, namespaces stride
254
+ # 2 — valid only inside the block, so the element builder
255
+ # copies them into pairs before returning.
256
+ moxml_element.materialize_fields do |kind, qname, prefix, namespace_uri, namespaces, attributes, text, depth|
264
257
  # The record contract is root-subtree-only since moxml 0.5.11
265
258
  # (moxml#140). Older 0.5.x releases — still allowed by canon's
266
259
  # gemspec floor — leaked epilog document-level records at depth
267
260
  # 0, which the document-children enumeration also covers; one
268
261
  # integer compare per record keeps those working.
269
- next if record[:depth].zero? && record[:kind] != :element
262
+ next if depth.zero? && kind != :element
263
+
264
+ # Relative-namespace validation rides the stream (the
265
+ # namespaces buffer is the element's own declarations) — no
266
+ # separate wrapper-tree walk per parse.
267
+ if kind == :element
268
+ i = 1
269
+ while i < namespaces.size
270
+ href = namespaces[i]
271
+ if !href.nil? && !href.empty? && relative_uri?(href)
272
+ raise Canon::Error,
273
+ "Relative namespace URI not allowed: #{href}"
274
+ end
275
+ i += 2
276
+ end
277
+ end
270
278
 
271
- node = case record[:kind]
279
+ node = case kind
272
280
  when :element
273
- build_moxml_element_from_record(record, frames,
274
- own_namespaces)
281
+ build_moxml_element_from_fields(
282
+ qname, prefix, namespace_uri, namespaces, attributes,
283
+ depth, frames, own_namespaces
284
+ )
275
285
  when :text, :cdata
276
- content = record[:text].to_s
286
+ content = text.to_s
277
287
  TreeBuilder::DEFAULT.text(content,
278
288
  keep: WhitespacePolicy.keep_dom_text?(
279
289
  content, preserve_whitespace: preserve_whitespace
280
290
  ))
281
291
  when :comment
282
- TreeBuilder::DEFAULT.comment(record[:text])
292
+ TreeBuilder::DEFAULT.comment(text)
283
293
  when :processing_instruction
284
- TreeBuilder::DEFAULT.processing_instruction(record[:qname],
285
- record[:text] || "")
294
+ TreeBuilder::DEFAULT.processing_instruction(qname, text || "")
286
295
  end
287
296
 
288
- frames.push([record[:depth], node]) if node
297
+ frames.push([depth, node]) if node
289
298
  end
290
299
 
291
300
  top = frames.last
@@ -295,21 +304,24 @@ preserve_whitespace: false)
295
304
  top[1]
296
305
  end
297
306
 
298
- def self.build_moxml_element_from_record(record, frames, own_namespaces)
307
+ def self.build_moxml_element_from_fields(qname, prefix, namespace_uri,
308
+ namespaces, attributes, depth,
309
+ frames, own_namespaces)
299
310
  element = TreeBuilder::DEFAULT.element(
300
- name: record[:qname],
301
- prefix: record[:prefix],
302
- namespace_uri: record[:namespace_uri],
303
- attributes: record[:attributes],
311
+ name: qname,
312
+ prefix: prefix,
313
+ namespace_uri: namespace_uri,
314
+ attributes: attributes.each_slice(4).to_a,
304
315
  )
305
316
 
306
317
  # Adopt completed children: they pop in reverse order; reversing
307
318
  # once is O(n) (unshift per child would be O(n^2) on wide trees).
308
319
  children = []
309
- children << frames.pop[1] while frames.any? && frames.last[0] > record[:depth]
320
+ children << frames.pop[1] while frames.any? && frames.last[0] > depth
310
321
  children.reverse_each { |child| element.add_child(child) }
311
322
 
312
- own_namespaces[element] = record[:namespaces]
323
+ # Copy the declarations out of the reused buffer.
324
+ own_namespaces[element] = namespaces.each_slice(2).to_a
313
325
  element
314
326
  end
315
327
 
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,16 +9,13 @@ 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). Both engines
14
+ # follow the resolved adapter: SAX since leptris#625/#99 were fixed,
15
+ # DOM since moxml#143's materialize_fields stream (zero-allocation
16
+ # flat buffers) made the conversion competitive. Conformance parity
17
+ # is complete (engine_parity_spec 12/12). CANON_XML_BACKEND forces
18
+ # either engine; the CI performance gate is the standing referee.
22
19
  #
23
20
  # HTML stays on Nokogiri on CRuby (Canon::Html::NokogiriSupport) and the
24
21
  # pretty-printers keep the Nokogiri pipeline — pretty-printed bytes are
@@ -63,7 +60,11 @@ module Canon
63
60
  def detect
64
61
  return :moxml if RUBY_ENGINE == "opal"
65
62
 
66
- :nokogiri
63
+ # Both engines now run leptris: SAX since leptris#625/#99
64
+ # (CI-gated), DOM since moxml#143's zero-allocation
65
+ # materialize_fields stream made the conversion competitive.
66
+ # The CI performance gate is the standing referee.
67
+ XmlParsing.moxml_adapter_name == :nokogiri ? :nokogiri : :moxml
67
68
  end
68
69
  end
69
70
  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.6
4
+ version: 0.3.8
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