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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10234a14be49f993f58acac6d82bb6e8ca409d309bcd8446296f32f60ad6380f
4
- data.tar.gz: 80e29ddf981d17f0fcd5812921f6ec1fbdf056b3ae673dc4d3d94c9979cddc21
3
+ metadata.gz: f2d050730d102cb224140f806e5b56634cdb98a16206301b0773d310dec582b9
4
+ data.tar.gz: 0b3bbf793abcfc9c3dd96a8c935260f4cf3360c7ee326e65ef8f165c48421f77
5
5
  SHA512:
6
- metadata.gz: bc63b5a8dae56a06781d8588809e8356eea618fdcdb13b933ea4c8a3c4f221201c05070a3ce100945c9f81ef6f122b73ff6ac723441aa2b3cbcc5fd31e8fee63
7
- data.tar.gz: fbcb9ceb782f7847e6f2c6afba275f7d68da410d253b6f34f263b5c91803a3fbaff031de9b2dda5b8787fc37d0c82e1cea97d3ae1489d507eaad969168a107a1
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, it's normative
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 :rendered preprocessing with Nokogiri nodes
397
- if preprocessing == :rendered
398
- # Normalize and return
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
- normalize_rendered_whitespace(frag, match_opts)
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
- "#{diff1} vs #{diff2}"
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
- "#{diff1} vs #{diff2}"
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
- "#{diff1} vs #{diff2}"
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 << "#{colorize('Reason:', :cyan, use_color,
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 << colorize("⊖ Expected (File 1):", :red, use_color,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
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.0
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-12 00:00:00.000000000 Z
11
+ date: 2026-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs