canon 0.3.85 → 0.3.86

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: 5dbb59b967dbaedab05c9dd8aa4186851ed71d055c44d89ecc23f2b8c8b92e96
4
- data.tar.gz: c665d661c0e4984f112a82239406ddc349b6120be032849adf5de7db75331217
3
+ metadata.gz: dce8920ae714ac966a2d0c91f81d8066c43cd7d7311bd4c100e00c4ea60ad66c
4
+ data.tar.gz: 0b1dd87f74475eb5e6ef3afa486ebd1ca4160d2aae408b7bfc410135687a117e
5
5
  SHA512:
6
- metadata.gz: f75058359a37d053f57d6c6d86b27330a0553f925ba49755fa24d31fd248ff0c72b3393d539f75e9cd16fefd02813a590b2e9192031ba1a201cadf4dc7175bc1
7
- data.tar.gz: e64b15e520378443018037c78931a7ce9172728ca7c25b8d0caad7acdc1bd6379682e63f8bd28bf88659eef99ec62d03dd86812db282ad936b92d5128d4031b0
6
+ metadata.gz: fd4055332d0210c7db855eff4027bc1c3a4a4a13a3a1d5398f76f34a00125bf4f180611109fd5e3437610d894f2dbaa27cb66bb5b10783c27cb019138be7f6ff
7
+ data.tar.gz: cce33bad9441d9f1d9d3f54ef18fc8944cbd37eb2f6b2bb607dfb6020426e3d455f06e0239b7dc9085cf915624a602a51a26219472fd03e2094f503a5987eda6
@@ -310,13 +310,12 @@ module Canon
310
310
 
311
311
  # Parse a node from string or return as-is
312
312
  # Applies preprocessing transformation before parsing if specified
313
- # Returns Nokogiri nodes for DOM comparison (preserves original behavior)
313
+ # Canon trees walk directly (engine HTML lane, semantic handoff);
314
+ # Nokogiri nodes and strings keep their existing paths.
314
315
  def parse_node(node, preprocessing = :none, match_opts = {})
315
- # If already a Canon::Xml::Node, convert to Nokogiri for DOM path
316
316
  if node.is_a?(Canon::Xml::Node)
317
- # Canon nodes used in semantic diff path, convert to Nokogiri for DOM path
318
- xml_str = Canon::Xml::DataModel.serialize(node)
319
- node = xml_str
317
+ apply_datamodel_preprocessing!(node, preprocessing, match_opts)
318
+ return node
320
319
  end
321
320
 
322
321
  # If already a Nokogiri node, check for incompatible XML documents
@@ -406,6 +405,102 @@ module Canon
406
405
  find_and_normalize_style_script(root)
407
406
  end
408
407
 
