canon 0.3.84 → 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 +4 -4
- data/lib/canon/comparison/html_comparator.rb +100 -5
- data/lib/canon/comparison/html_parser.rb +53 -3
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +0 -7
- data/lib/canon/xml/tree_builder.rb +8 -0
- data/lib/canon/xml_parsing.rb +5 -25
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dce8920ae714ac966a2d0c91f81d8066c43cd7d7311bd4c100e00c4ea60ad66c
|
|
4
|
+
data.tar.gz: 0b1dd87f74475eb5e6ef3afa486ebd1ca4160d2aae408b7bfc410135687a117e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
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
|
-
|
|
318
|
-
|
|
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
|
|
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 [
|
|
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
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -278,13 +278,6 @@ preserve_whitespace: false)
|
|
|
278
278
|
# consumes them synchronously before the next record reuses
|
|
279
279
|
# the buffer.
|
|
280
280
|
moxml_element.materialize_fields do |kind, qname, prefix, namespace_uri, namespaces, attributes, text, depth|
|
|
281
|
-
# The record contract is root-subtree-only since moxml 0.5.11
|
|
282
|
-
# (moxml#140). Older 0.5.x releases — still allowed by canon's
|
|
283
|
-
# gemspec floor — leaked epilog document-level records at depth
|
|
284
|
-
# 0, which the document-children enumeration also covers; one
|
|
285
|
-
# integer compare per record keeps those working.
|
|
286
|
-
next if depth.zero? && kind != :element
|
|
287
|
-
|
|
288
281
|
# Relative-namespace validation rides the stream (the
|
|
289
282
|
# namespaces buffer is the element's own declarations) — no
|
|
290
283
|
# separate wrapper-tree walk per parse.
|
|
@@ -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
|
data/lib/canon/xml_parsing.rb
CHANGED
|
@@ -60,7 +60,7 @@ module Canon
|
|
|
60
60
|
# contract keys off the RECORD, not the raise: non-HTML
|
|
61
61
|
# malformed input stays loud; HTML-shaped input falls back to
|
|
62
62
|
# the HTML parser. Engine-independent by construction.
|
|
63
|
-
recorded = doc.
|
|
63
|
+
recorded = doc.parse_errors
|
|
64
64
|
if recorded.any?
|
|
65
65
|
raise Moxml::ParseError, recorded.join("; ") unless html_shaped?(xml_string)
|
|
66
66
|
|
|
@@ -114,31 +114,11 @@ module Canon
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
# Same underlying engine node?
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
# wrapper classes for one node (NativeNode vs the FFI Element —
|
|
121
|
-
# moxml#216), so identity fails exactly where consumers exclude
|
|
122
|
-
# the document element from children; the shared C pointer
|
|
123
|
-
# address is the stable identity. Inert once wrappers unify.
|
|
117
|
+
# Same underlying engine node? Wrappers are unified on every
|
|
118
|
+
# moxml the gemspec floor allows (moxml#219 landed in 0.5.40), so
|
|
119
|
+
# object identity is the whole answer.
|
|
124
120
|
def same_engine_node?(left, right)
|
|
125
|
-
|
|
126
|
-
return false unless defined?(::Moxml::Node) &&
|
|
127
|
-
left.is_a?(::Moxml::Node) && right.is_a?(::Moxml::Node)
|
|
128
|
-
|
|
129
|
-
address_left = moxml_node_address(left)
|
|
130
|
-
address_right = moxml_node_address(right)
|
|
131
|
-
!address_left.nil? && address_left == address_right
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def moxml_node_address(wrapper)
|
|
135
|
-
native = wrapper.native
|
|
136
|
-
case native
|
|
137
|
-
when ::Leptris::XML::NativeNode then native.address
|
|
138
|
-
when ::Leptris::XML::Node then native.c_ptr.address
|
|
139
|
-
end
|
|
140
|
-
rescue StandardError
|
|
141
|
-
nil
|
|
121
|
+
left.equal?(right)
|
|
142
122
|
end
|
|
143
123
|
|
|
144
124
|
# --- Type checks (any recognized engine node) ---
|
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.
|
|
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
|
+
date: 2026-10-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -44,28 +44,28 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 1.9.
|
|
47
|
+
version: 1.9.341
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 1.9.
|
|
54
|
+
version: 1.9.341
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: moxml
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.5.
|
|
61
|
+
version: 0.5.126
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.5.
|
|
68
|
+
version: 0.5.126
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: paint
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|