canon 0.2.0 → 0.2.2
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/docs/advanced/diff-classification.adoc +15 -0
- data/docs/advanced/verbose-mode-architecture.adoc +23 -0
- data/docs/features/match-options/html-policies.adoc +3 -0
- data/lib/canon/comparison/compare_profile.rb +5 -4
- data/lib/canon/comparison/html_comparator.rb +9 -4
- data/lib/canon/comparison/markup_comparator.rb +5 -1
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +5 -1
- data/lib/canon/comparison/xml_comparator.rb +5 -1
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +51 -10
- data/lib/canon/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2d050730d102cb224140f806e5b56634cdb98a16206301b0773d310dec582b9
|
|
4
|
+
data.tar.gz: 0b3bbf793abcfc9c3dd96a8c935260f4cf3360c7ee326e65ef8f165c48421f77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69b187c69b2aee23b1c1763504a33b226347ccbaaaab92d228d2a70da7d1f94f7160fb84b67356001de7de4e51155cc856104344fa795da8573ccf5e27af0633
|
|
7
|
+
data.tar.gz: 687fb81fbf5cafd49e8b1818e3dc641c44f62cf0f5d387829916ab270bdd4b9fffa56278e61d09f6bdabec52da1640ad59baf3c8d472d6248d1e0fce38209227
|
|
@@ -253,6 +253,19 @@ result = Canon::Comparison.equivalent?(xml1, xml2)
|
|
|
253
253
|
- Comment differences tracked but don't affect equivalence
|
|
254
254
|
- Default for HTML (comments are presentational)
|
|
255
255
|
|
|
256
|
+
==== Element Structure
|
|
257
|
+
|
|
258
|
+
`element_structure` is a derived dimension — produced by comparators when children
|
|
259
|
+
differ structurally (insertions, deletions, name changes). It is not a user-configurable
|
|
260
|
+
match dimension but follows the same normative rule as all general dimensions:
|
|
261
|
+
|
|
262
|
+
* **Default (no explicit behavior)** → Normative
|
|
263
|
+
- Structural differences (added/removed/renamed elements) affect equivalence
|
|
264
|
+
|
|
265
|
+
* **`:ignore` behavior** → Informative
|
|
266
|
+
- Structural differences tracked but don't affect equivalence
|
|
267
|
+
- Useful for content-only comparisons where wrapper elements don't matter
|
|
268
|
+
|
|
256
269
|
.Example: Comment handling
|
|
257
270
|
====
|
|
258
271
|
[source,ruby]
|
|
@@ -405,6 +418,8 @@ The classification system uses three main classes:
|
|
|
405
418
|
- `normative_dimension?(dimension)` - Is this dimension normative?
|
|
406
419
|
- `affects_equivalence?(dimension)` - Does this dimension affect equivalence?
|
|
407
420
|
- `supports_formatting_detection?(dimension)` - Can this dimension have formatting-only diffs?
|
|
421
|
+
- Normative rules: `structural_whitespace` requires `:strict` for normative status;
|
|
422
|
+
all other dimensions are normative unless behavior is `:ignore`
|
|
408
423
|
|
|
409
424
|
* **`DiffClassifier`** - Orchestrates classification using the above
|
|
410
425
|
- First checks `XmlSerializationFormatter` for serialization formatting
|
|
@@ -130,10 +130,33 @@ Each difference displays:
|
|
|
130
130
|
* **Status indicator**: `[NORMATIVE]` (green) or `[INFORMATIVE]` (yellow)
|
|
131
131
|
* **Dimension**: Which aspect differs (colorized in magenta)
|
|
132
132
|
* **Location**: XPath for XML/HTML, path for JSON/YAML (colorized in blue)
|
|
133
|
+
* **Reason**: When the reason contains visualized whitespace characters (░),
|
|
134
|
+
it is split across two lines with the before and after text vertically
|
|
135
|
+
aligned for easier visual comparison:
|
|
136
|
+
+
|
|
137
|
+
[source]
|
|
138
|
+
----
|
|
139
|
+
Reason: Text: "░░term2░definition░░"
|
|
140
|
+
vs.: "░term2░definition░"
|
|
141
|
+
----
|
|
142
|
+
+
|
|
143
|
+
When no visualized whitespace is present, the reason remains a single line.
|
|
133
144
|
* **Expected section**: What was in File 1 (red heading, bold)
|
|
134
145
|
* **Actual section**: What was in File 2 (green heading, bold)
|
|
135
146
|
* **Changes summary**: Actionable description of the difference (yellow, bold)
|
|
136
147
|
|
|
148
|
+
When both the Expected and Actual values are short (under 30 characters), they
|
|
149
|
+
are rendered as compact single lines with aligned colons for succinctness:
|
|
150
|
+
|
|
151
|
+
[source]
|
|
152
|
+
----
|
|
153
|
+
⊖ Expected (File 1): "term2 definition"
|
|
154
|
+
⊕ Actual (File 2) : "term2 definition"
|
|
155
|
+
----
|
|
156
|
+
|
|
157
|
+
For longer values, they appear on separate lines with no blank line between
|
|
158
|
+
them.
|
|
159
|
+
|
|
137
160
|
=== Dimension-specific formats
|
|
138
161
|
|
|
139
162
|
==== Attribute presence differences
|
|
@@ -226,6 +226,9 @@ Canon::Comparison.equivalent?(html1, html2, preprocessing: :rendered)
|
|
|
226
226
|
Pretty-prints before comparison:
|
|
227
227
|
- Consistent indentation
|
|
228
228
|
- One element per line
|
|
229
|
+
- Whitespace-only text nodes in strip-context elements (e.g. `<div>`) are
|
|
230
|
+
removed after formatting, so differences in structural whitespace between
|
|
231
|
+
elements do not produce false normative differences
|
|
229
232
|
- Good for visual diffs
|
|
230
233
|
|
|
231
234
|
[source,ruby]
|
|
@@ -51,12 +51,13 @@ module Canon
|
|
|
51
51
|
#
|
|
52
52
|
# This is used by DiffClassifier to determine the normative flag.
|
|
53
53
|
#
|
|
54
|
+
# Normative rules by dimension:
|
|
55
|
+
# - structural_whitespace: only :strict is normative (:normalize and :ignore are informative)
|
|
56
|
+
# - all other dimensions: normative unless behavior is :ignore
|
|
57
|
+
#
|
|
54
58
|
# @param dimension [Symbol] The match dimension to check
|
|
55
59
|
# @return [Boolean] true if normative, false if informative
|
|
56
60
|
def normative_dimension?(dimension)
|
|
57
|
-
# Element structure changes are ALWAYS normative
|
|
58
|
-
return true if dimension == :element_structure
|
|
59
|
-
|
|
60
61
|
# Structural whitespace with :normalize or :ignore behavior is INFORMATIVE
|
|
61
62
|
# Only :strict mode makes whitespace normative
|
|
62
63
|
if dimension == :structural_whitespace
|
|
@@ -64,7 +65,7 @@ module Canon
|
|
|
64
65
|
return behavior == :strict
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
# For other dimensions, if behavior affects equivalence
|
|
68
|
+
# For all other dimensions, normative if behavior affects equivalence
|
|
68
69
|
affects_equivalence?(dimension)
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -393,12 +393,17 @@ module Canon
|
|
|
393
393
|
end
|
|
394
394
|
end
|
|
395
395
|
|
|
396
|
-
# For
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
# For preprocessing modes that require whitespace filtering,
|
|
397
|
+
# apply the same post-parsing normalization used for string inputs.
|
|
398
|
+
# This is needed because dom_diff() pre-parses HTML5 strings into
|
|
399
|
+
# Nokogiri fragments before calling HtmlComparator, bypassing the
|
|
400
|
+
# string-input path where these filters are normally applied.
|
|
401
|
+
if %i[normalize format rendered].include?(preprocessing)
|
|
399
402
|
frag = node.is_a?(Nokogiri::XML::DocumentFragment) ? node : Nokogiri::XML.fragment(node.to_html)
|
|
400
403
|
normalize_html_style_script_comments(frag)
|
|
401
|
-
|
|
404
|
+
if preprocessing == :rendered
|
|
405
|
+
normalize_rendered_whitespace(frag, match_opts)
|
|
406
|
+
end
|
|
402
407
|
remove_whitespace_only_text_nodes(frag)
|
|
403
408
|
return frag
|
|
404
409
|
end
|
|
@@ -306,7 +306,11 @@ module Canon
|
|
|
306
306
|
end
|
|
307
307
|
|
|
308
308
|
# Default reason - can be overridden in subclasses
|
|
309
|
-
|
|
309
|
+
if diff1 == Canon::Comparison::MISSING_NODE && diff2 == Canon::Comparison::MISSING_NODE
|
|
310
|
+
"element structure mismatch (children differ)"
|
|
311
|
+
else
|
|
312
|
+
"#{diff1} vs #{diff2}"
|
|
313
|
+
end
|
|
310
314
|
end
|
|
311
315
|
|
|
312
316
|
# Build a clear reason message for attribute presence differences
|
|
@@ -85,7 +85,11 @@ module Canon
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Default reason
|
|
88
|
-
|
|
88
|
+
if diff1 == Canon::Comparison::MISSING_NODE && diff2 == Canon::Comparison::MISSING_NODE
|
|
89
|
+
"element structure mismatch (children differ)"
|
|
90
|
+
else
|
|
91
|
+
"#{diff1} vs #{diff2}"
|
|
92
|
+
end
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
# Enrich DiffNode with canonical path, serialized content, and attributes
|
|
@@ -631,7 +631,11 @@ differences)
|
|
|
631
631
|
return "Attribute order changed: [#{attrs1.join(', ')}] → [#{attrs2.join(', ')}]"
|
|
632
632
|
end
|
|
633
633
|
|
|
634
|
-
|
|
634
|
+
if diff1 == Canon::Comparison::MISSING_NODE && diff2 == Canon::Comparison::MISSING_NODE
|
|
635
|
+
"element structure mismatch (children differ)"
|
|
636
|
+
else
|
|
637
|
+
"#{diff1} vs #{diff2}"
|
|
638
|
+
end
|
|
635
639
|
end
|
|
636
640
|
|
|
637
641
|
# Build a clear reason message for attribute value differences
|
|
@@ -14,6 +14,9 @@ module Canon
|
|
|
14
14
|
# Formats dimension-specific detail for individual differences
|
|
15
15
|
# Provides actionable, colorized output showing exactly what changed
|
|
16
16
|
module DiffDetailFormatter
|
|
17
|
+
ANSI_ESCAPE = /\e\[[0-9;]*m/
|
|
18
|
+
COMPACT_DETAIL_MAX = 30
|
|
19
|
+
|
|
17
20
|
class << self
|
|
18
21
|
# Format all differences as a semantic diff report
|
|
19
22
|
#
|
|
@@ -127,9 +130,7 @@ compact: false, expand_difference: false)
|
|
|
127
130
|
|
|
128
131
|
# show reason if available
|
|
129
132
|
if diff.respond_to?(:reason) && diff.reason
|
|
130
|
-
output
|
|
131
|
-
bold: true)} #{colorize(diff.reason,
|
|
132
|
-
:yellow, use_color)}"
|
|
133
|
+
format_reason_line(output, diff.reason, use_color)
|
|
133
134
|
end
|
|
134
135
|
output << ""
|
|
135
136
|
|
|
@@ -138,13 +139,7 @@ compact: false, expand_difference: false)
|
|
|
138
139
|
diff, use_color, compact: compact, expand_difference: expand_difference
|
|
139
140
|
)
|
|
140
141
|
|
|
141
|
-
output
|
|
142
|
-
bold: true)
|
|
143
|
-
output << " #{detail1}"
|
|
144
|
-
output << ""
|
|
145
|
-
output << colorize("⊕ Actual (File 2):", :green, use_color,
|
|
146
|
-
bold: true)
|
|
147
|
-
output << " #{detail2}"
|
|
142
|
+
format_expected_actual(output, detail1, detail2, use_color)
|
|
148
143
|
|
|
149
144
|
if changes && !changes.empty?
|
|
150
145
|
output << ""
|
|
@@ -182,6 +177,52 @@ compact: false, expand_difference: false)
|
|
|
182
177
|
colorize(error_msg, :red, use_color, bold: true)
|
|
183
178
|
end
|
|
184
179
|
|
|
180
|
+
# Format the Reason line. When the reason contains visualized
|
|
181
|
+
# spaces (░), split into two vertically-aligned lines so the
|
|
182
|
+
# before/after text can be compared visually.
|
|
183
|
+
def format_reason_line(output, reason_text, use_color)
|
|
184
|
+
if reason_text.include?("\u2591") &&
|
|
185
|
+
reason_text.match?(/\A(Text|whitespace): .*\bvs\b/)
|
|
186
|
+
parts = reason_text.split(" vs ", 2)
|
|
187
|
+
if parts.length == 2
|
|
188
|
+
output << "#{colorize('Reason:', :cyan, use_color,
|
|
189
|
+
bold: true)} #{colorize(parts[0],
|
|
190
|
+
:yellow, use_color)}"
|
|
191
|
+
output << "#{' ' * 10}#{colorize("vs.: #{parts[1]}",
|
|
192
|
+
:yellow, use_color)}"
|
|
193
|
+
return
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
output << "#{colorize('Reason:', :cyan, use_color,
|
|
197
|
+
bold: true)} #{colorize(reason_text,
|
|
198
|
+
:yellow, use_color)}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Format the Expected/Actual block. Short values (both under 30
|
|
202
|
+
# chars) are rendered as compact single lines with aligned colons;
|
|
203
|
+
# longer values use the multi-line layout without a blank line gap.
|
|
204
|
+
def format_expected_actual(output, detail1, detail2, use_color)
|
|
205
|
+
plain1 = detail1.gsub(ANSI_ESCAPE, "")
|
|
206
|
+
plain2 = detail2.gsub(ANSI_ESCAPE, "")
|
|
207
|
+
|
|
208
|
+
if plain1.length < COMPACT_DETAIL_MAX &&
|
|
209
|
+
plain2.length < COMPACT_DETAIL_MAX
|
|
210
|
+
lbl1 = colorize("\u2296 Expected (File 1)", :red, use_color,
|
|
211
|
+
bold: true)
|
|
212
|
+
lbl2 = colorize("\u2295 Actual (File 2) ", :green, use_color,
|
|
213
|
+
bold: true)
|
|
214
|
+
output << "#{lbl1}: #{detail1}"
|
|
215
|
+
output << "#{lbl2}: #{detail2}"
|
|
216
|
+
else
|
|
217
|
+
output << colorize("\u2296 Expected (File 1):", :red, use_color,
|
|
218
|
+
bold: true)
|
|
219
|
+
output << " #{detail1}"
|
|
220
|
+
output << colorize("\u2295 Actual (File 2):", :green, use_color,
|
|
221
|
+
bold: true)
|
|
222
|
+
output << " #{detail2}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
185
226
|
# Helper: Colorize text
|
|
186
227
|
def colorize(text, color, use_color, bold: false)
|
|
187
228
|
DiffDetailFormatterHelpers::ColorHelper.colorize(text, color,
|
data/lib/canon/version.rb
CHANGED
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.2.
|
|
4
|
+
version: 0.2.2
|
|
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-04-
|
|
11
|
+
date: 2026-04-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|