canon 0.2.11 → 0.2.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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +12 -22
  3. data/Rakefile +5 -2
  4. data/lib/canon/cache.rb +3 -1
  5. data/lib/canon/cli.rb +0 -3
  6. data/lib/canon/commands/diff_command.rb +0 -6
  7. data/lib/canon/commands/format_command.rb +0 -4
  8. data/lib/canon/commands.rb +9 -0
  9. data/lib/canon/comparison/child_realignment.rb +0 -2
  10. data/lib/canon/comparison/compare_profile.rb +30 -36
  11. data/lib/canon/comparison/comparison_result.rb +0 -2
  12. data/lib/canon/comparison/diff_node_builder.rb +353 -0
  13. data/lib/canon/comparison/dimensions/dimension.rb +51 -0
  14. data/lib/canon/comparison/dimensions/dimension_set.rb +49 -0
  15. data/lib/canon/comparison/dimensions/registry.rb +101 -60
  16. data/lib/canon/comparison/dimensions.rb +15 -46
  17. data/lib/canon/comparison/html_comparator.rb +18 -141
  18. data/lib/canon/comparison/html_compare_profile.rb +15 -18
  19. data/lib/canon/comparison/json_comparator.rb +4 -165
  20. data/lib/canon/comparison/json_parser.rb +0 -2
  21. data/lib/canon/comparison/markup_comparator.rb +14 -210
  22. data/lib/canon/comparison/match_options/base_resolver.rb +18 -29
  23. data/lib/canon/comparison/match_options/json_resolver.rb +4 -28
  24. data/lib/canon/comparison/match_options/xml_resolver.rb +4 -45
  25. data/lib/canon/comparison/match_options/yaml_resolver.rb +4 -30
  26. data/lib/canon/comparison/match_options.rb +13 -88
  27. data/lib/canon/comparison/pipeline.rb +269 -0
  28. data/lib/canon/comparison/profile_definition.rb +0 -2
  29. data/lib/canon/comparison/ruby_object_comparator.rb +1 -1
  30. data/lib/canon/comparison/strategies/match_strategy_factory.rb +9 -58
  31. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +4 -11
  32. data/lib/canon/comparison/strategies.rb +16 -0
  33. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +0 -3
  34. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +0 -3
  35. data/lib/canon/comparison/xml_comparator/child_comparison.rb +0 -6
  36. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +1 -6
  37. data/lib/canon/comparison/xml_comparator/node_parser.rb +0 -4
  38. data/lib/canon/comparison/xml_comparator.rb +4 -492
  39. data/lib/canon/comparison/xml_comparator_helpers.rb +21 -0
  40. data/lib/canon/comparison/xml_node_comparison.rb +4 -119
  41. data/lib/canon/comparison/yaml_comparator.rb +0 -3
  42. data/lib/canon/comparison.rb +143 -266
  43. data/lib/canon/config/config_dsl.rb +159 -0
  44. data/lib/canon/config/env_provider.rb +0 -3
  45. data/lib/canon/config/env_schema.rb +48 -58
  46. data/lib/canon/config/profile_loader.rb +0 -1
  47. data/lib/canon/config.rb +116 -468
  48. data/lib/canon/diff/diff_block_builder.rb +0 -2
  49. data/lib/canon/diff/diff_classifier.rb +0 -5
  50. data/lib/canon/diff/diff_context.rb +0 -2
  51. data/lib/canon/diff/diff_context_builder.rb +0 -2
  52. data/lib/canon/diff/diff_line_builder.rb +0 -3
  53. data/lib/canon/diff/diff_node_enricher.rb +0 -4
  54. data/lib/canon/diff/diff_node_mapper.rb +0 -4
  55. data/lib/canon/diff/diff_report_builder.rb +0 -4
  56. data/lib/canon/diff/formatting_detector.rb +0 -1
  57. data/lib/canon/diff/node_serializer.rb +0 -7
  58. data/lib/canon/diff.rb +39 -0
  59. data/lib/canon/diff_formatter/by_line/base_formatter.rb +4 -17
  60. data/lib/canon/diff_formatter/by_line/html_formatter.rb +7 -19
  61. data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -3
  62. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -3
  63. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +7 -26
  64. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -3
  65. data/lib/canon/diff_formatter/by_object/base_formatter.rb +8 -15
  66. data/lib/canon/diff_formatter/by_object/json_formatter.rb +0 -2
  67. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +0 -2
  68. data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +0 -2
  69. data/lib/canon/diff_formatter/debug_output.rb +0 -2
  70. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +24 -58
  71. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +0 -2
  72. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +1 -2
  73. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +1 -7
  74. data/lib/canon/diff_formatter/diff_detail_formatter.rb +0 -7
  75. data/lib/canon/diff_formatter/diff_detail_formatter_helpers.rb +23 -0
  76. data/lib/canon/diff_formatter.rb +11 -9
  77. data/lib/canon/formatters/html4_formatter.rb +0 -2
  78. data/lib/canon/formatters/html5_formatter.rb +0 -2
  79. data/lib/canon/formatters/html_formatter.rb +0 -3
  80. data/lib/canon/formatters/json_formatter.rb +0 -1
  81. data/lib/canon/formatters/xml_formatter.rb +0 -4
  82. data/lib/canon/formatters/yaml_formatter.rb +0 -1
  83. data/lib/canon/formatters.rb +16 -0
  84. data/lib/canon/html/data_model.rb +0 -10
  85. data/lib/canon/html.rb +4 -3
  86. data/lib/canon/options/cli_generator.rb +0 -2
  87. data/lib/canon/options/registry.rb +0 -2
  88. data/lib/canon/options.rb +9 -0
  89. data/lib/canon/pretty_printer/html.rb +0 -1
  90. data/lib/canon/pretty_printer/xml_normalized.rb +0 -2
  91. data/lib/canon/pretty_printer.rb +12 -0
  92. data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
  93. data/lib/canon/tree_diff/adapters.rb +14 -0
  94. data/lib/canon/tree_diff/core/attribute_comparator.rb +0 -6
  95. data/lib/canon/tree_diff/core/node_signature.rb +1 -1
  96. data/lib/canon/tree_diff/core/tree_node.rb +12 -5
  97. data/lib/canon/tree_diff/core.rb +17 -0
  98. data/lib/canon/tree_diff/matchers/hash_matcher.rb +0 -7
  99. data/lib/canon/tree_diff/matchers/similarity_matcher.rb +1 -5
  100. data/lib/canon/tree_diff/matchers/structural_propagator.rb +1 -5
  101. data/lib/canon/tree_diff/matchers.rb +15 -0
  102. data/lib/canon/tree_diff/operation_converter.rb +0 -8
  103. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +2 -12
  104. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +13 -7
  105. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +2 -2
  106. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +4 -6
  107. data/lib/canon/tree_diff/operation_converter_helpers.rb +18 -0
  108. data/lib/canon/tree_diff/operations/operation_detector.rb +2 -5
  109. data/lib/canon/tree_diff/operations.rb +13 -0
  110. data/lib/canon/tree_diff.rb +26 -27
  111. data/lib/canon/validators/base_validator.rb +0 -2
  112. data/lib/canon/validators/html_validator.rb +0 -1
  113. data/lib/canon/validators/json_validator.rb +0 -1
  114. data/lib/canon/validators/xml_validator.rb +0 -1
  115. data/lib/canon/validators/yaml_validator.rb +0 -1
  116. data/lib/canon/validators.rb +12 -0
  117. data/lib/canon/version.rb +1 -1
  118. data/lib/canon/xml/c14n.rb +0 -4
  119. data/lib/canon/xml/data_model.rb +0 -10
  120. data/lib/canon/xml/line_range_mapper.rb +0 -2
  121. data/lib/canon/xml/nodes/attribute_node.rb +0 -2
  122. data/lib/canon/xml/nodes/comment_node.rb +0 -2
  123. data/lib/canon/xml/nodes/element_node.rb +0 -2
  124. data/lib/canon/xml/nodes/namespace_node.rb +0 -2
  125. data/lib/canon/xml/nodes/processing_instruction_node.rb +0 -2
  126. data/lib/canon/xml/nodes/root_node.rb +0 -2
  127. data/lib/canon/xml/nodes/text_node.rb +0 -2
  128. data/lib/canon/xml/nodes.rb +19 -0
  129. data/lib/canon/xml/processor.rb +0 -5
  130. data/lib/canon/xml/sax_builder.rb +0 -7
  131. data/lib/canon/xml.rb +33 -0
  132. data/lib/canon/xml_backend.rb +50 -14
  133. data/lib/canon/xml_parsing.rb +4 -2
  134. data/lib/canon.rb +25 -15
  135. data/lib/tasks/performance.rake +0 -58
  136. data/lib/tasks/performance_comparator.rb +132 -65
  137. data/lib/tasks/performance_helpers.rb +4 -249
  138. data/lib/tasks/performance_report.rb +309 -0
  139. metadata +24 -11
  140. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +0 -64
  141. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +0 -64
  142. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +0 -167
  143. data/lib/canon/comparison/dimensions/base_dimension.rb +0 -107
  144. data/lib/canon/comparison/dimensions/comments_dimension.rb +0 -117
  145. data/lib/canon/comparison/dimensions/element_position_dimension.rb +0 -86
  146. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +0 -115
  147. data/lib/canon/comparison/dimensions/text_content_dimension.rb +0 -102
  148. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +0 -300
