canon 0.2.11 → 0.3.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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +62 -25
  3. data/CLAUDE.md +197 -0
  4. data/Rakefile +5 -2
  5. data/lib/canon/cache.rb +3 -1
  6. data/lib/canon/cli.rb +0 -3
  7. data/lib/canon/commands/diff_command.rb +1 -8
  8. data/lib/canon/commands/format_command.rb +0 -4
  9. data/lib/canon/commands.rb +9 -0
  10. data/lib/canon/comparison/child_realignment.rb +0 -2
  11. data/lib/canon/comparison/compare_profile.rb +30 -36
  12. data/lib/canon/comparison/comparison_result.rb +1 -4
  13. data/lib/canon/comparison/diff_node_builder.rb +353 -0
  14. data/lib/canon/comparison/dimensions/dimension.rb +51 -0
  15. data/lib/canon/comparison/dimensions/dimension_set.rb +49 -0
  16. data/lib/canon/comparison/dimensions/registry.rb +101 -60
  17. data/lib/canon/comparison/dimensions.rb +15 -46
  18. data/lib/canon/comparison/format_detector.rb +27 -29
  19. data/lib/canon/comparison/html_comparator.rb +20 -143
  20. data/lib/canon/comparison/html_compare_profile.rb +15 -18
  21. data/lib/canon/comparison/json_comparator.rb +4 -165
  22. data/lib/canon/comparison/json_parser.rb +0 -2
  23. data/lib/canon/comparison/markup_comparator.rb +14 -210
  24. data/lib/canon/comparison/match_options/base_resolver.rb +18 -29
  25. data/lib/canon/comparison/match_options/json_resolver.rb +4 -28
  26. data/lib/canon/comparison/match_options/xml_resolver.rb +4 -45
  27. data/lib/canon/comparison/match_options/yaml_resolver.rb +4 -30
  28. data/lib/canon/comparison/match_options.rb +13 -88
  29. data/lib/canon/comparison/node_inspector.rb +5 -9
  30. data/lib/canon/comparison/pipeline.rb +269 -0
  31. data/lib/canon/comparison/profile_definition.rb +0 -2
  32. data/lib/canon/comparison/ruby_object_comparator.rb +1 -1
  33. data/lib/canon/comparison/strategies/match_strategy_factory.rb +9 -58
  34. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +4 -11
  35. data/lib/canon/comparison/strategies.rb +16 -0
  36. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +1 -4
  37. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +0 -3
  38. data/lib/canon/comparison/xml_comparator/child_comparison.rb +0 -6
  39. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +1 -6
  40. data/lib/canon/comparison/xml_comparator/node_parser.rb +3 -7
  41. data/lib/canon/comparison/xml_comparator.rb +4 -492
  42. data/lib/canon/comparison/xml_comparator_helpers.rb +21 -0
  43. data/lib/canon/comparison/xml_node_comparison.rb +6 -121
  44. data/lib/canon/comparison/yaml_comparator.rb +0 -3
  45. data/lib/canon/comparison.rb +144 -267
  46. data/lib/canon/config/config_dsl.rb +159 -0
  47. data/lib/canon/config/env_provider.rb +0 -3
  48. data/lib/canon/config/env_schema.rb +48 -58
  49. data/lib/canon/config/profile_loader.rb +0 -1
  50. data/lib/canon/config.rb +116 -468
  51. data/lib/canon/diff/diff_block_builder.rb +0 -2
  52. data/lib/canon/diff/diff_classifier.rb +0 -5
  53. data/lib/canon/diff/diff_context.rb +0 -2
  54. data/lib/canon/diff/diff_context_builder.rb +0 -2
  55. data/lib/canon/diff/diff_line_builder.rb +0 -3
  56. data/lib/canon/diff/diff_node_enricher.rb +0 -4
  57. data/lib/canon/diff/diff_node_mapper.rb +0 -4
  58. data/lib/canon/diff/diff_report_builder.rb +0 -4
  59. data/lib/canon/diff/formatting_detector.rb +0 -1
  60. data/lib/canon/diff/node_serializer.rb +0 -7
  61. data/lib/canon/diff.rb +39 -0
  62. data/lib/canon/diff_formatter/by_line/base_formatter.rb +5 -19
  63. data/lib/canon/diff_formatter/by_line/html_formatter.rb +8 -21
  64. data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -3
  65. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -3
  66. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +7 -26
  67. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -3
  68. data/lib/canon/diff_formatter/by_line_formatter.rb +1 -2
  69. data/lib/canon/diff_formatter/by_object/base_formatter.rb +8 -15
  70. data/lib/canon/diff_formatter/by_object/json_formatter.rb +0 -2
  71. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +0 -2
  72. data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +0 -2
  73. data/lib/canon/diff_formatter/debug_output.rb +0 -2
  74. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +24 -58
  75. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +0 -2
  76. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +3 -4
  77. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +1 -7
  78. data/lib/canon/diff_formatter/diff_detail_formatter.rb +0 -7
  79. data/lib/canon/diff_formatter/diff_detail_formatter_helpers.rb +23 -0
  80. data/lib/canon/diff_formatter/theme.rb +2 -2
  81. data/lib/canon/diff_formatter.rb +13 -13
  82. data/lib/canon/formatters/html4_formatter.rb +0 -2
  83. data/lib/canon/formatters/html5_formatter.rb +0 -2
  84. data/lib/canon/formatters/html_formatter.rb +0 -3
  85. data/lib/canon/formatters/json_formatter.rb +0 -1
  86. data/lib/canon/formatters/xml_formatter.rb +0 -4
  87. data/lib/canon/formatters/yaml_formatter.rb +0 -1
  88. data/lib/canon/formatters.rb +16 -0
  89. data/lib/canon/html/data_model.rb +0 -10
  90. data/lib/canon/html/nokogiri_support.rb +60 -0
  91. data/lib/canon/html.rb +5 -3
  92. data/lib/canon/options/cli_generator.rb +0 -2
  93. data/lib/canon/options/registry.rb +0 -2
  94. data/lib/canon/options.rb +9 -0
  95. data/lib/canon/pretty_printer/html.rb +0 -1
  96. data/lib/canon/pretty_printer/xml.rb +6 -1
  97. data/lib/canon/pretty_printer/xml_normalized.rb +5 -7
  98. data/lib/canon/pretty_printer.rb +12 -0
  99. data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
  100. data/lib/canon/tree_diff/adapters.rb +14 -0
  101. data/lib/canon/tree_diff/core/attribute_comparator.rb +0 -6
  102. data/lib/canon/tree_diff/core/node_signature.rb +1 -1
  103. data/lib/canon/tree_diff/core/tree_node.rb +13 -5
  104. data/lib/canon/tree_diff/core.rb +17 -0
  105. data/lib/canon/tree_diff/matchers/hash_matcher.rb +0 -7
  106. data/lib/canon/tree_diff/matchers/similarity_matcher.rb +1 -5
  107. data/lib/canon/tree_diff/matchers/structural_propagator.rb +1 -5
  108. data/lib/canon/tree_diff/matchers.rb +15 -0
  109. data/lib/canon/tree_diff/operation_converter.rb +0 -8
  110. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +2 -12
  111. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +13 -7
  112. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +2 -2
  113. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +4 -6
  114. data/lib/canon/tree_diff/operation_converter_helpers.rb +18 -0
  115. data/lib/canon/tree_diff/operations/operation_detector.rb +2 -5
  116. data/lib/canon/tree_diff/operations.rb +13 -0
  117. data/lib/canon/tree_diff.rb +26 -27
  118. data/lib/canon/validators/base_validator.rb +0 -2
  119. data/lib/canon/validators/html_validator.rb +0 -1
  120. data/lib/canon/validators/json_validator.rb +0 -1
  121. data/lib/canon/validators/xml_validator.rb +0 -1
  122. data/lib/canon/validators/yaml_validator.rb +0 -1
  123. data/lib/canon/validators.rb +12 -0
  124. data/lib/canon/version.rb +1 -1
  125. data/lib/canon/xml/c14n.rb +0 -4
  126. data/lib/canon/xml/data_model.rb +206 -43
  127. data/lib/canon/xml/line_range_mapper.rb +0 -2
  128. data/lib/canon/xml/nodes/attribute_node.rb +0 -2
  129. data/lib/canon/xml/nodes/comment_node.rb +0 -2
  130. data/lib/canon/xml/nodes/element_node.rb +0 -2
  131. data/lib/canon/xml/nodes/namespace_node.rb +0 -2
  132. data/lib/canon/xml/nodes/processing_instruction_node.rb +0 -2
  133. data/lib/canon/xml/nodes/root_node.rb +0 -2
  134. data/lib/canon/xml/nodes/text_node.rb +0 -2
  135. data/lib/canon/xml/nodes.rb +19 -0
  136. data/lib/canon/xml/processor.rb +10 -7
  137. data/lib/canon/xml/sax/moxml_driver.rb +58 -0
  138. data/lib/canon/xml/sax/nokogiri_driver.rb +54 -0
  139. data/lib/canon/xml/sax.rb +37 -0
  140. data/lib/canon/xml/sax_builder.rb +31 -27
  141. data/lib/canon/xml.rb +34 -0
  142. data/lib/canon/xml_backend.rb +40 -19
  143. data/lib/canon/xml_parsing.rb +89 -116
  144. data/lib/canon.rb +26 -17
  145. data/lib/tasks/benchmark_runner.rb +4 -0
  146. data/lib/tasks/performance.rake +2 -58
  147. data/lib/tasks/performance_comparator.rb +132 -65
  148. data/lib/tasks/performance_helpers.rb +4 -249
  149. data/lib/tasks/performance_report.rb +313 -0
  150. metadata +31 -13
  151. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +0 -64
  152. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +0 -64
  153. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +0 -167
  154. data/lib/canon/comparison/dimensions/base_dimension.rb +0 -107
  155. data/lib/canon/comparison/dimensions/comments_dimension.rb +0 -117
  156. data/lib/canon/comparison/dimensions/element_position_dimension.rb +0 -86
  157. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +0 -115
  158. data/lib/canon/comparison/dimensions/text_content_dimension.rb +0 -102
  159. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +0 -300
@@ -2,16 +2,6 @@
2
2
 
3
3
  require "nokogiri" unless RUBY_ENGINE == "opal"
4
4
  require "set"
5
- require_relative "../data_model"
6
- require_relative "../xml_backend"
7
- require_relative "../xml_parsing"
8
- require_relative "nodes/root_node"
9
- require_relative "nodes/element_node"
10
- require_relative "nodes/namespace_node"
11
- require_relative "nodes/attribute_node"
12
- require_relative "nodes/text_node"
13
- require_relative "nodes/comment_node"
14
- require_relative "nodes/processing_instruction_node"
15
5
 
16
6
  module Canon
17
7
  module Xml
@@ -169,7 +159,7 @@ preserve_whitespace: false)
169
159
  when Nokogiri::XML::Element
170
160
  build_element_node(nokogiri_node,
171
161
  preserve_whitespace: preserve_whitespace)
172
- when Nokogiri::XML::Text
162
+ when Nokogiri::XML::Text, Nokogiri::XML::CDATA
173
163
  build_text_node(nokogiri_node,
174
164
  preserve_whitespace: preserve_whitespace)
175
165
  when Nokogiri::XML::Comment
@@ -232,7 +222,10 @@ preserve_whitespace: false)
232
222
  end
233
223
 
234
224
  def self.build_attribute_nodes(nokogiri_element, element)
235
- nokogiri_element.attributes.each_value do |attr|
225
+ # attribute_nodes, not the attributes hash: the hash is keyed by
226
+ # local name and collapses e.g. attr / a:attr / b:attr into one
227
+ # entry, losing namespaced attributes (W3C C14N ex 3.3).
228
+ nokogiri_element.attribute_nodes.each do |attr|
236
229
  attr_node = Nodes::AttributeNode.new(
237
230
  name: attr.name,
238
231
  value: attr.value,
@@ -269,26 +262,183 @@ preserve_whitespace: false)
269
262
 
270
263
  def self.from_moxml_xml(xml_string, preserve_whitespace:)
271
264
  doc = Canon::XmlParsing.parse(xml_string)
272
- build_from_moxml(doc, preserve_whitespace: preserve_whitespace)
265
+ check_moxml_relative_namespace_uris(doc)
266
+ result = build_from_moxml(doc, preserve_whitespace: preserve_whitespace)
267
+ # Canon owns this document's full lifecycle: the canon tree holds
268
+ # no engine references once built, so release the native tree now
269
+ # instead of waiting for the GC finalizer (moxml#134; no-op on
270
+ # GC-managed adapters).
271
+ doc.free
272
+ result
273
+ end
274
+
275
+ def self.check_moxml_relative_namespace_uris(node)
276
+ case node
277
+ when Moxml::Element
278
+ node.namespace_definitions.each do |ns|
279
+ href = ns.uri
280
+ next if href.nil? || href.empty?
281
+ if relative_uri?(href)
282
+ raise Canon::Error,
283
+ "Relative namespace URI not allowed: #{href}"
284
+ end
285
+ end
286
+ node.children.each { |child| check_moxml_relative_namespace_uris(child) }
287
+ when Moxml::Document
288
+ node.children.each { |child| check_moxml_relative_namespace_uris(child) }
289
+ end
273
290
  end
