canon 0.1.10 → 0.1.12

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: 4f9d0e9c0c1bc9f213d837f480d3d9a26ce11505691ff48b63907e7a4abd530e
4
- data.tar.gz: aa591a7682cede5f23a8dcb8b8eb8f7616d849bc5f9cad1aa2038463ee9c52b0
3
+ metadata.gz: 0c7f014834dac890a353a1e85abe80dc834e81f20b853cfa00f51a06c423a699
4
+ data.tar.gz: 3b849925479fcec2777b22619bee9ab3d710fd33ce29b10bf2873c90d3095192
5
5
  SHA512:
6
- metadata.gz: 6c0af5461fff1d1cd1347ba57681bc671cda71d55d62efd328ac9424ef10b8329ec877ccf43f9ff78e83a54ca03df1026e160b259396caac7bd2704227ef01b1
7
- data.tar.gz: 8803713442225ae16c0c6c9c03c9cff55dd27dc6b96f5254ee5f814a29b7ad7b5ef6eafd0cd6a58d17f070a2609154476215147d595a01a69586ca7de8608a7f
6
+ metadata.gz: 7def7dca16c59b0e3f3952b12f814ad93b38cb108654903471320e0e3c7d55418af60068a114eb9d3fcd4e260bdc806c5cd65b9471b06a85b5c5a223f6373395
7
+ data.tar.gz: b36133cf1c3c0597e12b880057ebdc5af80689ab59bb8983ff631208d31db994151858521fb8897c88c00d1651e0ac25ad376584558a3b5b514cfd409c2f20be
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-01-21 01:26:28 UTC using RuboCop version 1.81.7.
3
+ # on 2026-01-21 09:17:44 UTC using RuboCop version 1.81.7.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -12,52 +12,13 @@ Gemspec/RequiredRubyVersion:
12
12
  Exclude:
13
13
  - 'canon.gemspec'
14
14
 
15
- # Offense count: 16
16
- # This cop supports safe autocorrection (--autocorrect).
17
- # Configuration parameters: EnforcedStyle, IndentationWidth.
18
- # SupportedStyles: with_first_argument, with_fixed_indentation
19
- Layout/ArgumentAlignment:
20
- Exclude:
21
- - 'lib/canon/comparison/xml_comparator.rb'
22
- - 'lib/canon/diff/xml_serialization_formatter.rb'
23
- - 'spec/canon/diff/xml_serialization_formatter_spec.rb'
24
-
25
- # Offense count: 1
26
- # This cop supports safe autocorrection (--autocorrect).
27
- # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
28
- # SupportedHashRocketStyles: key, separator, table
29
- # SupportedColonStyles: key, separator, table
30
- # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
31
- Layout/HashAlignment:
32
- Exclude:
33
- - 'test_verify_equivalent.rb'
34
-
35
- # Offense count: 709
15
+ # Offense count: 700
36
16
  # This cop supports safe autocorrection (--autocorrect).
37
17
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
38
18
  # URISchemes: http, https
39
19
  Layout/LineLength:
40
20
  Enabled: false
41
21
 
42
- # Offense count: 4
43
- # This cop supports safe autocorrection (--autocorrect).
44
- # Configuration parameters: EnforcedStyle, IndentationWidth.
45
- # SupportedStyles: aligned, indented
46
- Layout/MultilineOperationIndentation:
47
- Exclude:
48
- - 'lib/canon/diff/diff_classifier.rb'
49
- - 'lib/canon/diff/xml_serialization_formatter.rb'
50
-
51
- # Offense count: 17
52
- # This cop supports safe autocorrection (--autocorrect).
53
- # Configuration parameters: AllowInHeredoc.
54
- Layout/TrailingWhitespace:
55
- Exclude:
56
- - 'lib/canon/comparison/xml_comparator.rb'
57
- - 'lib/canon/diff/xml_serialization_formatter.rb'
58
- - 'spec/canon/diff/xml_serialization_formatter_spec.rb'
59
- - 'test_verify_equivalent.rb'
60
-
61
22
  # Offense count: 48
62
23
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
63
24
  Lint/DuplicateBranch:
@@ -98,7 +59,7 @@ Lint/UnusedMethodArgument:
98
59
  - 'lib/canon/diff_formatter/by_line/xml_formatter.rb'
99
60
  - 'lib/canon/diff_formatter/by_object/base_formatter.rb'
100
61
 
101
- # Offense count: 207
62
+ # Offense count: 209
102
63
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
103
64
  Metrics/AbcSize:
104
65
  Enabled: false
@@ -109,22 +70,22 @@ Metrics/AbcSize:
109
70
  Metrics/BlockLength:
110
71
  Max: 84
111
72
 
112
- # Offense count: 176
73
+ # Offense count: 177
113
74
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
114
75
  Metrics/CyclomaticComplexity:
115
76
  Enabled: false
116
77
 
117
- # Offense count: 360
78
+ # Offense count: 363
118
79
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
119
80
  Metrics/MethodLength:
120
81
  Max: 110
121
82
 
122
- # Offense count: 45
83
+ # Offense count: 44
123
84
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
124
85
  Metrics/ParameterLists:
125
86
  Max: 9
126
87
 
127
- # Offense count: 142
88
+ # Offense count: 143
128
89
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
129
90
  Metrics/PerceivedComplexity:
130
91
  Enabled: false
@@ -165,7 +126,7 @@ Performance/CollectionLiteralInLoop:
165
126
  - 'lib/canon/comparison/html_comparator.rb'
166
127
  - 'lib/canon/xml/xml_base_handler.rb'
167
128
 
168
- # Offense count: 64
129
+ # Offense count: 68
169
130
  # Configuration parameters: Prefixes, AllowedPatterns.
170
131
  # Prefixes: when, with, without
171
132
  RSpec/ContextWording:
@@ -182,7 +143,13 @@ RSpec/DescribeMethod:
182
143
  - 'spec/canon/comparison/multiple_differences_spec.rb'