408
+ # Post-parse preprocessing for Canon trees (engine HTML lane,
409
+ # semantic handoff) — the canon-tree mirror of the Nokogiri
410
+ # fragment normalizers at the bottom of this module, applied at
411
+ # the same preprocessing points (:normalize/:format get the
412
+ # style/script pass and whitespace-only removal; :rendered adds
413
+ # whitespace collapsing).
414
+ def apply_datamodel_preprocessing!(root, preprocessing, match_opts)
415
+ return root unless %i[normalize format rendered].include?(preprocessing)
416
+
417
+ normalize_html_style_script_comments_datamodel(root)
418
+ if preprocessing == :rendered
419
+ normalize_rendered_whitespace_datamodel(root, match_opts)
420
+ end
421
+ remove_whitespace_only_text_nodes_datamodel(root)
422
+ root
423
+ end
424
+
425
+ # Canon-tree mirror of normalize_rendered_whitespace: collapse
426
+ # whitespace runs in text nodes under non-preserving ancestors
427
+ # and trim edge-child text. Mutates TextNode#value in place.
428
+ def normalize_rendered_whitespace_datamodel(root, match_opts = {})
429
+ return if match_opts[:text_content] == :strict
430
+
431
+ preserve_set = WhitespaceSensitivity
432
+ .format_default_preserve_elements(match_opts)
433
+ .to_set(&:to_s)
434
+ name_cache = {}
435
+ whitespace_result_cache = {}
436
+
437
+ each_datamodel_text_node(root) do |text|
438
+ next if ancestor_preserves_whitespace?(text.parent, preserve_set,
439
+ name_cache,
440
+ whitespace_result_cache)
441
+
442
+ content = text.value
443
+ normalized = if content.match?(/[\t\n\r\f\v]/) || content.include?(" ")
444
+ content.tr("\t\n\r\f\v", " ").squeeze(" ")
445
+ else
446
+ content
447
+ end
448
+
449
+ siblings = text.parent.children
450
+ if siblings.first.equal?(text) || siblings.last.equal?(text)
451
+ normalized = normalized.strip
452
+ end
453
+ text.value = normalized
454
+ end
455
+ end
456
+
457
+ # Canon-tree mirror of remove_whitespace_only_text_nodes:
458
+ # drop whitespace-only text under non-preserving ancestors,
459
+ # keeping NBSP-bearing and inline-significant nodes. Removals
460
+ # are batched per parent (writers go through children=).
461
+ def remove_whitespace_only_text_nodes_datamodel(root)
462
+ preserve_set = WhitespaceSensitivity
463
+ .format_default_preserve_elements(format: :html)
464
+ .to_set(&:to_s)
465
+ name_cache = {}
466
+ whitespace_result_cache = {}
467
+ to_remove = []
468
+
469
+ each_datamodel_text_node(root) do |text|
470
+ next if ancestor_preserves_whitespace?(text.parent, preserve_set,
471
+ name_cache,
472
+ whitespace_result_cache)
473
+
474
+ content = text.value
475
+ next if content.include?("\u00A0")
476
+ next if WhitespaceSensitivity.inline_whitespace_significant?(text)
477
+
478
+ to_remove << text if content.strip.empty?
479
+ end
480
+
481
+ to_remove.each do |text|
482
+ parent = text.parent
483
+ parent.children = parent.children.reject { |c| c.equal?(text) }
484
+ end
485
+ end
486
+
487
+ # Yield every TextNode in the tree in document order
488
+ # (pre-order walk — mirrors xpath(".//text()") order).
489
+ def each_datamodel_text_node(root)
490
+ stack = [root]
491
+ until stack.empty?
492
+ node = stack.pop
493
+ yield node if node.is_a?(Canon::Xml::Nodes::TextNode)
494
+
495
+ children = node.children
496
+ i = children.size
497
+ while i.positive?
498
+ i -= 1
499
+ stack.push(children[i])
500
+ end
501
+ end
502
+ end
503
+
409
504
  def find_and_normalize_style_script(node)
410
505
  return unless node.is_a?(Canon::Xml::Node)
411
506
 
@@ -16,20 +16,29 @@ module Canon
16
16
  # HtmlParser.detect_and_parse("<!DOCTYPE html><html>...</html>")
17
17
  class HtmlParser
18
18
  class << self
19
- # Parse HTML string into Nokogiri document with the correct parser
19
+ # Parse HTML string into the comparison tree.
20
+ #
21
+ # The engine lane (moxml Context#parse_html_fragment, moxml#353
22
+ # riding leptris' WHATWG fragment parsing) builds a Canon tree
23
+ # through the same TreeBuilder walk the XML lane uses: same
24
+ # tree construction as Nokogiri::HTML5.fragment (the #118
25
+ # html4/html5 unification semantics), native named-entity
26
+ # decoding, and a ~5x faster parse. The Nokogiri fragment
27
+ # path remains for runtimes without the engine surface.
20
28
  #
21
29
  # @param content [String, Object] Content to parse (returns as-is if not a string)
22
30
  # @param format [Symbol] HTML format (:html, :html4, :html5)