274
291
 
275
292
  def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
276
293
  root = Nodes::RootNode.new
277
294
 
278
295
  if moxml_doc.is_a?(Moxml::Document) && moxml_doc.root
279
- root.add_child(build_moxml_element_node(moxml_doc.root,
280
- preserve_whitespace: preserve_whitespace))
296
+ element = build_moxml_subtree_from_records(moxml_doc.root,
297
+ preserve_whitespace: preserve_whitespace)
298
+ root.add_child(element) if element
299
+ moxml_doc.children.each do |child|
300
+ next if child.equal?(moxml_doc.root)
301
+ next if child.is_a?(Moxml::Doctype)
302
+
303
+ node = build_moxml_node(child,
304
+ preserve_whitespace: preserve_whitespace)
305
+ root.add_child(node) if node
306
+ end
307
+ else
308
+ moxml_doc.children.each do |child|
309
+ next if child.is_a?(Moxml::Doctype)
310
+
311
+ node = build_moxml_node(child,
312
+ preserve_whitespace: preserve_whitespace)
313
+ root.add_child(node) if node
314
+ end
281
315
  end
282
316
 
283
317
  root
284
318
  end
285
319
 
286
- def self.build_moxml_node(node, preserve_whitespace: false)
320
+ # Document path: build the root subtree from materialize records —
321
+ # one flattened record stream (moxml#132/#138) instead of a wrapper
322
+ # walk, so conversion costs no Moxml::Node allocation per node.
323
+ #
324
+ # Records arrive post-order (children before parents): completed
325
+ # nodes stack up with their depth, and an element record adopts
326
+ # every completed node deeper than itself. Each element's OWN
327
+ # namespace declarations ride an identity-keyed stash; afterwards
328
+ # assign_moxml_namespace_scopes expands them to in-scope scopes
329
+ # with the same first-wins/xml-prebound semantics as the Nokogiri
330
+ # collector.
331
+ def self.build_moxml_subtree_from_records(moxml_element,
332
+ preserve_whitespace: false)
333
+ frames = []
334
+ own_namespaces = {}.compare_by_identity
335
+
336
+ moxml_element.materialize do |record|
337
+ # The record contract is root-subtree-only since moxml 0.5.11
338
+ # (moxml#140). Older 0.5.x releases — still allowed by canon's
339
+ # gemspec floor — leaked epilog document-level records at depth
340
+ # 0, which the document-children enumeration below also covers;
341
+ # one integer compare per record keeps those working.
342
+ next if record[:depth].zero? && record[:kind] != :element
343
+
344
+ node = case record[:kind]
345
+ when :element
346
+ build_moxml_element_from_record(record, frames,
347
+ own_namespaces)
348
+ when :text, :cdata
349
+ build_moxml_text_from_record(record, preserve_whitespace)
350
+ when :comment
351
+ Nodes::CommentNode.new(value: record[:text])
352
+ when :processing_instruction
353
+ Nodes::ProcessingInstructionNode.new(
354
+ target: record[:qname],
355
+ data: record[:text] || "",
356
+ )
357
+ end
358
+
359
+ frames.push([record[:depth], node]) if node
360
+ end
361
+
362
+ top = frames.last
363
+ return nil unless top
364
+
365
+ element = top[1]
366
+ assign_moxml_namespace_scopes(element,
367
+ { "xml" => "http://www.w3.org/XML/1998/namespace" },
368
+ own_namespaces)
369
+ element
370
+ end
371
+
372
+ def self.build_moxml_element_from_record(record, frames, own_namespaces)
373
+ element = Nodes::ElementNode.new(
374
+ name: record[:qname],
375
+ namespace_uri: record[:namespace_uri],
376
+ prefix: record[:prefix],
377
+ )
378
+
379
+ seen = {}
380
+ record[:attributes].each do |name, value, namespace_uri, prefix|
381
+ key = [name, namespace_uri]
382
+ next if seen.key?(key)
383
+
384
+ seen[key] = true
385
+ element.add_attribute(Nodes::AttributeNode.new(
386
+ name: name,
387
+ value: value,
388
+ namespace_uri: namespace_uri,
389
+ prefix: prefix,
390
+ ))
391
+ end
392
+
393
+ # Adopt completed children (they stack in reverse: unshift
394
+ # restores document order).
395
+ children = []
396
+ children.unshift(frames.pop[1]) while frames.any? && frames.last[0] > record[:depth]
397
+ children.each { |child| element.add_child(child) }
398
+
399
+ own_namespaces[element] = record[:namespaces]
400
+ element
401
+ end
402
+
403
+ # Text records inside a document subtree always have an element
404
+ # parent (only the document element sits at depth 0), so the
405
+ # whitespace-only skip rule matches the wrapper path exactly.
406
+ def self.build_moxml_text_from_record(record, preserve_whitespace)
407
+ content = record[:text].to_s
408
+ return nil if !preserve_whitespace && content.strip.empty?
409
+
410
+ Nodes::TextNode.new(value: content, original: content)
411
+ end
412
+
413
+ # Expand each element's own declarations to in-scope bindings:
414
+ # the element's declarations shadow inherited ones; xml is
415
+ # prebound. Scope flows down the canon tree — no engine calls.
416
+ def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
417
+ scope = inherited.dup
418
+ own_namespaces[element].to_a.each do |prefix, uri|
419
+ scope[prefix || ""] = uri
420
+ end
421
+
422
+ scope.each do |prefix, uri|
423
+ element.add_namespace(Nodes::NamespaceNode.new(
424
+ prefix: prefix,
425
+ uri: uri,
426
+ ))
427
+ end
428
+
429
+ element.children.each do |child|
430
+ assign_moxml_namespace_scopes(child, scope, own_namespaces) if child.is_a?(Nodes::ElementNode)
431
+ end
432
+ end
433
+
434
+ def self.build_moxml_node(node, preserve_whitespace: false,
435
+ inherited_namespaces: nil)
287
436
  case node
288
437
  when Moxml::Element
289
438
  build_moxml_element_node(node,
290
- preserve_whitespace: preserve_whitespace)
291
- when Moxml::Text
439
+ preserve_whitespace: preserve_whitespace,
440
+ inherited_namespaces: inherited_namespaces)
441
+ when Moxml::Text, Moxml::Cdata
292
442
  build_moxml_text_node(node, preserve_whitespace: preserve_whitespace)
293
443
  when Moxml::Comment
294
444
  build_moxml_comment_node(node)
@@ -298,7 +448,8 @@ preserve_whitespace: false)
298
448
  end
299
449
 
300
450
  def self.build_moxml_element_node(moxml_element,
301
- preserve_whitespace: false)
451
+ preserve_whitespace: false,
452
+ inherited_namespaces: nil)
302
453
  ns = moxml_element.namespace
303
454
  element = Nodes::ElementNode.new(
304
455
  name: moxml_element.name,
@@ -306,44 +457,56 @@ preserve_whitespace: false)
306
457
  prefix: ns&.prefix,
307
458
  )
308
459
 
309
- build_moxml_namespace_nodes(moxml_element, element)
460
+ scope = moxml_namespace_scope(moxml_element, inherited_namespaces)
461
+ scope.each do |prefix, uri|
462
+ element.add_namespace(Nodes::NamespaceNode.new(
463
+ prefix: prefix,
464
+ uri: uri,
465
+ ))
466
+ end
467
+
310
468
  build_moxml_attribute_nodes(moxml_element, element)
311
469
 
312
470
  moxml_element.children.each do |child|
313
471
  node = build_moxml_node(child,
314
- preserve_whitespace: preserve_whitespace)
472
+ preserve_whitespace: preserve_whitespace,
473
+ inherited_namespaces: scope)
315
474
  element.add_child(node) if node
316
475
  end
317
476
 
318
477
  element
319
478
  end
320
479
 
321
- def self.build_moxml_namespace_nodes(moxml_element, element)
322
- moxml_element.namespace_definitions.each do |ns|
323
- ns_node = Nodes::NamespaceNode.new(
324
- prefix: ns.prefix || "",
325
- uri: ns.uri,
326
- )
327
- element.add_namespace(ns_node)
328
- end
329
-
330
- unless element.namespace_nodes.any? do |n|
331
- n.prefix == "xml"
332
- end
333
- element.add_namespace(Nodes::NamespaceNode.new(
334
- prefix: "xml",
335
- uri: "http://www.w3.org/XML/1998/namespace",
336
- ))
480
+ # In-scope namespace bindings: the element's own declarations
481
+ # shadow inherited ones, xml is prebound. The scope is passed down
482
+ # the recursion so each element pays one declaration fetch instead
483
+ # of an ancestor walk through the adapter layer (the Nokogiri
484
+ # collector walks ancestors because those calls are cheap there).
485
+ def self.moxml_namespace_scope(moxml_element, inherited)
486
+ scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
487
+ moxml_element.namespace_definitions.each do |decl|
488
+ scope[decl.prefix || ""] = decl.uri
337
489
  end
490
+ scope
338
491
  end
339
492
 
340
493
  def self.build_moxml_attribute_nodes(moxml_element, element)
494
+ seen = {}
341
495
  moxml_element.attributes.each do |attr|
342
- attr_node = Nodes::AttributeNode.new(
343
- name: attr.name,
344
- value: attr.value,
345
- )
346
- element.add_attribute(attr_node)
496
+ # Duplicate attribute names are invalid XML; engines expose
497
+ # them differently (libxml2 lists repeats, libleptris dedups
498
+ # last-wins). Canon follows the Nokogiri presentation contract:
499
+ # first occurrence wins, keyed by name + namespace.
500
+ key = [attr.name, attr.namespace&.uri]
501
+ next if seen.key?(key)
502
+
503
+ seen[key] = true
504
+ element.add_attribute(Nodes::AttributeNode.new(
505
+ name: attr.name,
506
+ value: attr.value,
507
+ namespace_uri: attr.namespace&.uri,
508
+ prefix: attr.namespace&.prefix,
509
+ ))
347
510
  end
348
511
  end
349
512
 
@@ -364,7 +527,7 @@ preserve_whitespace: false)
364
527
  def self.build_moxml_pi_node(moxml_pi)
365
528
  Nodes::ProcessingInstructionNode.new(
366
529
  target: moxml_pi.target,
367
- data: moxml_pi.data,
530
+ data: moxml_pi.content,
368
531
  )
369
532
  end
370
533
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../pretty_printer/xml"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  # Maps DOM elements to line ranges in pretty-printed XML
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../node"
4
-
5
3
  module Canon
6
4
  module Xml
7
5
  module Nodes
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Xml
5
+ # XPath data model node types. All nodes inherit from
6
+ # {Canon::Xml::Node}. Children are autoloaded — never
7
+ # `require_relative` them.
8
+ module Nodes
9
+ autoload :AttributeNode, "canon/xml/nodes/attribute_node"
10
+ autoload :CommentNode, "canon/xml/nodes/comment_node"
11
+ autoload :ElementNode, "canon/xml/nodes/element_node"
12
+ autoload :NamespaceNode, "canon/xml/nodes/namespace_node"
13
+ autoload :ProcessingInstructionNode,
14
+ "canon/xml/nodes/processing_instruction_node"
15
+ autoload :RootNode, "canon/xml/nodes/root_node"
16
+ autoload :TextNode, "canon/xml/nodes/text_node"
17
+ end
18
+ end
19
+ end
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "character_encoder"
4
- require_relative "namespace_handler"
5
- require_relative "attribute_handler"
6
- require_relative "xml_base_handler"
7
-
8
3
  module Canon
9
4
  module Xml
10
5
  # C14N 1.1 processor
@@ -135,10 +130,18 @@ module Canon
135
130
  # Add line break before PI if it's outside document element
136
131
  output << "\n" if parent_element.nil? && output.length.positive?
137
132
 
133
+ # C14N renders "<?target S data?>". Both CRuby engines now
134
+ # consume the leading whitespace run after the PI target
135
+ # (leptris-ruby#85), but the canonical writer owns the rule —
136
+ # it must not depend on engine pre-processing (and the pure-Ruby
137
+ # adapters don't). Trailing whitespace and internal newlines are
138
+ # preserved (W3C ex 3.1 keeps them).
139
+ data = node.data.to_s.sub(/\A[ \t\r\n]+/, "")
140
+
138
141
  output << "<?" << node.target