183
144
  - 'spec/canon/diff_formatter/character_map_customization_spec.rb'
184
145
 
185
- # Offense count: 675
146
+ # Offense count: 1
147
+ # This cop supports safe autocorrection (--autocorrect).
148
+ RSpec/EmptyHook:
149
+ Exclude:
150
+ - 'spec/canon/color_detector_spec.rb'
151
+
152
+ # Offense count: 679
186
153
  # Configuration parameters: CountAsOne.
187
154
  RSpec/ExampleLength:
188
155
  Max: 67
@@ -213,10 +180,11 @@ RSpec/IndexedLet:
213
180
  - 'spec/canon/tree_diff/matchers/universal_matcher_spec.rb'
214
181
  - 'spec/canon/tree_diff/operations/operation_detector_spec.rb'
215
182
 
216
- # Offense count: 4
183
+ # Offense count: 5
217
184
  # Configuration parameters: AssignmentOnly.
218
185
  RSpec/InstanceVariable:
219
186
  Exclude:
187
+ - 'spec/canon/color_detector_spec.rb'
220
188
  - 'spec/canon/rspec_matchers_spec.rb'
221
189
 
222
190
  # Offense count: 15
@@ -233,7 +201,7 @@ RSpec/MultipleDescribes:
233
201
  Exclude:
234
202
  - 'spec/canon/comparison/match_options_spec.rb'
235
203
 
236
- # Offense count: 518
204
+ # Offense count: 522
237
205
  RSpec/MultipleExpectations:
238
206
  Max: 15
239
207
 
@@ -251,7 +219,7 @@ RSpec/NamedSubject:
251
219
  - 'spec/canon/pretty_printer/json_spec.rb'
252
220
  - 'spec/canon/pretty_printer/xml_spec.rb'
253
221
 
254
- # Offense count: 37
222
+ # Offense count: 40
255
223
  # Configuration parameters: AllowedGroups.
256
224
  RSpec/NestedGroups:
257
225
  Max: 4
@@ -311,35 +279,9 @@ Style/IdenticalConditionalBranches:
311
279
  - 'lib/canon/diff_formatter/by_object/base_formatter.rb'
312
280
  - 'lib/canon/diff_formatter/legend.rb'
313
281
 
314
- # Offense count: 2
315
- # This cop supports unsafe autocorrection (--autocorrect-all).
316
- # Configuration parameters: InverseMethods, InverseBlocks.
317
- Style/InverseMethods:
318
- Exclude:
319
- - 'lib/canon/comparison/markup_comparator.rb'
320
- - 'lib/canon/comparison/xml_comparator/diff_node_builder.rb'
321
-
322
282
  # Offense count: 1
323
283
  # Configuration parameters: AllowedMethods.
324
284
  # AllowedMethods: respond_to_missing?
325
285
  Style/OptionalBooleanParameter:
326
286
  Exclude:
327
287
  - 'lib/canon/diff_formatter/debug_output.rb'
328
-
329
- # Offense count: 3
330
- # This cop supports safe autocorrection (--autocorrect).
331
- # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
332
- # SupportedStyles: single_quotes, double_quotes
333
- Style/StringLiterals:
334
- Exclude:
335
- - 'lib/canon/comparison/markup_comparator.rb'
336
- - 'lib/canon/comparison/xml_comparator/diff_node_builder.rb'
337
- - 'test_verify_equivalent.rb'
338
-
339
- # Offense count: 12
340
- # This cop supports safe autocorrection (--autocorrect).
341
- # Configuration parameters: EnforcedStyleForMultiline.
342
- # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
343
- Style/TrailingCommaInArguments:
344
- Exclude:
345
- - 'spec/canon/diff/xml_serialization_formatter_spec.rb'
data/README.adoc CHANGED
@@ -224,68 +224,262 @@ expect(actual).to be_xml_equivalent_to(expected)
224
224
  .show_diffs(:normative)
225
225
  ----
226
226
 
227
- === Original input string display
227
+ === Input display options
228
228
 
229
- When debugging test failures, it's often helpful to see the exact strings that
230
- were passed to the comparison before any preprocessing or normalization. The
231
- `verbose_diff` option displays the original input strings in an RSpec-style
232
- format with line numbers.
229
+ When debugging test failures, it's often helpful to see the exact inputs that were
230
+ passed to the comparison. Canon provides four input display options:
233
231
 
232
+ `show_raw_inputs`:: Show raw/original file contents (before preprocessing)
233
+
234
+ `show_preprocessed_inputs`:: Show what was actually compared (after preprocessing)
235
+
236
+ `show_line_numbered_inputs`:: Show raw inputs with line numbers (for reference)
237
+
238
+ `verbose_diff`:: Convenience flag that enables all three options above
239
+
240
+ You can enable any combination of them independently.
241
+
242
+ ==== Show raw inputs
243
+
244
+ Display the raw file contents before any preprocessing. This shows exactly what's
245
+ in your files, useful for copying to specs.
246
+
247
+ **Ruby API:**
234
248
  [source,ruby]
235
249
  ----
236
- # Enable original string display in configuration
250
+ # Enable raw input display in configuration
237
251
  Canon::Config.configure do |config|
238
- config.xml.diff.verbose_diff = true
252
+ config.xml.diff.show_raw_inputs = true
239
253
  end
240
254
 
241
255
  # Or programmatically for a specific comparison
242
256
  result = Canon::Comparison.equivalent?(xml1, xml2,
243
257
  verbose: true,
244
- verbose_diff: true
258
+ show_raw_inputs: true
245
259
  )
246
260
  ----
247
261
 
262
+ **CLI usage:**
263
+ [source,bash]
264
+ ----
265
+ # Show raw file contents (for copying to specs)
266
+ $ canon diff file1.xml file2.xml --show-raw-inputs
267
+
268
+ # Show both raw and preprocessed (full trace)
269
+ $ canon diff file1.xml file2.xml --show-raw-inputs --show-preprocessed-inputs
270
+ ----
271
+
272
+ **Environment variables:**
273
+ [source,bash]
274
+ ----
275
+ # Global (all formats)
276
+ export CANON_SHOW_RAW_INPUTS=true
277
+
278
+ # Format-specific
279
+ export CANON_XML_SHOW_RAW_INPUTS=true
280
+ export CANON_HTML_SHOW_RAW_INPUTS=true
281
+ export CANON_JSON_SHOW_RAW_INPUTS=true
282
+ export CANON_YAML_SHOW_RAW_INPUTS=true
283
+ ----
284
+
248
285
  **Output format:**
249
286
  ----
250
- ==================================================================
251
- ORIGINAL INPUT STRINGS
252
- ==================================================================
287
+ === ORIGINAL INPUTS (Raw) ===
253
288
 
254
- Expected (as string):
255
- 1 | <root>
256
- 2 | <element>value1</element>
257
- 3 | </root>
289
+ EXPECTED:
290
+ ----------------------------------------------------------------------
291
+ <root> hello
292
+ world</root>
293
+
294
+ RECEIVED:
295
+ ----------------------------------------------------------------------
296
+ <root>hello world</root>
297
+
298
+ ----
299
+
300
+ ==== Show preprocessed inputs
301
+
302
+ Display the content after preprocessing (c14n, normalize, format, etc.).
303
+ This shows what the comparison actually compared, useful for understanding
304
+ how preprocessing affects your content.
305
+
306
+ **Ruby API:**
307
+ [source,ruby]
308
+ ----
309
+ # Enable preprocessed input display in configuration
310
+ Canon::Config.configure do |config|
311
+ config.xml.diff.show_preprocessed_inputs = true
312
+ end
313
+
314
+ # Or programmatically for a specific comparison
315
+ result = Canon::Comparison.equivalent?(xml1, xml2,
316
+ verbose: true,
317
+ show_preprocessed_inputs: true
318
+ )
319
+ ----
320
+
321
+ **CLI usage:**
322
+ [source,bash]
323
+ ----
324
+ # Show what was actually compared
325
+ $ canon diff file1.xml file2.xml --show-preprocessed-inputs
326
+
327
+ # Preprocess with normalization and show what was compared
328
+ $ canon diff file1.xml file2.xml --preprocessing normalize --show-preprocessed-inputs
329
+ ----
330
+
331
+ **Environment variables:**
332
+ [source,bash]
333
+ ----
334
+ # Global (all formats)
335
+ export CANON_SHOW_PROCESSED_INPUTS=true
336
+
337
+ # Format-specific
338
+ export CANON_XML_SHOW_PROCESSED_INPUTS=true
339
+ export CANON_HTML_SHOW_PROCESSED_INPUTS=true
340
+ export CANON_JSON_SHOW_PROCESSED_INPUTS=true
341
+ export CANON_YAML_SHOW_PROCESSED_INPUTS=true
342
+ ----
343
+
344
+ **Output format:**
345
+ ----
346
+ === PREPROCESSED INPUTS (Compared) ===
347
+ Preprocessing: normalize
348
+
349
+ EXPECTED:
350
+ ----------------------------------------------------------------------
351
+ <root> hello world </root>
352
+
353
+ RECEIVED:
354
+ ----------------------------------------------------------------------
355
+ <root>hello world</root>
258
356
 
259
- Actual (as string):
357
+ ----
358
+
359
+ ==== Show line-numbered inputs
360
+
361
+ Display raw inputs with line numbers (RSpec-style format). Useful for
362
+ pinpointing specific lines when debugging.
363
+
364
+ **Ruby API:**
365
+ [source,ruby]
366
+ ----
367
+ # Enable line-numbered input display in configuration
368
+ Canon::Config.configure do |config|
369
+ config.xml.diff.show_line_numbered_inputs = true
370
+ end
371
+
372
+ # Or programmatically for a specific comparison
373
+ result = Canon::Comparison.equivalent?(xml1, xml2,
374
+ verbose: true,
375
+ show_line_numbered_inputs: true
376
+ )
377
+ ----
378
+
379
+ **CLI usage:**
380
+ [source,bash]
381
+ ----
382
+ # Show raw inputs with line numbers (RSpec-style)
383
+ $ canon diff file1.xml file2.xml --show-line-numbered-inputs
384
+ ----
385
+
386
+ **Environment variables:**
387
+ [source,bash]
388
+ ----
389
+ # Global (all formats)
390
+ export CANON_SHOW_LINE_NUMBERED_INPUTS=true
391
+
392
+ # Format-specific
393
+ export CANON_XML_SHOW_LINE_NUMBERED_INPUTS=true
394
+ export CANON_HTML_SHOW_LINE_NUMBERED_INPUTS=true
395
+ export CANON_JSON_SHOW_LINE_NUMBERED_INPUTS=true
396
+ export CANON_YAML_SHOW_LINE_NUMBERED_INPUTS=true
397
+ ----
398
+
399
+ **Output format:**
400
+ ----
401
+ ======================================================================
402
+ ORIGINAL INPUTS (with line numbers)
403
+ ======================================================================
404
+
405
+ Expected:
260
406
  1 | <root>
261
- 2 | <element>value2</element>
407
+ 2 | hello
262
408
  3 | </root>
263
409
 
264
- ==================================================================
410
+ Received:
411
+ 1 | <root>hello world</root>
412
+
413
+ ======================================================================
265
414
  ----
266
415
 
267
- **When to use this feature:**
416
+ ==== Verbose diff (all input displays)
268
417
 
269
- * Debugging why two documents are considered different
270
- * Understanding preprocessing effects (c14n, normalization, etc.)
271
- * Verifying the exact input received by the comparison
272
- * Comparing raw vs processed content
418
+ The `verbose_diff` option is a convenience flag that enables all three input
419
+ display options at once. This is useful for maximum debugging output.
420
+
421
+ **Ruby API:**
422
+ [source,ruby]
423
+ ----
424
+ # Enable all input displays in configuration
425
+ Canon::Config.configure do |config|
426
+ config.xml.diff.verbose_diff = true
427
+ end
273
428
 
274
- **Environment variable:**
429
+ # Or programmatically for a specific comparison
430
+ result = Canon::Comparison.equivalent?(xml1, xml2,
431
+ verbose: true,
432
+ verbose_diff: true
433
+ )
434
+ ----
435
+
436
+ **CLI usage:**
437
+ [source,bash]
438
+ ----
439
+ # Show all three input displays
440
+ $ canon diff file1.xml file2.xml --verbose
441
+ ----
442
+
443
+ **Environment variables:**
275
444
  [source,bash]
276
445
  ----
446
+ # Format-specific
277
447
  export CANON_XML_DIFF_VERBOSE_DIFF=true
278
448
  export CANON_HTML_DIFF_VERBOSE_DIFF=true
279
449
  export CANON_JSON_DIFF_VERBOSE_DIFF=true
280
450
  export CANON_YAML_DIFF_VERBOSE_DIFF=true
281
451
  ----
282
452
 
453
+ ==== When to use each option
454
+
455
+ `show_raw_inputs`::
456
+ ** You want to copy the expected output to paste into your test specs
457
+ ** You need to see the exact formatting/structure of your input files
458
+ ** You're debugging file reading/parsing issues
459
+
460
+ `show_preprocessed_inputs`::
461
+ ** You want to understand what the comparison actually compared
462
+ ** You're debugging why two documents are considered equivalent (or not)
463
+ ** You need to trace the effect of preprocessing (c14n, normalize, format)
464
+
465
+ `show_line_numbered_inputs`::
466
+ ** You need line numbers for pinpointing specific lines
467
+ ** You prefer the RSpec-style format
468
+ ** You're collaborating on debugging with others
469
+
470
+ `verbose_diff`::
471
+ ** You want maximum debugging output (all three displays at once)
472
+ ** You're doing initial investigation of a complex comparison issue
473
+
283
474
  === Algorithm choice
284
475
 
285
476
  Canon provides two diff algorithms:
286
477
 
287
- * **DOM diff** (default): Stable, position-based comparison for traditional line-by-line output
288
- * **Semantic tree diff** (experimental): Advanced operation detection (INSERT, DELETE, UPDATE, MOVE, MERGE, SPLIT, UPGRADE, DOWNGRADE)
478
+ DOM diff (default):: Stable, position-based comparison for traditional
479
+ line-by-line output
480
+
481
+ Semantic tree diff (experimental):: Advanced operation detection (INSERT,
482
+ DELETE, UPDATE, MOVE, MERGE, SPLIT, UPGRADE, DOWNGRADE)
289
483
 
290
484
  [source,ruby]
291
485
  ----
@@ -457,59 +457,110 @@ The whitespace difference is informative because `structural_whitespace: :ignore
457
457
 
458
458
  == Color Control
459
459
 
460
+ === Automatic Color Detection
461
+
462
+ Canon automatically detects whether the terminal supports color output:
463
+
464
+ * **TTY detection**: Colors are only enabled when output is to a terminal (not when piped to a file)
465
+ * **NO_COLOR support**: If the `NO_COLOR` environment variable is set (per https://no-color.org/), colors are disabled
466
+ * **Terminal capability detection**: Checks environment variables like `TERM` and `COLORTERM`
467
+ * **CI environment awareness**: Detects and adapts to CI systems (GitHub Actions, Travis CI, etc.)
468
+
469
+ This means colors work automatically in most scenarios without manual configuration.
470
+
460
471
  === Enabling/Disabling Colors
461
472
 
462
- Colors are controlled by the `use_color` option:
473
+ Colors can be explicitly controlled using the `use_color` option:
474
+
475
+ .Using automatic detection (default)
476
+ [example]
477
+ ====
478
+ [source,bash]
479
+ ----
480
+ # Auto-detects color support (default)
481
+ canon diff file1.xml file2.xml
482
+ ----
483
+ ====
463
484
 
464
- .CLI
485
+ .Explicit control
465
486
  [example]
466
487
  ====
467
488
  [source,bash]
468
489
  ----
469
- # Enable colors (default)
490
+ # Force enable colors
470
491
  canon diff file1.xml file2.xml --color
471
492
 
472
- # Disable colors
493
+ # Force disable colors
473
494
  canon diff file1.xml file2.xml --no-color
474
495
  ----
475
496
  ====
476
497
 
477
498
  .Ruby API
478
- [example]
479
- ====
480
499
  [source,ruby]
481
500
  ----
482
- # Enable colors
483
- Canon.compare(file1, file2, format: :xml, color: true)
501
+ # Auto-detect (default)
502
+ Canon.compare(file1, file2, format: :xml)
484
503
 
485
- # Disable colors
486
- Canon.compare(file1, file2, format: :xml, color: false)
504
+ # Explicit control
505
+ Canon.compare(file1, file2, format: :xml, use_color: false)
487
506
  ----
488
507
  ====
489
508
 
490
- .RSpec
491
- [example]
492
- ====
493
- [source,ruby]
509
+ === NO_COLOR Environment Variable
510
+
511
+ Per the https://no-color.org/[NO_COLOR standard], setting the `NO_COLOR`
512
+ environment variable will disable colors:
513
+
514
+ [source,bash]
494
515
  ----
495
- RSpec.configure do |config|
496
- # Enable for local, disable for CI
497
- config.canon.xml.diff.use_color = !ENV['CI']
498
- end
516
+ # Disable colors (respected by Canon and other NO_COLOR-aware tools)
517
+ export NO_COLOR=1
518
+
519
+ # Run canon - colors will be disabled
520
+ canon diff file1.xml file2.xml
499
521
  ----
500
- ====
501
522
 
502
- === Environment Variable
523
+ The `NO_COLOR` variable always takes precedence over other color settings.
524
+
525
+ === Environment Variables
526
+
527
+ In addition to `NO_COLOR`, Canon supports these environment variables:
503
528
 
504
529
  [source,bash]
505
530
  ----
506
- # Disable colors globally
531
+ # Disable colors globally (CANON_USE_COLOR)
507
532
  export CANON_USE_COLOR=false
508
533
 
509
- # Enable colors globally
534
+ # Enable colors globally (CANON_USE_COLOR)
510
535
  export CANON_USE_COLOR=true
536
+
537
+ # Format-specific color control
538
+ export CANON_XML_DIFF_USE_COLOR=false
539
+ export CANON_HTML_DIFF_USE_COLOR=false
540
+ export CANON_JSON_DIFF_USE_COLOR=false
541
+ export CANON_YAML_DIFF_USE_COLOR=false
511
542
  ----
512
543
 
544
+ === Terminal Detection
545
+
546
+ Canon detects terminal capabilities through these environment variables:
547
+
548
+ `COLORTERM`::
549
+ Set to `24bit`, `truecolor`, or `true` indicates True Color support
550
+
551
+ `TERM`:: Terminal type:
552
+
553
+ 256-color support::: `xterm-256color`, `screen-256color`
554
+ Direct color support::: `xterm-direct`
555
+ No color support::: `dumb`, `emacs`
556
+ Assume basic ANSI color support::: Most other values
557
+
558
+ `CI`:: CI environment detection
559
+ (plus CI-specific variables)
560
+
561
+ If no terminal information is available, Canon assumes color support for modern
562
+ terminals.
563
+
513
564
 
514
565
  == Diff Type Display Options
515
566
 
data/lib/canon/cli.rb CHANGED
@@ -126,6 +126,24 @@ module Canon
126
126
 
127
127
  # Disable color output
128
128
  $ canon diff file1.xml file2.xml --no-color
129
+
130
+ # Show raw file contents (for copying to specs)
131
+ $ canon diff file1.xml file2.xml --show-raw-inputs
132
+
133
+ # Show preprocessed contents (what was actually compared)
134
+ $ canon diff file1.xml file2.xml --show-preprocessed-inputs
135
+
136
+ # Show both raw and preprocessed (full trace)
137
+ $ canon diff file1.xml file2.xml --show-raw-inputs --show-preprocessed-inputs
138
+
139
+ # Preprocess with normalization and show what was compared
140
+ $ canon diff file1.xml file2.xml --preprocessing normalize --show-preprocessed-inputs
141
+
142
+ # Show raw inputs with line numbers (RSpec-style)
143
+ $ canon diff file1.xml file2.xml --show-line-numbered-inputs
144
+
145
+ # Verbose mode (shows all three input displays)
146
+ $ canon diff file1.xml file2.xml --verbose
129
147
  DESC
130
148
  method_option :format,
131
149
  aliases: "-f",
@@ -142,8 +160,7 @@ module Canon
142
160
  desc: "Format type for second file"
143
161
  method_option :color,
144
162
  type: :boolean,
145
- default: true,
146
- desc: "Colorize diff output"
163
+ desc: "Colorize diff output (auto-detected by default)"
147
164
  method_option :verbose,
148
165
  aliases: "-v",
149
166
  type: :boolean,
@@ -213,6 +230,18 @@ module Canon
213
230
  method_option :diff_grouping_lines,
214
231
  type: :numeric,
215
232
  desc: "Group diffs within N lines into context blocks (default: no grouping)"
233
+ method_option :show_raw_inputs,
234
+ type: :boolean,
235
+ default: false,
236
+ desc: "Show raw/original file contents before diff"
237
+ method_option :show_preprocessed_inputs,
238
+ type: :boolean,
239
+ default: false,
240
+ desc: "Show preprocessed contents (what was actually compared)"
241
+ method_option :show_line_numbered_inputs,
242
+ type: :boolean,
243
+ default: false,
244
+ desc: "Show raw inputs with line numbers (RSpec-style)"
216
245
  def diff(file1, file2)
217
246
  Commands::DiffCommand.new(options).run(file1, file2)
218
247
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ # Detects whether the current terminal supports color output.
5
+ #
6
+ # This class provides cross-platform detection of terminal color capabilities
7
+ # by checking environment variables and, on Unix-like systems, optionally
8
+ # querying the terminfo database.
9
+ #
10
+ # == Detection Logic
11
+ #
12
+ # The detection follows this priority order:
13
+ #
14
+ # 1. **NO_COLOR**: If set (regardless of value), colors are disabled
15
+ # (per https://no-color.org/)
16
+ # 2. **Explicit user choice**: If explicitly set, honor that choice
17
+ # 3. **Terminal capability detection**:
18
+ # - COLORTERM=24bit or truecolor → True color support
19
+ # - TERM ending with 256 or 256color → 256-color support
20
+ # - TERM=dumb, TERM containing "emacs" → No color support
21
+ # - CI environment → Check specific CI variables
22
+ # - TTY detection → Only enable colors if output is to a TTY
23
+ #
24
+ # == Usage
25
+ #
26
+ # # Auto-detect
27
+ # ColorDetector.supports_color? # => true or false
28
+ #
29
+ # # Explicit choice (bypass detection)
30
+ # ColorDetector.supports_color?(explicit: true) # => true
31
+ # ColorDetector.supports_color?(explicit: false) # => false
32
+ #
33
+ # # With output stream check
34
+ # ColorDetector.supports_color?(output: $stdout)
35
+ #
36
+ class ColorDetector
37
+ # Environment variables that indicate color support
38
+ COLOR_TERM_VALUES = %w[24bit truecolor true].freeze
39
+ COLOR_TERM_SUFFIXES = %w[256 256color direct].freeze
40
+ NO_COLOR_TERMS = %w[dumb emacs].freeze
41
+ CI_ENV_VARS = %w[CI GITHUB_ACTIONS TRAVIS GITLAB_CI JENKINS_HOME].freeze
42
+
43
+ class << self
44
+ # Detect whether the current environment supports color output.
45
+ #
46
+ # @param explicit [Boolean, nil] Explicit user choice to bypass detection
47
+ # @param output [IO, nil] Output stream to check (default: $stdout)
48
+ # @return [Boolean] true if colors are supported, false otherwise
49
+ def supports_color?(explicit: nil, output: $stdout)
50
+ # 1. NO_COLOR always wins (per https://no-color.org/)
51
+ return false if ENV.key?("NO_COLOR")
52
+
53
+ # 2. Explicit user choice bypasses detection
54
+ return explicit unless explicit.nil?
55
+
56
+ # 3. Check if output is a TTY (don't use colors for piped/file output)
57
+ return false unless tty?(output)
58
+
59
+ # 4. Check terminal capability indicators
60
+ detect_from_env
61
+ end
62
+
63
+ private
64
+
65
+ # Check if output stream is a TTY
66
+ #
67
+ # @param io [IO] Output stream
68
+ # @return [Boolean] true if the stream is a TTY
69
+ def tty?(io)
70
+ return false unless io.respond_to?(:tty?)
71
+ return false unless io.respond_to?(:isatty)
72
+
73
+ # Ruby 2.5+ uses tty?, older uses isatty
74
+ io.tty? || io.isatty
75
+ rescue ArgumentError, IOError
76
+ # Stream might be closed or invalid
77
+ false
78
+ end
79
+
80
+ # Detect color support from environment variables
81
+ #
82
+ # @return [Boolean] true if colors appear to be supported
83
+ def detect_from_env
84
+ # Check for known color-capable terminals
85
+ colorterm = ENV["COLORTERM"]
86
+ return true if COLOR_TERM_VALUES.include?(colorterm)
87
+
88
+ # Check TERM variable
89
+ term = ENV["TERM"]
90
+ if term
91
+ # Known no-color terminals
92
+ return false if NO_COLOR_TERMS.any? { |t| term.include?(t) }
93
+ # Known color-capable terminals
94
+ return true if COLOR_TERM_SUFFIXES.any? { |s| term.end_with?(s) }
95
+ # Most modern terminals support basic ANSI colors
96
+ return true unless term.empty? || term == "unknown"
97
+ end
98
+
99
+ # Check CI environments
100
+ # Some CI systems support colors, others don't
101
+ return detect_ci_colors if ci_environment?
102
+
103
+ # Default: assume colors are supported on modern terminals
104
+ # This is a safe default for most use cases
105
+ true
106
+ end
107
+
108
+ # Detect if we're in a CI environment
109
+ #
110
+ # @return [Boolean] true if in a CI environment
111
+ def ci_environment?
112
+ CI_ENV_VARS.any? { |var| ENV.key?(var) }
113
+ end
114
+
115
+ # Detect color support in CI environments
116
+ #
117
+ # Different CI systems have different color support:
118
+ # - GitHub Actions: supports colors (explicit CI env vars)
119
+ # - Travis CI: supports colors
120
+ # - GitLab CI: supports colors
121
+ # - Jenkins: supports colors
122
+ # - Generic CI: check for specific TeamCity/Terminal variables
123
+ #
124
+ # @return [Boolean] true if CI environment likely supports colors
125
+ def detect_ci_colors
126
+ # GitHub Actions explicitly supports colors
127
+ return true if ENV["GITHUB_ACTIONS"]
128
+
129
+ # TeamCity supports colors with specific env var
130
+ return true if ENV["TEAMCITY_VERSION"]
131
+
132
+ # Most modern CI systems support ANSI colors
133
+ # Only disable for explicitly known non-color CI
134
+ return false if ENV["TERM"] == "dumb"
135
+
136
+ # Default to supporting colors in CI
137
+ true
138
+ end
139
+ end
140
+ end
141
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../comparison"
4
4
  require_relative "../diff_formatter"
5
+ require_relative "../color_detector"
5
6
  require "json"
6
7
  require "yaml"
7
8
 
@@ -48,11 +49,14 @@ module Canon
48
49
 
49
50
  # Format and output results
50
51
  formatter = Canon::DiffFormatter.new(
51
- use_color: @options[:color],
52
+ use_color: resolve_color_option,
52
53
  mode: mode,
53
54
  context_lines: @options.fetch(:context_lines, 3),
54
55
  diff_grouping_lines: @options[:diff_grouping_lines],
55
56
  show_diffs: @options[:show_diffs]&.to_sym || :all,
57
+ show_raw_inputs: @options[:show_raw_inputs] || false,
58
+ show_preprocessed_inputs: @options[:show_preprocessed_inputs] || false,
59
+ show_line_numbered_inputs: @options[:show_line_numbered_inputs] || false,
56
60
  )
57
61
 
58
62
  # Show configuration in verbose mode using shared DebugOutput
@@ -61,7 +65,7 @@ module Canon
61
65
  config_output = Canon::DiffFormatter::DebugOutput.verbose_tables_only(
62
66
  result,
63
67
  {
64
- use_color: @options[:color],
68
+ use_color: resolve_color_option,
65
69
  mode: mode,
66
70
  context_lines: @options.fetch(:context_lines, 3),
67
71
  diff_grouping_lines: @options[:diff_grouping_lines],
@@ -275,6 +279,19 @@ module Canon
275
279
  5_242_880 # Default 5MB
276
280
  end
277
281
  end
282
+
283
+ # Resolve color option with auto-detection
284
+ #
285
+ # Returns the user's explicit choice if provided, otherwise
286
+ # auto-detects terminal color support.
287
+ #
288
+ # @return [Boolean] true if colors should be used
289
+ def resolve_color_option
290
+ return @options[:color] unless @options[:color].nil?
291
+
292
+ # Auto-detect when no explicit choice was made
293
+ ColorDetector.supports_color?
294
+ end
278
295
  end
279
296
  end
280
297
  end
@@ -14,6 +14,9 @@ module Canon
14
14
  show_diffs: :symbol,
15
15
  verbose_diff: :boolean,
16
16
  algorithm: :symbol,
17
+ show_raw_inputs: :boolean,
18
+ show_preprocessed_inputs: :boolean,
19
+ show_line_numbered_inputs: :boolean,
17
20
 
18
21
  # MatchConfig attributes
19
22
  profile: :symbol,
@@ -42,7 +45,8 @@ module Canon
42
45
 
43
46
  def all_diff_attributes
44
47
  %i[mode use_color context_lines grouping_lines show_diffs
45
- verbose_diff algorithm max_file_size max_node_count max_diff_lines]
48
+ verbose_diff algorithm show_raw_inputs show_preprocessed_inputs
49
+ show_line_numbered_inputs max_file_size max_node_count max_diff_lines]
46
50
  end
47
51
 
48
52
  def all_match_attributes
data/lib/canon/config.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "config/env_provider"
4
4
  require_relative "config/override_resolver"
5
+ require_relative "color_detector"
5
6
 
6
7
  module Canon
7
8
  # Global configuration for Canon
@@ -221,6 +222,30 @@ module Canon
221
222
  @resolver.set_programmatic(:verbose_diff, value)
222
223
  end
223
224
 
225
+ def show_raw_inputs
226
+ @resolver.resolve(:show_raw_inputs)
227
+ end
228
+
229
+ def show_raw_inputs=(value)
230
+ @resolver.set_programmatic(:show_raw_inputs, value)
231
+ end
232
+
233
+ def show_preprocessed_inputs
234
+ @resolver.resolve(:show_preprocessed_inputs)
235
+ end
236
+
237
+ def show_preprocessed_inputs=(value)
238
+ @resolver.set_programmatic(:show_preprocessed_inputs, value)
239
+ end
240
+
241
+ def show_line_numbered_inputs
242
+ @resolver.resolve(:show_line_numbered_inputs)
243
+ end
244
+
245
+ def show_line_numbered_inputs=(value)
246
+ @resolver.set_programmatic(:show_line_numbered_inputs, value)
247
+ end
248
+
224
249
  def algorithm
225
250
  @resolver.resolve(:algorithm)
226
251
  end
@@ -266,6 +291,9 @@ module Canon
266
291
  show_diffs: show_diffs,
267
292
  verbose_diff: verbose_diff,
268
293
  diff_algorithm: algorithm,
294
+ show_raw_inputs: show_raw_inputs,
295
+ show_preprocessed_inputs: show_preprocessed_inputs,
296
+ show_line_numbered_inputs: show_line_numbered_inputs,
269
297
  max_file_size: max_file_size,
270
298
  max_node_count: max_node_count,
271
299
  max_diff_lines: max_diff_lines,
@@ -277,12 +305,15 @@ module Canon
277
305
  def build_resolver(format)
278
306
  defaults = {
279
307
  mode: :by_line,
280
- use_color: true,
308
+ use_color: ColorDetector.supports_color?,
281
309
  context_lines: 3,
282
310
  grouping_lines: 10,
283
311
  show_diffs: :all,
284
312
  verbose_diff: false,
285
313
  algorithm: :dom,
314
+ show_raw_inputs: false,
315
+ show_preprocessed_inputs: false,
316
+ show_line_numbered_inputs: false,
286
317
  max_file_size: 5_242_880, # 5MB in bytes
287
318
  max_node_count: 10_000, # Maximum nodes in tree
288
319
  max_diff_lines: 10_000, # Maximum diff output lines
@@ -162,16 +162,23 @@ module Canon
162
162
  Comparison::UNEQUAL_PRIMITIVES => "Unequal primitive values",
163
163
  }.freeze
164
164
 
165
+ # rubocop:disable Metrics/ParameterLists
165
166
  def initialize(use_color: true, mode: :by_object, context_lines: 3,
166
167
  diff_grouping_lines: nil, visualization_map: nil,
167
168
  character_map_file: nil, character_definitions: nil,
168
- show_diffs: :all, verbose_diff: false)
169
+ show_diffs: :all, verbose_diff: false,
170
+ show_raw_inputs: false, show_preprocessed_inputs: false,
171
+ show_line_numbered_inputs: false)
172
+ # rubocop:enable Metrics/ParameterLists
169
173
  @use_color = use_color
170
174
  @mode = mode
171
175
  @context_lines = context_lines
172
176
  @diff_grouping_lines = diff_grouping_lines
173
177
  @show_diffs = show_diffs
174
178
  @verbose_diff = verbose_diff
179
+ @show_raw_inputs = show_raw_inputs
180
+ @show_preprocessed_inputs = show_preprocessed_inputs
181
+ @show_line_numbered_inputs = show_line_numbered_inputs
175
182
  @visualization_map = build_visualization_map(
176
183
  visualization_map: visualization_map,
177
184
  character_map_file: character_map_file,
@@ -329,15 +336,39 @@ module Canon
329
336
  )
330
337
  end
331
338
 
332
- # 2.5. Original Input Strings (ONLY if verbose_diff is enabled)
333
- if @verbose_diff && comparison_result.is_a?(Canon::Comparison::ComparisonResult)
339
+ # verbose_diff enables all three input displays as a convenience
340
+ verbose = @verbose_diff || @show_raw_inputs
341
+ show_prep = @verbose_diff || @show_preprocessed_inputs
342
+ show_line = @verbose_diff || @show_line_numbered_inputs
343
+
344
+ # 3. Raw/Original Input Display (when show_raw_inputs is enabled)
345
+ if verbose && comparison_result.is_a?(Canon::Comparison::ComparisonResult)
346
+ original1, original2 = comparison_result.original_strings
347
+ if original1 && original2
348
+ output << format_raw_inputs(original1, original2)
349
+ end
350
+ end
351
+
352
+ # 4. Preprocessed Input Display (when show_preprocessed_inputs is enabled)
353
+ if show_prep && comparison_result.is_a?(Canon::Comparison::ComparisonResult)
354
+ preprocessed1, preprocessed2 = comparison_result.preprocessed_strings
355
+ if preprocessed1 && preprocessed2
356
+ preprocessing_info = comparison_result.match_options&.dig(:match,
357
+ :preprocessing)
358
+ output << format_preprocessed_inputs(preprocessed1, preprocessed2,
359
+ preprocessing_info)
360
+ end
361
+ end
362
+
363
+ # 5. Line-Numbered Input Display (when show_line_numbered_inputs is enabled)
364
+ if show_line && comparison_result.is_a?(Canon::Comparison::ComparisonResult)
334
365
  original1, original2 = comparison_result.original_strings
335
366
  if original1 && original2
336
- output << format_original_strings(original1, original2)
367
+ output << format_line_numbered_inputs(original1, original2)
337
368
  end
338
369
  end
339
370
 
340
- # 3. Main diff output (by-line or by-object) - ALWAYS
371
+ # 6. Main diff output (by-line or by-object) - ALWAYS
341
372
 
342
373
  # Check if comparison result is a ComparisonResult object
343
374
  if comparison_result.is_a?(Canon::Comparison::ComparisonResult)
@@ -428,24 +459,24 @@ module Canon
428
459
  html.to_s
429
460
  end
430
461
 
431
- # Format original input strings for display (RSpec-style)
432
- # Shows the actual strings that were passed in before any preprocessing
462
+ # Format original input strings with line numbers (RSpec-style)
463
+ # Shows the actual strings that were passed in with line numbers for reference
433
464
  #
434
465
  # @param original1 [String] First original input string
435
466
  # @param original2 [String] Second original input string
436
- # @return [String] Formatted display of original strings
437
- def format_original_strings(original1, original2)
467
+ # @return [String] Formatted display with line numbers
468
+ def format_line_numbered_inputs(original1, original2)
438
469
  return "" if original1.nil? || original2.nil?
439
470
 
440
471
  output = []
441
472
  output << ""
442
473
  output << colorize("=" * 70, :cyan, :bold)
443
- output << colorize(" ORIGINAL INPUT STRINGS", :cyan, :bold)
474
+ output << colorize(" ORIGINAL INPUTS (with line numbers)", :cyan, :bold)
444
475
  output << colorize("=" * 70, :cyan, :bold)
445
476
  output << ""
446
477
 
447
478
  # Format expected
448
- output << colorize("Expected (as string):", :yellow, :bold)
479
+ output << colorize("Expected:", :yellow, :bold)
449
480
  original1.each_line.with_index do |line, idx|
450
481
  output << " #{colorize(sprintf('%4d', idx + 1),
451
482
  :blue)} | #{line.chomp}"
@@ -453,7 +484,7 @@ module Canon
453
484
  output << ""
454
485
 
455
486
  # Format actual
456
- output << colorize("Actual (as string):", :yellow, :bold)
487
+ output << colorize("Received:", :yellow, :bold)
457
488
  original2.each_line.with_index do |line, idx|
458
489
  output << " #{colorize(sprintf('%4d', idx + 1),
459
490
  :blue)} | #{line.chomp}"
@@ -465,6 +496,65 @@ module Canon
465
496
  output.join("\n")
466
497
  end
467
498
 
499
+ # Format raw/original inputs for display (user-friendly copyable format)
500
+ # Shows the raw file contents before any preprocessing
501
+ #
502
+ # @param raw1 [String] First raw input string
503
+ # @param raw2 [String] Second raw input string
504
+ # @return [String] Formatted display of raw inputs
505
+ def format_raw_inputs(raw1, raw2)
506
+ return "" if raw1.nil? || raw2.nil?
507
+
508
+ output = []
509
+ output << ""
510
+ output << colorize("=== ORIGINAL INPUTS (Raw) ===", :cyan, :bold)
511
+ output << ""
512
+ output << colorize("EXPECTED:", :yellow, :bold)
513
+ output << "-" * 70
514
+ output << raw1
515
+ output << ""
516
+ output << colorize("RECEIVED:", :yellow, :bold)
517
+ output << "-" * 70
518
+ output << raw2
519
+ output << ""
520
+ output << ""
521
+
522
+ output.join("\n")
523
+ end
524
+
525
+ # Format preprocessed inputs for display (what was actually compared)
526
+ # Shows the content after preprocessing (c14n, normalize, format, etc.)
527
+ #
528
+ # @param preprocessed1 [String] First preprocessed string
529
+ # @param preprocessed2 [String] Second preprocessed string
530
+ # @param preprocessing_info [Symbol, nil] Preprocessing mode (:c14n, :normalize, :format, etc.)
531
+ # @return [String] Formatted display of preprocessed inputs
532
+ def format_preprocessed_inputs(preprocessed1, preprocessed2,
533
+ preprocessing_info = nil)
534
+ return "" if preprocessed1.nil? || preprocessed2.nil?
535
+
536
+ output = []
537
+ output << ""
538
+ output << colorize("=== PREPROCESSED INPUTS (Compared) ===", :cyan, :bold)
539
+
540
+ # Show preprocessing mode if available
541
+ if preprocessing_info
542
+ output << "Preprocessing: #{preprocessing_info}"
543
+ end
544
+ output << ""
545
+ output << colorize("EXPECTED:", :yellow, :bold)
546
+ output << "-" * 70
547
+ output << preprocessed1
548
+ output << ""
549
+ output << colorize("RECEIVED:", :yellow, :bold)
550
+ output << "-" * 70
551
+ output << preprocessed2
552
+ output << ""
553
+ output << ""
554
+
555
+ output.join("\n")
556
+ end
557
+
468
558
  # Build the final visualization map from various customization options
469
559
  #
470
560
  # @param visualization_map [Hash, nil] Complete custom visualization map
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.1.10"
4
+ VERSION = "0.1.12"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -193,6 +193,7 @@ files:
193
193
  - lib/canon.rb
194
194
  - lib/canon/cache.rb
195
195
  - lib/canon/cli.rb
196
+ - lib/canon/color_detector.rb
196
197
  - lib/canon/commands/diff_command.rb
197
198
  - lib/canon/commands/format_command.rb
198
199
  - lib/canon/comparison.rb