23
- # @return [Nokogiri::HTML::Document, Nokogiri::HTML5::Document, Nokogiri::HTML::DocumentFragment, Object]
31
+ # @return [Canon::Xml::Nodes::RootNode, Object] Canon tree or content
24
32
  def parse(content, format)
25
33
  return content unless content.is_a?(String)
26
- return content if already_parsed?(content)
27
34
 
28
35
  # Normalize HTML to ensure consistent parsing by HTML4.fragment
29
36
  # The key issue is that HTML4.fragment treats newlines after </head>
30
37
  # differently than no newlines, causing inconsistent parsing
31
38
  content = normalize_html_for_parsing(content)
32
39
 
40
+ return engine_fragment(content) if engine_html_fragments?
41
+
33
42
  begin
34
43
  case format
35
44
  when :html5
@@ -47,6 +56,47 @@ module Canon
47
56
  end
48
57
  end
49
58
 
59
+ # Parse an HTML string through the engine's WHATWG fragment
60
+ # parsing and build a Canon tree from the fragment children.
61
+ # Whitespace text nodes are preserved unconditionally — HTML
62
+ # whitespace significance is the comparison profile's decision
63
+ # (#112/#137 ride on stray whitespace surviving the parse),
64
+ # unlike the XML DOM lane's parse-time policy. Top-level
65
+ # element children convert through the materialize-records
66
+ # fast path; the wrapper walk serves the rare non-element
67
+ # top-level nodes (text, comments).
68
+ #
69
+ # NOTE: the fragment record buffer delivers a nil namespace
70
+ # URI for prefixed attributes on HTML-parsed trees (xml:lang
71
+ # arrives as name="lang", prefix="xml", uri=nil) where the
72
+ # wrapper API reports the bound URI — moxml#367. The TreeBuilder
73
+ # covers the xml prefix (the one legal binding); other
74
+ # foreign-content prefixes keep nil URIs on this lane until
75
+ # the records gap closes.
76
+ def engine_fragment(html)
77
+ frag = Canon::XmlParsing.moxml_context.parse_html_fragment(html)
78
+ root = Canon::Xml::Nodes::RootNode.new
79
+ frag.children.each do |child|
80
+ node =
81
+ if child.is_a?(Moxml::Element)
82
+ Canon::Xml::DataModel.build_moxml_subtree_from_records(
83
+ child, preserve_whitespace: true
84
+ )
85
+ else
86
+ Canon::Xml::DataModel.walk_moxml(child, preserve_whitespace: true)
87
+ end
88
+ root.add_child(node) if node
89
+ end
90
+ root
91
+ end
92
+
93
+ # Engine HTML fragments are served by adapters with an engine
94
+ # HTML mode (leptris, nokogiri) — moxml#353 pins WHATWG tree
95
+ # construction on both.
96
+ def engine_html_fragments?
97
+ %i[leptris nokogiri].include?(Canon::XmlParsing.moxml_adapter_name)
98
+ end
99
+
50
100
  # Check if content is already a parsed HTML document/fragment
51
101
  #
52
102
  # @param content [Object] Content to check
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.85"
4
+ VERSION = "0.3.86"
5
5
  end
@@ -100,7 +100,15 @@ module Canon
100
100
  limit = attributes.size
101
101
  while base < limit
102
102
  attr_name = attributes[base]
103
+ attr_prefix = attributes[base + 3]
104
+ # The xml prefix is bound by definition to the XML namespace
105
+ # (XML Namespaces 3): when a feed delivers the prefix without
106
+ # the URI — the HTML fragment records lane, moxml#367 — the
107
+ # constant is the one legal binding.
103
108
  attr_namespace_uri = attributes[base + 2]
109
+ if attr_namespace_uri.nil? && attr_prefix == "xml"
110
+ attr_namespace_uri = "http://www.w3.org/XML/1998/namespace"
111
+ end
104
112
  duplicate = false
105
113
  prior = 0
106
114
  while prior < base
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.85
4
+ version: 0.3.86
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-10-11 00:00:00.000000000 Z
11
+ date: 2026-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs