canon 0.3.35 → 0.3.37
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc8ab08578b3b1ceed40cb8f14ee8e83fdb116d26214a4a546789108950802dc
|
|
4
|
+
data.tar.gz: 435c4f9300160c8b502b8441096b2eea9aec0e182f025e84789f8f0fd492451d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9c49d99613a801415eec95e663caa577319e9c4ce63546c1708c0599c891289e225c415279131c2652f83e9cec17b225c15f0bbc620da1a1237cc321eb2d381
|
|
7
|
+
data.tar.gz: fa8e1ea33c6527b7a215c0b8e097f4266d3db0cc00e865bc39c883b8b3de8a8683533beb66bccf1538d89059aeb348a716c5c1d9fb2b4eefd14d257b23cc993c
|
|
@@ -75,6 +75,17 @@ module Canon
|
|
|
75
75
|
# Use ElementMatcher for semantic comparison
|
|
76
76
|
def use_element_matcher_comparison(children1, children2, parent_node, comparator,
|
|
77
77
|
opts, child_opts, diff_children, differences)
|
|
78
|
+
# FAST PATH: positionally paired children — compare pairwise
|
|
79
|
+
# with zero MatchResult allocation. Same pairing the matcher
|
|
80
|
+
# would emit; pos1==pos2 so position_changed? never fires.
|
|
81
|
+
if Canon::Xml::ElementMatcher.positionally_paired?(children1,
|
|
82
|
+
children2)
|
|
83
|
+
return compare_positionally_paired(
|
|
84
|
+
children1, children2, comparator, opts, child_opts,
|
|
85
|
+
diff_children, differences
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
# Single-level matching: the comparator itself descends via
|
|
79
90
|
# compare_nodes below, so nested matches would be discarded.
|
|
80
91
|
matches = Canon::Xml::ElementMatcher.new
|
|
@@ -92,6 +103,24 @@ module Canon
|
|
|
92
103
|
opts, child_opts, diff_children, differences)
|
|
93
104
|
end
|
|
94
105
|
|
|
106
|
+
# Pairwise compare when ElementMatcher.positionally_paired?
|
|
107
|
+
# has already proved the pairing. No MatchResult objects.
|
|
108
|
+
def compare_positionally_paired(children1, children2, comparator,
|
|
109
|
+
_opts, child_opts, diff_children,
|
|
110
|
+
differences)
|
|
111
|
+
all_equivalent = true
|
|
112
|
+
i = 0
|
|
113
|
+
while i < children1.length
|
|
114
|
+
result = comparator.compare_nodes(
|
|
115
|
+
children1[i], children2[i],
|
|
116
|
+
child_opts, child_opts, diff_children, differences
|
|
117
|
+
)
|
|
118
|
+
all_equivalent = false unless result == Comparison::EQUIVALENT
|
|
119
|
+
i += 1
|
|
120
|
+
end
|
|
121
|
+
all_equivalent ? Comparison::EQUIVALENT : Comparison::UNEQUAL_ELEMENTS
|
|
122
|
+
end
|
|
123
|
+
|
|
95
124
|
# Process ElementMatcher results
|
|
96
125
|
def process_matches(matches, _children1, _children2, _parent_node, comparator,
|
|
97
126
|
opts, child_opts, diff_children, differences)
|
|
@@ -613,13 +613,17 @@ module Canon
|
|
|
613
613
|
# occurrence. Shorter strings match too many places in the document.
|
|
614
614
|
min_locate_length = 3
|
|
615
615
|
|
|
616
|
-
# Element added (only in text2)
|
|
616
|
+
# Element added (only in text2). Index-aware first — see the
|
|
617
|
+
# removed branch note (identical-sibling anchor bug).
|
|
617
618
|
if before.nil?
|
|
618
|
-
loc = if
|
|
619
|
-
|
|
620
|
-
else
|
|
621
|
-
SourceLocator.locate(after, @text2, @line_map2)
|
|
619
|
+
loc = if path
|
|
620
|
+
locate_element_at_index(after, @text2, @line_map2, path)
|
|
622
621
|
end
|
|
622
|
+
loc ||= if after.length < min_locate_length && path
|
|
623
|
+
locate_via_parent_element(path, @text2, @line_map2)
|
|
624
|
+
else
|
|
625
|
+
SourceLocator.locate(after, @text2, @line_map2)
|
|
626
|
+
end
|
|
623
627
|
|
|
624
628
|
if loc
|
|
625
629
|
end_line = find_end_line(loc[:line_number], @line_map2, after)
|
|
@@ -643,13 +647,20 @@ module Canon
|
|
|
643
647
|
return
|
|
644
648
|
end
|
|
645
649
|
|
|
646
|
-
# Element removed (only in text1)
|
|
650
|
+
# Element removed (only in text1). The path-aware locate runs
|
|
651
|
+
# FIRST: an identical sibling earlier in the document would win
|
|
652
|
+
# a plain first-occurrence search, anchoring the deletion to
|
|
653
|
+
# the wrong element and letting the line builder absorb it
|
|
654
|
+
# into a formatting gap (issue #85/#86).
|
|
647
655
|
if after.nil?
|
|
648
|
-
loc = if
|
|
649
|
-
|
|
650
|
-
else
|
|
651
|
-
SourceLocator.locate(before, @text1, @line_map1)
|
|
656
|
+
loc = if path
|
|
657
|
+
locate_element_at_index(before, @text1, @line_map1, path)
|
|
652
658
|
end
|
|
659
|
+
loc ||= if before.length < min_locate_length && path
|
|
660
|
+
locate_via_parent_element(path, @text1, @line_map1)
|
|
661
|
+
else
|
|
662
|
+
SourceLocator.locate(before, @text1, @line_map1)
|
|
663
|
+
end
|
|
653
664
|
|
|
654
665
|
if loc
|
|
655
666
|
end_line = find_end_line(loc[:line_number], @line_map1, before)
|
|
@@ -982,6 +993,39 @@ module Canon
|
|
|
982
993
|
SourceLocator.locate(value, text, line_map)
|
|
983
994
|
end
|
|
984
995
|
|
|
996
|
+
# Index-aware location for element_structure changes: the path's
|
|
997
|
+
# last bracketed segment names the element itself (unlike
|
|
998
|
+
# text_content paths, whose last segment is the text node).
|
|
999
|
+
# Picks the occurrence whose element index matches the path, so
|
|
1000
|
+
# identical siblings anchor at the right one; falls back to the
|
|
1001
|
+
# first occurrence when no indexed match exists.
|
|
1002
|
+
def locate_element_at_index(value, text, line_map, path)
|
|
1003
|
+
return nil if value.nil? || value.length < 3
|
|
1004
|
+
|
|
1005
|
+
segments = path.split("/").reject(&:empty?)
|
|
1006
|
+
element_segment = segments.reverse.find { |seg| seg.include?("[") }
|
|
1007
|
+
return nil unless element_segment
|
|
1008
|
+
|
|
1009
|
+
element_match = element_segment.match(/([a-zA-Z0-9_:-]+)\[(\d+)\]/)
|
|
1010
|
+
return nil unless element_match
|
|
1011
|
+
|
|
1012
|
+
element_name = element_match[1]
|
|
1013
|
+
target_index = element_match[2].to_i
|
|
1014
|
+
|
|
1015
|
+
# Occurrence offsets point AT the element's own opening tag,
|
|
1016
|
+
# so the prefix scan counts exactly the preceding siblings —
|
|
1017
|
+
# no "inside the element" correction (count_elements_before_
|
|
1018
|
+
# position subtracts one for text-node offsets).
|
|
1019
|
+
opener = /<#{element_name}[>\s]/
|
|
1020
|
+
occurrences = SourceLocator.locate_all(value, text, line_map)
|
|
1021
|
+
occurrences.each do |occ|
|
|
1022
|
+
count = text[0...occ[:char_offset]].scan(opener).length
|
|
1023
|
+
return occ if count == target_index
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
SourceLocator.locate(value, text, line_map)
|
|
1027
|
+
end
|
|
1028
|
+
|
|
985
1029
|
# Fallback location strategy for text_content when locate_at_element_index
|
|
986
1030
|
# fails (e.g., the text value is too short to locate reliably).
|
|
987
1031
|
# Walks the full element hierarchy from the path to locate the correct
|
data/lib/canon/version.rb
CHANGED
|
@@ -26,12 +26,12 @@ module Canon
|
|
|
26
26
|
return false unless Canon::XmlBackend.moxml? &&
|
|
27
27
|
Canon::XmlParsing.moxml_adapter_name == :leptris
|
|
28
28
|
|
|
29
|
-
# Node#digest
|
|
30
|
-
#
|
|
29
|
+
# moxml Node#digest (moxml#173) wraps leptris Node#digest;
|
|
30
|
+
# feature-detect on a throwaway document.
|
|
31
31
|
doc = Canon::XmlParsing.moxml_context.parse("<r/>", readonly: true,
|
|
32
32
|
strict: false)
|
|
33
33
|
root = doc.root
|
|
34
|
-
digestable = !root.
|
|
34
|
+
digestable = !root.digest(drop_ws_text: true).nil?
|
|
35
35
|
doc.free
|
|
36
36
|
digestable
|
|
37
37
|
rescue StandardError
|
|
@@ -85,7 +85,13 @@ module Canon
|
|
|
85
85
|
when Moxml::Text then child.content.strip.empty? ? nil : child.to_s
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
|
-
|
|
88
|
+
digest = root.digest(drop_ws_text: true)
|
|
89
|
+
# nil digest means the adapter has no Merkle support — never
|
|
90
|
+
# treat two nils as equal (that would false-positive under
|
|
91
|
+
# Opal / non-leptris backends where every tree digests nil).
|
|
92
|
+
return nil if digest.nil?
|
|
93
|
+
|
|
94
|
+
[digest, skeleton]
|
|
89
95
|
ensure
|
|
90
96
|
doc&.free
|
|
91
97
|
end
|
|
@@ -54,6 +54,7 @@ module Canon
|
|
|
54
54
|
class ElementMatcher
|
|
55
55
|
# Default attributes used to identify elements
|
|
56
56
|
DEFAULT_IDENTITY_ATTRS = %w[id ref name key class].freeze
|
|
57
|
+
EMPTY_PATH = [].freeze
|
|
57
58
|
|
|
58
59
|
# Represents the result of matching an element across two DOM trees
|
|
59
60
|
#
|
|
@@ -145,6 +146,21 @@ module Canon
|
|
|
145
146
|
@matches
|
|
146
147
|
end
|
|
147
148
|
|
|
149
|
+
# Public: true when both lists pair positionally (equal length,
|
|
150
|
+
# equal name/ns/identity at every index). ChildComparison uses
|
|
151
|
+
# this to skip MatchResult allocation on the common same-structure
|
|
152
|
+
# path.
|
|
153
|
+
def self.positionally_paired?(elems1, elems2)
|
|
154
|
+
return false if elems1.length != elems2.length || elems1.empty?
|
|
155
|
+
|
|
156
|
+
new.paired_positionally?(elems1, elems2)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Instance form of .positionally_paired?
|
|
160
|
+
def paired_positionally?(elems1, elems2)
|
|
161
|
+
pairwise_corresponding?(elems1, elems2)
|
|
162
|
+
end
|
|
163
|
+
|
|
148
164
|
private
|
|
149
165
|
|
|
150
166
|
# Match children recursively
|
|
@@ -295,7 +311,11 @@ module Canon
|
|
|
295
311
|
def record_positional_matches(elems1, elems2, path, recursive:)
|
|
296
312
|
elems1.each_index do |i|
|
|
297
313
|
elem1 = elems1[i]
|
|
298
|
-
|
|
314
|
+
# match_children_only never reads path; keep a shared empty
|
|
315
|
+
# for the non-recursive entry so we do not allocate one
|
|
316
|
+
# [name] array per element. Recursive match_trees still
|
|
317
|
+
# builds real paths for its public API.
|
|
318
|
+
elem_path = recursive || !path.empty? ? element_path(path, elem1) : EMPTY_PATH
|
|
299
319
|
@matches << MatchResult.new(
|
|
300
320
|
status: :matched,
|
|
301
321
|
elem1: elem1,
|
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.37
|
|
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-09-
|
|
11
|
+
date: 2026-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|