data/lib/canon/config.rb CHANGED
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "config/env_provider"
4
- require_relative "config/override_resolver"
5
- require_relative "config/profile_loader"
6
- require_relative "color_detector"
7
-
8
3
  module Canon
9
4
  # Global configuration for Canon
10
5
  # Provides unified configuration across CLI, Ruby API, and RSpec interfaces
11
6
  class Config
7
+ autoload :ConfigDSL, "canon/config/config_dsl"
8
+ autoload :EnvProvider, "canon/config/env_provider"
9
+ autoload :EnvSchema, "canon/config/env_schema"
10
+ autoload :OverrideResolver, "canon/config/override_resolver"
11
+ autoload :ProfileLoader, "canon/config/profile_loader"
12
+ autoload :TypeConverter, "canon/config/type_converter"
13
+
12
14
  class << self
13
15
  def instance
14
16
  @instance ||= new
@@ -295,22 +297,16 @@ module Canon
295
297
  end
296
298
 
297
299
  # Diff configuration for output formatting
300
+ #
301
+ # Each user-tunable attribute is declared with +config_key+ via the
302
+ # +ConfigDSL+ module. The DSL generates the matching getter/setter
303
+ # pair and registers metadata (type, enum, default) so +EnvSchema+
304
+ # and +TypeConverter+ can discover it without re-declaring it
305
+ # (lutaml/canon TODO.improve/07 — single source of truth).
298
306
  class DiffConfig
299
- attr_reader :pretty_printer
307
+ extend ConfigDSL
300
308
 
301
- # Valid values for enum-like configuration options
302
- VALID_ENUM_VALUES = {
303
- mode: %i[by_line by_object pretty_diff],
304
- show_diffs: %i[all normative informative],
305
- algorithm: %i[dom semantic],
306
- parser: %i[sax dom],
307
- display_preprocessing: %i[none pretty_print normalize_pretty_print
308
- c14n],
309
- display_format: %i[raw canonical],
310
- pretty_printer_indent_type: %i[space tab],
311
- character_visualization: [true, false, :content_only],
312
- theme: %i[light dark retro claude cyberpunk],
313
- }.freeze
309
+ attr_reader :pretty_printer
314
310
 
315
311
  def initialize(format = nil)
316
312
  @format = format
@@ -338,416 +334,104 @@ module Canon
338
334
  @resolver.clear_profile!
339
335
  end
340
336
 
341
- # Validate a config value against its allowed enum values
342
- def self.validate_config_value!(key, value)
343
- valid = VALID_ENUM_VALUES[key]
344
- return unless valid
345
-
346
- return if valid.include?(value)
347
-
348
- raise ArgumentError,
349
- "Invalid value #{value.inspect} for #{key}. " \
350
- "Valid values: #{valid.map(&:inspect).join(', ')}"
351
- end
352
-
353
- # Accessors with ENV override support
354
- def mode
355
- @resolver.resolve(:mode)
356
- end
357
-
358
- def mode=(value)
359
- self.class.validate_config_value!(:mode, value)
360
- @resolver.set_programmatic(:mode, value)
361
- end
362
-
363
- def use_color
364
- @resolver.resolve(:use_color)
365
- end
366
-
367
- def use_color=(value)
368
- @resolver.set_programmatic(:use_color, value)
369
- end
370
-
371
- def context_lines
372
- @resolver.resolve(:context_lines)
373
- end
374
-
375
- def context_lines=(value)
376
- @resolver.set_programmatic(:context_lines, value)
377
- end
378
-
379
- def grouping_lines
380
- @resolver.resolve(:grouping_lines)
381
- end
382
-
383
- def grouping_lines=(value)
384
- @resolver.set_programmatic(:grouping_lines, value)
385
- end
386
-
387
- def show_diffs
388
- @resolver.resolve(:show_diffs)
389
- end
390
-
391
- def show_diffs=(value)
392
- self.class.validate_config_value!(:show_diffs, value)
393
- @resolver.set_programmatic(:show_diffs, value)
394
- end
395
-
396
- def verbose_diff
397
- @resolver.resolve(:verbose_diff)
398
- end
399
-
400
- def verbose_diff=(value)
401
- @resolver.set_programmatic(:verbose_diff, value)
402
- end
403
-
404
- def show_raw_inputs
405
- @resolver.resolve(:show_raw_inputs)
406
- end
407
-
408
- def show_raw_inputs=(value)
409
- @resolver.set_programmatic(:show_raw_inputs, value)
410
- end
411
-
412
- # Show only the EXPECTED (fixture) block in the raw-inputs section.
413
- # Has no effect unless +show_raw_inputs+ or +verbose_diff+ is also set.
414
- # Use +show_raw_expected: false+ together with +show_raw_received: true+
415
- # (or +show_raw_inputs: true+) to suppress the fixture display while
416
- # keeping the received output.
417
- #
418
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_RAW_EXPECTED+
419
- def show_raw_expected
420
- @resolver.resolve(:show_raw_expected)
421
- end
422
-
423
- def show_raw_expected=(value)
424
- @resolver.set_programmatic(:show_raw_expected, value)
425
- end
426
-
427
- # Show only the RECEIVED (actual) block in the raw-inputs section.
428
- # Combined with +show_raw_expected: false+ (or leaving it at the default
429
- # +false+) this suppresses the fixture while still displaying the output
430
- # that was generated.
431
- #
432
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_RAW_RECEIVED+
433
- def show_raw_received
434
- @resolver.resolve(:show_raw_received)
435
- end
436
-
437
- def show_raw_received=(value)
438
- @resolver.set_programmatic(:show_raw_received, value)
439
- end
440
-
441
- def show_preprocessed_inputs
442
- @resolver.resolve(:show_preprocessed_inputs)
443
- end
444
-
445
- def show_preprocessed_inputs=(value)
446
- @resolver.set_programmatic(:show_preprocessed_inputs, value)
447
- end
448
-
449
- # Show only the EXPECTED (fixture) block in the preprocessed-inputs
450
- # section. Has no effect unless +show_preprocessed_inputs+ or
451
- # +verbose_diff+ is also set. Use +show_preprocessed_expected: true+
452
- # together with +show_preprocessed_received: false+ to display only the
453
- # preprocessed fixture while suppressing the preprocessed received output.
454
- #
455
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_PREPROCESSED_EXPECTED+
456
- def show_preprocessed_expected
457
- @resolver.resolve(:show_preprocessed_expected)
458
- end
459
-
460
- def show_preprocessed_expected=(value)
461
- @resolver.set_programmatic(:show_preprocessed_expected, value)
462
- end
463
-
464
- # Show only the RECEIVED (actual) block in the preprocessed-inputs
465
- # section. Combined with +show_preprocessed_expected: false+ (or leaving
466
- # it at the default +false+) this suppresses the fixture preprocessing
467
- # display while still showing what the received document looked like after
468
- # preprocessing.
469
- #
470
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_PREPROCESSED_RECEIVED+
471
- def show_preprocessed_received
472
- @resolver.resolve(:show_preprocessed_received)
473
- end
474
-
475
- def show_preprocessed_received=(value)
476
- @resolver.set_programmatic(:show_preprocessed_received, value)
477
- end
478
-
479
- # Show both EXPECTED and RECEIVED blocks in a fixture-ready pretty-printed
480
- # section. The output uses the same pretty-printer as
481
- # +display_preprocessing: :pretty_print+ (one tag per line, indentation)
482
- # but with *no* character visualization — whitespace appears as plain ASCII
483
- # so the output can be copy-pasted directly into RSpec fixture heredocs.
484
- #
485
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_PRETTYPRINT_INPUTS+
486
- def show_prettyprint_inputs
487
- @resolver.resolve(:show_prettyprint_inputs)
488
- end
489
-
490
- def show_prettyprint_inputs=(value)
491
- @resolver.set_programmatic(:show_prettyprint_inputs, value)
492
- end
493
-
494
- # Show only the EXPECTED (fixture) block in the pretty-print section.
495
- # Useful when the fixture is what needs updating and the received side is
496
- # not needed for copy-pasting.
497
- #
498
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_PRETTYPRINT_EXPECTED+
499
- def show_prettyprint_expected
500
- @resolver.resolve(:show_prettyprint_expected)
501
- end
502
-
503
- def show_prettyprint_expected=(value)
504
- @resolver.set_programmatic(:show_prettyprint_expected, value)
505
- end
506
-
507
- # Show only the RECEIVED (actual) block in the pretty-print section.
508
- # Use this to get a copy-pasteable pretty-printed form of the generated
509
- # output (the most common fixture-update workflow).
510
- #
511
- # ENV variable: +CANON_<FORMAT>_DIFF_SHOW_PRETTYPRINT_RECEIVED+
512
- def show_prettyprint_received
513
- @resolver.resolve(:show_prettyprint_received)
514
- end
515
-
516
- def show_prettyprint_received=(value)
517
- @resolver.set_programmatic(:show_prettyprint_received, value)
518
- end
519
-
520
- def show_line_numbered_inputs
521
- @resolver.resolve(:show_line_numbered_inputs)
522
- end
523
-
524
- def show_line_numbered_inputs=(value)
525
- @resolver.set_programmatic(:show_line_numbered_inputs, value)
526
- end
527
-
528
- def display_format
529
- @resolver.resolve(:display_format)
530
- end
531
-
532
- def display_format=(value)
533
- self.class.validate_config_value!(:display_format, value)
534
- @resolver.set_programmatic(:display_format, value)
535
- end
536
-
537
- # Controls how documents are normalized *for display* before the line
538
- # diff. This is independent of +FormatConfig#preprocessing+, which
539
- # controls normalization for *comparison* (equivalence detection).
540
- #
541
- # Values:
542
- # :none - use documents as-is (default, existing behaviour)
543
- # :pretty_print - run through Canon::PrettyPrinter::Xml before diffing
544
- # :c14n - run through XML C14N normalization before diffing
545
- def display_preprocessing
546
- @resolver.resolve(:display_preprocessing)
547
- end
548
-
549
- def display_preprocessing=(value)
550
- self.class.validate_config_value!(:display_preprocessing, value)
551
- @resolver.set_programmatic(:display_preprocessing, value)
552
- end
553
-
554
- # Element names where whitespace is PRESERVED exactly (no manipulation).
555
- # All whitespace characters are significant in these elements.
556
- # ENV variable: +CANON_<FORMAT>_DIFF_PRESERVE_WHITESPACE_ELEMENTS+
557
- def preserve_whitespace_elements
558
- @resolver.resolve(:preserve_whitespace_elements) || []
559
- end
560
-
561
- def preserve_whitespace_elements=(value)
562
- @resolver.set_programmatic(:preserve_whitespace_elements,
563
- Array(value).map(&:to_s))
564
- end
565
-
566
- # Element names where whitespace is COLLAPSED (HTML-style behavior).
567
- # Multiple whitespace chars collapse to single space; boundaries preserved.
568
- # ENV variable: +CANON_<FORMAT>_DIFF_COLLAPSE_WHITESPACE_ELEMENTS+
569
- def collapse_whitespace_elements
570
- @resolver.resolve(:collapse_whitespace_elements) || []
571
- end
572
-
573
- def collapse_whitespace_elements=(value)
574
- @resolver.set_programmatic(:collapse_whitespace_elements,
575
- Array(value).map(&:to_s))
576
- end
577
-
578
- # Element names where whitespace-only text nodes are STRIPPED.
579
- # ENV variable: +CANON_<FORMAT>_DIFF_STRIP_WHITESPACE_ELEMENTS+
580
- def strip_whitespace_elements
581
- @resolver.resolve(:strip_whitespace_elements) || []
582
- end
583
-
584
- def strip_whitespace_elements=(value)
585
- @resolver.set_programmatic(:strip_whitespace_elements,
586
- Array(value).map(&:to_s))
587
- end
588
-
589
- # When true, whitespace-only text nodes starting with "\n" in :collapse
590
- # elements of the **expected** (fixture) document are treated as structural
591
- # indentation and dropped from both comparison and display. Use this when
592
- # fixture files are indented but received XML is compact.
593
- # ENV variable: +CANON_<FORMAT>_DIFF_PRETTY_PRINTED_EXPECTED+
594
- def pretty_printed_expected
595
- @resolver.resolve(:pretty_printed_expected)
596
- end
597
-
598
- def pretty_printed_expected=(value)
599
- @resolver.set_programmatic(:pretty_printed_expected, value)
600
- end
601
-
602
- # When true, whitespace-only text nodes starting with "\n" in :normalize
603
- # elements of the **received** document are treated as structural
604
- # indentation and dropped from both comparison and display. Use this when
605
- # received XML may be pretty-printed but the fixture is compact.
606
- # ENV variable: +CANON_<FORMAT>_DIFF_PRETTY_PRINTED_RECEIVED+
607
- def pretty_printed_received
608
- @resolver.resolve(:pretty_printed_received)
609
- end
610
-
611
- def pretty_printed_received=(value)
612
- @resolver.set_programmatic(:pretty_printed_received, value)
613
- end
614
-
615
- # When true, attributes on each element are sorted by namespace URI
616
- # then local name in the pretty-printed display, eliminating spurious
617
- # diff noise from differing attribute order.
618
- # ENV variable: +CANON_<FORMAT>_DIFF_PRETTY_PRINTER_SORT_ATTRIBUTES+
619
- def pretty_printer_sort_attributes
620
- @resolver.resolve(:pretty_printer_sort_attributes)
621
- end
622
-
623
- def pretty_printer_sort_attributes=(value)
624
- @resolver.set_programmatic(:pretty_printer_sort_attributes, value)
625
- end
626
-
627
- # Render element nodes in the Semantic Diff Report as compact inline XML
628
- # (e.g. +<strong>Annex</strong>+) instead of the verbose node_info
629
- # description string (e.g. "name: strong namespace_uri: …").
630
- #
631
- # Default: +false+ (keep existing verbose format for backwards compatibility)
632
- # ENV variable: +CANON_<FORMAT>_DIFF_COMPACT_SEMANTIC_REPORT+
633
- def compact_semantic_report
634
- @resolver.resolve(:compact_semantic_report)
635
- end
636
-
637
- def compact_semantic_report=(value)
638
- @resolver.set_programmatic(:compact_semantic_report, value)
639
- end
640
-
641
- # Show the full serialized node content (including children) in
642
- # element_structure diffs instead of just the tag name.
643
- #
644
- # Default: +false+ (show only the tag name, e.g. +<biblio-tag>+)
645
- # ENV variable: +CANON_<FORMAT>_DIFF_EXPAND_DIFFERENCE+
646
- def expand_difference
647
- @resolver.resolve(:expand_difference)
648
- end
649
-
650
- def expand_difference=(value)
651
- @resolver.set_programmatic(:expand_difference, value)
652
- end
653
-
654
- # Controls whether invisible characters (spaces, tabs, non-breaking
655
- # spaces, etc.) are replaced with visible Unicode symbols in diff output.
656
- #
657
- # Values:
658
- # true - apply the full default visualization map (default)
659
- # false - disable visualization; output plain text
660
- # :content_only - apply visualization only to text content, not
661
- # to structural indentation whitespace.
662
- def character_visualization
663
- val = @resolver.resolve(:character_visualization)
664
- # Coerce symbol booleans that may arrive via ENV (env_schema uses :symbol type
665
- # so "true"/"false" env strings become :true/:false symbols)
666
- case val
667
- when true, :true then true # rubocop:disable Lint/BooleanSymbol
668
- when false, :false then false # rubocop:disable Lint/BooleanSymbol
669
- else val # true/false from programmatic, or :content_only
670
- end
671
- end
672
-
673
- def character_visualization=(value)
674
- self.class.validate_config_value!(:character_visualization, value)
675
- @resolver.set_programmatic(:character_visualization, value)
676
- end
677
-
678
- def algorithm
679
- @resolver.resolve(:algorithm)
680
- end
681
-
682
- def algorithm=(value)
683
- self.class.validate_config_value!(:algorithm, value)
684
- @resolver.set_programmatic(:algorithm, value)
685
- end
686
-
687
- # XML parser backend (:sax or :dom, default :sax)
688
- def parser
689
- @resolver.resolve(:parser)
690
- end
691
-
692
- def parser=(value)
693
- self.class.validate_config_value!(:parser, value)
694
- @resolver.set_programmatic(:parser, value)
695
- end
696
-
697
- # Theme name (:light, :dark, :retro, :claude)
698
- def theme
699
- @resolver.resolve(:theme)
700
- end
701
-
702
- def theme=(value)
703
- self.class.validate_config_value!(:theme, value)
704
- @resolver.set_programmatic(:theme, value)
705
- end
706
-
707
- # Theme inheritance (custom theme with base + overrides)
708
- def theme_inheritance
709
- @resolver.resolve(:theme_inheritance)
710
- end
711
-
712
- def theme_inheritance=(value)
713
- @resolver.set_programmatic(:theme_inheritance, value)
714
- end
715
-
716
- # Full custom theme hash
717
- def custom_theme
718
- @resolver.resolve(:custom_theme)
719
- end
720
-
721
- def custom_theme=(value)
722
- @resolver.set_programmatic(:custom_theme, value)
723
- end
724
-
725
- # File size limit in bytes (default 5MB)
726
- def max_file_size
727
- @resolver.resolve(:max_file_size)
728
- end
729
-
730
- def max_file_size=(value)
731
- @resolver.set_programmatic(:max_file_size, value)
732
- end
733
-
734
- # Maximum node count in tree (default 10,000)
735
- def max_node_count
736
- @resolver.resolve(:max_node_count)
737
- end
738
-
739
- def max_node_count=(value)
740
- @resolver.set_programmatic(:max_node_count, value)
741
- end
742
-
743
- # Maximum diff output lines (default 10,000)
744
- def max_diff_lines
745
- @resolver.resolve(:max_diff_lines)
746
- end
747
-
748
- def max_diff_lines=(value)
749
- @resolver.set_programmatic(:max_diff_lines, value)
750
- end
337
+ # --- Attribute declarations --------------------------------------
338
+
339
+ config_key :mode, type: :symbol,
340
+ enum: %i[by_line by_object pretty_diff],
341
+ default: :by_line
342
+ config_key :use_color, type: :boolean,
343
+ default: -> { ColorDetector.supports_color? }
344
+ config_key :context_lines, type: :integer, default: 3
345
+ config_key :grouping_lines, type: :integer, default: 10
346
+ config_key :show_diffs, type: :symbol,
347
+ enum: %i[all normative informative],
348
+ default: :all
349
+ config_key :verbose_diff, type: :boolean, default: false
350
+ config_key :algorithm, type: :symbol, enum: %i[dom semantic],
351
+ default: :dom
352
+ config_key :parser, type: :symbol, enum: %i[sax dom], default: :sax
353
+
354
+ config_key :show_raw_inputs, type: :boolean, default: false
355
+ config_key :show_raw_expected, type: :boolean, default: false
356
+ config_key :show_raw_received, type: :boolean, default: false
357
+ config_key :show_preprocessed_inputs, type: :boolean, default: false
358
+ config_key :show_preprocessed_expected, type: :boolean, default: false
359
+ config_key :show_preprocessed_received, type: :boolean, default: false
360
+ config_key :show_prettyprint_inputs, type: :boolean, default: false
361
+ config_key :show_prettyprint_expected, type: :boolean, default: false
362
+ config_key :show_prettyprint_received, type: :boolean, default: false
363
+ config_key :show_line_numbered_inputs, type: :boolean, default: false
364
+
365
+ config_key :display_format, type: :symbol, enum: %i[raw canonical],
366
+ default: :raw
367
+ config_key :display_preprocessing, type: :symbol,
368
+ enum: %i[none pretty_print
369
+ normalize_pretty_print c14n],
370
+ default: :none
371
+
372
+ config_key :preserve_whitespace_elements,
373
+ type: :string_array,
374
+ default: [],
375
+ coerce: ->(v) { Array(v).map(&:to_s) },
376
+ getter_coerce: ->(v) { v || [] }
377
+ config_key :collapse_whitespace_elements,
378
+ type: :string_array,
379
+ default: [],
380
+ coerce: ->(v) { Array(v).map(&:to_s) },
381
+ getter_coerce: ->(v) { v || [] }
382
+ config_key :strip_whitespace_elements,
383
+ type: :string_array,
384
+ default: [],
385
+ coerce: ->(v) { Array(v).map(&:to_s) },
386
+ getter_coerce: ->(v) { v || [] }
387
+
388
+ config_key :pretty_printed_expected, type: :boolean, default: false
389
+ config_key :pretty_printed_received, type: :boolean, default: false
390
+ config_key :pretty_printer_sort_attributes, type: :boolean,
391
+ default: false
392
+ config_key :compact_semantic_report, type: :boolean, default: false
393
+ config_key :expand_difference, type: :boolean, default: false
394
+
395
+ # Accepts +true+, +false+, or +:content_only+. ENV-supplied
396
+ # +"true"/"false"+ strings arrive as +:true/:false+ (TypeConverter
397
+ # uses the +:symbol+ type) so the getter coerces them back to
398
+ # booleans.
399
+ config_key :character_visualization,
400
+ type: :symbol,
401
+ enum: [true, false, :content_only],
402
+ default: true,
403
+ getter_coerce: lambda { |val|
404
+ case val
405
+ when true, :true then true # rubocop:disable Lint/BooleanSymbol
406
+ when false, :false then false # rubocop:disable Lint/BooleanSymbol
407
+ else val
408
+ end
409
+ }
410
+
411
+ config_key :theme, type: :symbol,
412
+ enum: %i[light dark retro claude cyberpunk],
413
+ default: :dark
414
+ config_key :theme_inheritance, type: :pass_through, default: nil
415
+ config_key :custom_theme, type: :pass_through, default: nil
416
+
417
+ config_key :max_file_size, type: :integer, default: 5_242_880
418
+ config_key :max_node_count, type: :integer, default: 10_000
419
+ config_key :max_diff_lines, type: :integer, default: 10_000
420
+
421
+ # Pretty-printer keys are declared here so they participate in
422
+ # +config_keys+ (for EnvSchema discovery and validation) even
423
+ # though user code reaches them via the +PrettyPrinterConfig+
424
+ # facade through +DiffConfig#pretty_printer+.
425
+ config_key :pretty_printer_indent, type: :integer, default: 2
426
+ config_key :pretty_printer_indent_type, type: :symbol,
427
+ enum: %i[space tab],
428
+ default: :space
429
+
430
+ # Enum constraint map derived from declared config keys. Kept as a
431
+ # constant for backward compatibility with code that referenced
432
+ # +DiffConfig::VALID_ENUM_VALUES+ directly. Must be assigned AFTER
433
+ # all +config_key+ declarations above so the registry is populated.
434
+ VALID_ENUM_VALUES = enum_values.freeze
751
435
 
752
436
  # Build diff options
753
437
  def to_h
@@ -792,45 +476,9 @@ module Canon
792
476
  private
793
477
 
794
478
  def build_resolver(format)
795
- defaults = {
796
- mode: :by_line,
797
- use_color: ColorDetector.supports_color?,
798
- context_lines: 3,
799
- grouping_lines: 10,
800
- show_diffs: :all,
801
- verbose_diff: false,
802
- algorithm: :dom,
803
- parser: :sax,
804
- show_raw_inputs: false,
805
- show_raw_expected: false,
806
- show_raw_received: false,
807
- show_preprocessed_inputs: false,
808
- show_preprocessed_expected: false,
809
- show_preprocessed_received: false,
810
- show_prettyprint_inputs: false,
811
- show_prettyprint_expected: false,
812
- show_prettyprint_received: false,
813
- show_line_numbered_inputs: false,
814
- character_visualization: true, # true, false, :content_only
815
- display_format: :raw, # :raw = no formatting, :canonical = HTML-aware formatting
816
- display_preprocessing: :none, # :none, :pretty_print, :c14n
817
- pretty_printer_indent: 2,
818
- pretty_printer_indent_type: :space, # :space or :tab
819
- preserve_whitespace_elements: [],
820
- collapse_whitespace_elements: [],
821
- strip_whitespace_elements: [],
822
- pretty_printed_expected: false,
823
- pretty_printed_received: false,
824
- pretty_printer_sort_attributes: false,
825
- compact_semantic_report: false,
826
- expand_difference: false,
827
- max_file_size: 5_242_880, # 5MB in bytes
828
- max_node_count: 10_000, # Maximum nodes in tree
829
- max_diff_lines: 10_000, # Maximum diff output lines
830
- theme: :dark, # Default theme
831
- theme_inheritance: nil, # Custom theme with base + overrides
832
- custom_theme: nil, # Full custom theme hash
833
- }
479
+ defaults = self.class.config_keys.each_with_object({}) do |(k, _m), h|
480
+ h[k] = self.class.resolve_default(k)
481
+ end
834
482
 
835
483
  env = format ? EnvProvider.load_diff_for_format(format) : {}
836
484
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diff_block"
4
-
5
3
  module Canon
6
4
  module Diff
7
5
  # Builds DiffBlocks from DiffLines
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "formatting_detector"
4
- require_relative "xml_serialization_formatter"
5
- require_relative "../comparison/compare_profile"
6
- require_relative "../comparison/whitespace_sensitivity"
7
-
8
3
  module Canon
9
4
  module Diff
10
5
  # Classifies DiffNodes as normative (affects equivalence) or informative (doesn't affect equivalence)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diff_block"
4
-
5
3
  module Canon
6
4
  module Diff
7
5
  # Represents a context - a group of diff blocks with surrounding context lines
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diff_context"
4
-
5
3
  module Canon
6
4
  module Diff
7
5
  # Builds DiffContexts from DiffBlocks
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diff_line"
4
- require_relative "formatting_detector"
5
-
6
3
  module Canon
7
4
  module Diff
8
5
  # Assembles DiffLines from enriched DiffNodes.
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diff_char_range"
4
- require_relative "text_decomposer"
5
- require_relative "source_locator"
6
-
7
3
  module Canon
8
4
  module Diff
9
5
  # Enriches DiffNodes with character position data (DiffCharRanges).