139
142
 
140
- unless node.data.empty?
141
- output << " " << node.data
143
+ unless data.empty?
144
+ output << " " << data
142
145
  end
143
146
 
144
147
  output << "?>"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Xml
5
+ module Sax
6
+ # moxml SAX driver (rexml adapter under Opal). moxml splits namespace
7
+ # declarations out of the attribute hash and reports them separately,
8
+ # so this driver recombines them as inline "xmlns"/"xmlns:prefix"
9
+ # pairs to match the builder's engine-neutral protocol.
10
+ class MoxmlDriver < Moxml::SAX::Handler
11
+ def initialize(builder)
12
+ super()
13
+ @builder = builder
14
+ end
15
+
16
+ def parse(xml_string)
17
+ Canon::XmlParsing.moxml_context.sax_parse(xml_string, self)
18
+ end
19
+
20
+ def on_start_element(name, attributes = {}, namespaces = {})
21
+ attrs = attributes.to_a
22
+ namespaces.each do |prefix, uri|
23
+ attrs << if prefix.nil? || prefix.empty?
24
+ ["xmlns", uri]
25
+ else
26
+ ["xmlns:#{prefix}", uri]
27
+ end
28
+ end
29
+ @builder.start_element(name, attrs)
30
+ end
31
+
32
+ def on_end_element(name)
33
+ @builder.end_element(name)
34
+ end
35
+
36
+ def on_characters(text)
37
+ @builder.characters(text)
38
+ end
39
+
40
+ def on_cdata(text)
41
+ @builder.cdata(text)
42
+ end
43
+
44
+ def on_comment(text)
45
+ @builder.comment(text)
46
+ end
47
+
48
+ def on_processing_instruction(name, content)
49
+ @builder.processing_instruction(name, content || "")
50
+ end
51
+
52
+ def on_error(error)
53
+ @builder.error(error.message.to_s)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri" unless RUBY_ENGINE == "opal"
4
+
5
+ module Canon
6
+ module Xml
7
+ module Sax
8
+ # Nokogiri SAX driver: the builder's protocol already matches
9
+ # Nokogiri::XML::SAX::Document, so every event is a pass-through.
10
+ class NokogiriDriver < Nokogiri::XML::SAX::Document
11
+ def initialize(builder)
12
+ super()
13
+ @builder = builder
14
+ end
15
+
16
+ def parse(xml_string)
17
+ Nokogiri::XML::SAX::Parser.new(self).parse(xml_string)
18
+ end
19
+
20
+ def error(string)
21
+ @builder.error(string)
22
+ end
23
+
24
+ def warning(string)
25
+ @builder.warning(string)
26
+ end
27
+
28
+ def start_element(name, attrs = [])
29
+ @builder.start_element(name, attrs)
30
+ end
31
+
32
+ def end_element(name)
33
+ @builder.end_element(name)
34
+ end
35
+
36
+ def characters(string)
37
+ @builder.characters(string)
38
+ end
39
+
40
+ def cdata_block(string)
41
+ @builder.cdata(string)
42
+ end
43
+
44
+ def comment(string)
45
+ @builder.comment(string)
46
+ end
47
+
48
+ def processing_instruction(name, content)
49
+ @builder.processing_instruction(name, content)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Xml
5
+ # SAX engine selection and driving for the data-model builder.
6
+ #
7
+ # The builder (SaxBuilder) owns tree construction and speaks a single
8
+ # engine-neutral event protocol (Nokogiri-shaped: qname + attribute
9
+ # pairs with xmlns entries inline). Each driver adapts one engine's SAX
10
+ # API to that protocol. OCP: a new engine is a new driver plus one line
11
+ # here; the builder never changes.
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 callbacks — BUT 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.
20
+ module Sax
21
+ autoload :NokogiriDriver, "canon/xml/sax/nokogiri_driver"
22
+ autoload :MoxmlDriver, "canon/xml/sax/moxml_driver"
23
+
24
+ class << self
25
+ # Drive `builder` with the runtime's SAX engine.
26
+ def parse(xml_string, builder)
27
+ if RUBY_ENGINE == "opal"
28
+ MoxmlDriver.new(builder).parse(xml_string)
29
+ else
30
+ NokogiriDriver.new(builder).parse(xml_string)
31
+ end
32
+ nil
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end