canon 0.1.7 → 0.1.8

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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +25 -135
  3. data/README.adoc +13 -13
  4. data/docs/.lycheeignore +69 -0
  5. data/docs/advanced/extending-canon.adoc +193 -0
  6. data/docs/internals/diffnode-enrichment.adoc +611 -0
  7. data/docs/internals/index.adoc +251 -0
  8. data/docs/lychee.toml +13 -6
  9. data/docs/plans/2025-01-17-html-parser-selection-fix.adoc +250 -0
  10. data/docs/understanding/architecture.adoc +749 -33
  11. data/docs/understanding/comparison-pipeline.adoc +122 -0
  12. data/false_positive_analysis.txt +0 -0
  13. data/file1.html +1 -0
  14. data/file2.html +1 -0
  15. data/lib/canon/cache.rb +129 -0
  16. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +68 -0
  17. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +68 -0
  18. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +171 -0
  19. data/lib/canon/comparison/dimensions/base_dimension.rb +107 -0
  20. data/lib/canon/comparison/dimensions/comments_dimension.rb +121 -0
  21. data/lib/canon/comparison/dimensions/element_position_dimension.rb +90 -0
  22. data/lib/canon/comparison/dimensions/registry.rb +77 -0
  23. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +119 -0
  24. data/lib/canon/comparison/dimensions/text_content_dimension.rb +96 -0
  25. data/lib/canon/comparison/dimensions.rb +54 -0
  26. data/lib/canon/comparison/format_detector.rb +86 -0
  27. data/lib/canon/comparison/html_comparator.rb +51 -18
  28. data/lib/canon/comparison/html_parser.rb +80 -0
  29. data/lib/canon/comparison/json_comparator.rb +12 -0
  30. data/lib/canon/comparison/json_parser.rb +19 -0
  31. data/lib/canon/comparison/markup_comparator.rb +293 -0
  32. data/lib/canon/comparison/match_options/base_resolver.rb +143 -0
  33. data/lib/canon/comparison/match_options/json_resolver.rb +82 -0
  34. data/lib/canon/comparison/match_options/xml_resolver.rb +151 -0
  35. data/lib/canon/comparison/match_options/yaml_resolver.rb +87 -0
  36. data/lib/canon/comparison/match_options.rb +68 -463
  37. data/lib/canon/comparison/profile_definition.rb +149 -0
  38. data/lib/canon/comparison/ruby_object_comparator.rb +180 -0
  39. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +7 -10
  40. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +177 -0
  41. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +136 -0
  42. data/lib/canon/comparison/xml_comparator/child_comparison.rb +189 -0
  43. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +115 -0
  44. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +186 -0
  45. data/lib/canon/comparison/xml_comparator/node_parser.rb +74 -0
  46. data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +95 -0
  47. data/lib/canon/comparison/xml_comparator.rb +52 -664
  48. data/lib/canon/comparison/xml_node_comparison.rb +297 -0
  49. data/lib/canon/comparison/xml_parser.rb +19 -0
  50. data/lib/canon/comparison/yaml_comparator.rb +3 -3
  51. data/lib/canon/comparison.rb +265 -110
  52. data/lib/canon/diff/diff_node.rb +32 -2
  53. data/lib/canon/diff/node_serializer.rb +191 -0
  54. data/lib/canon/diff/path_builder.rb +143 -0
  55. data/lib/canon/diff_formatter/by_line/base_formatter.rb +251 -0
  56. data/lib/canon/diff_formatter/by_line/html_formatter.rb +6 -248
  57. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +38 -229
  58. data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +30 -0
  59. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +579 -0
  60. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +121 -0
  61. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +253 -0
  62. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +61 -0
  63. data/lib/canon/diff_formatter/diff_detail_formatter.rb +31 -1028
  64. data/lib/canon/diff_formatter.rb +1 -1
  65. data/lib/canon/rspec_matchers.rb +1 -1
  66. data/lib/canon/tree_diff/operation_converter.rb +92 -338
  67. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +71 -0
  68. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +103 -0
  69. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +168 -0
  70. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +188 -0
  71. data/lib/canon/version.rb +1 -1
  72. data/old-docs/ADVANCED_TOPICS.adoc +20 -0
  73. data/old-docs/BASIC_USAGE.adoc +16 -0
  74. data/old-docs/CHARACTER_VISUALIZATION.adoc +567 -0
  75. data/old-docs/CLI.adoc +497 -0
  76. data/old-docs/CUSTOMIZING_BEHAVIOR.adoc +19 -0
  77. data/old-docs/DIFF_ARCHITECTURE.adoc +435 -0
  78. data/old-docs/DIFF_FORMATTING.adoc +540 -0
  79. data/old-docs/DIFF_PARAMETERS.adoc +261 -0
  80. data/old-docs/DOM_DIFF.adoc +1017 -0
  81. data/old-docs/ENV_CONFIG.adoc +876 -0
  82. data/old-docs/FORMATS.adoc +867 -0
  83. data/old-docs/INPUT_VALIDATION.adoc +477 -0
  84. data/old-docs/MATCHER_BEHAVIOR.adoc +90 -0
  85. data/old-docs/MATCH_ARCHITECTURE.adoc +463 -0
  86. data/old-docs/MATCH_OPTIONS.adoc +912 -0
  87. data/old-docs/MODES.adoc +432 -0
  88. data/old-docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +219 -0
  89. data/old-docs/OPTIONS.adoc +1387 -0
  90. data/old-docs/PREPROCESSING.adoc +491 -0
  91. data/old-docs/README.old.adoc +2831 -0
  92. data/old-docs/RSPEC.adoc +814 -0
  93. data/old-docs/RUBY_API.adoc +485 -0
  94. data/old-docs/SEMANTIC_DIFF_REPORT.adoc +646 -0
  95. data/old-docs/SEMANTIC_TREE_DIFF.adoc +765 -0
  96. data/old-docs/STRING_COMPARE.adoc +345 -0
  97. data/old-docs/TMP.adoc +3384 -0
  98. data/old-docs/TREE_DIFF.adoc +1080 -0
  99. data/old-docs/UNDERSTANDING_CANON.adoc +17 -0
  100. data/old-docs/VERBOSE.adoc +482 -0
  101. data/old-docs/VISUALIZATION_MAP.adoc +625 -0
  102. data/old-docs/WHITESPACE_TREATMENT.adoc +1155 -0
  103. data/scripts/analyze_current_state.rb +85 -0
  104. data/scripts/analyze_false_positives.rb +114 -0
  105. data/scripts/analyze_remaining_failures.rb +105 -0
  106. data/scripts/compare_current_failures.rb +95 -0
  107. data/scripts/compare_dom_tree_diff.rb +158 -0
  108. data/scripts/compare_failures.rb +151 -0
  109. data/scripts/debug_attribute_extraction.rb +66 -0
  110. data/scripts/debug_blocks_839.rb +115 -0
  111. data/scripts/debug_meta_matching.rb +52 -0
  112. data/scripts/debug_p_matching.rb +192 -0
  113. data/scripts/debug_signature_matching.rb +118 -0
  114. data/scripts/debug_sourcecode_124.rb +32 -0
  115. data/scripts/debug_whitespace_sensitive.rb +192 -0
  116. data/scripts/extract_false_positives.rb +138 -0
  117. data/scripts/find_actual_false_positives.rb +125 -0
  118. data/scripts/investigate_all_false_positives.rb +161 -0
  119. data/scripts/investigate_batch1.rb +127 -0
  120. data/scripts/investigate_classification.rb +150 -0
  121. data/scripts/investigate_classification_detailed.rb +190 -0
  122. data/scripts/investigate_common_failures.rb +342 -0
  123. data/scripts/investigate_false_negative.rb +80 -0
  124. data/scripts/investigate_false_positive.rb +83 -0
  125. data/scripts/investigate_false_positives.rb +227 -0
  126. data/scripts/investigate_false_positives_batch.rb +163 -0
  127. data/scripts/investigate_mixed_content.rb +125 -0
  128. data/scripts/investigate_remaining_16.rb +214 -0
  129. data/scripts/run_single_test.rb +29 -0
  130. data/scripts/test_all_false_positives.rb +95 -0
  131. data/scripts/test_attribute_details.rb +61 -0
  132. data/scripts/test_both_algorithms.rb +49 -0
  133. data/scripts/test_both_simple.rb +49 -0
  134. data/scripts/test_enhanced_semantic_output.rb +125 -0
  135. data/scripts/test_readme_examples.rb +131 -0
  136. data/scripts/test_semantic_tree_diff.rb +99 -0
  137. data/scripts/test_semantic_ux_improvements.rb +135 -0
  138. data/scripts/test_single_false_positive.rb +119 -0
  139. data/scripts/test_size_limits.rb +99 -0
  140. data/test_html_1.html +21 -0
  141. data/test_html_2.html +21 -0
  142. data/test_nokogiri.rb +33 -0
  143. data/test_normalize.rb +45 -0
  144. metadata +123 -2
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Comparison
5
+ module MatchOptions
6
+ # Base class for match option resolvers
7
+ # Provides common resolve logic with format-specific customization
8
+ class BaseResolver
9
+ class << self
10
+ # Resolve match options with precedence handling
11
+ #
12
+ # Precedence order (highest to lowest):
13
+ # 1. Explicit match parameter
14
+ # 2. Profile from match_profile parameter
15
+ # 3. Global configuration
16
+ # 4. Format-specific defaults
17
+ #
18
+ # @param format [Symbol] Format type
19
+ # @param match_profile [Symbol, nil] Profile name
20
+ # @param match [Hash, nil] Explicit options per dimension
21
+ # @param preprocessing [Symbol, nil] Preprocessing option
22
+ # @param global_profile [Symbol, nil] Global configured profile
23
+ # @param global_options [Hash, nil] Global configured options
24
+ # @return [Hash] Resolved options for all dimensions
25
+ def resolve(format:, match_profile: nil, match: nil, preprocessing: nil,
26
+ global_profile: nil, global_options: nil)
27
+ # Start with format-specific defaults
28
+ options = format_defaults(format).dup
29
+
30
+ # Apply global profile if specified
31
+ if global_profile
32
+ profile_opts = get_profile_options(global_profile)
33
+ options.merge!(profile_opts)
34
+ end
35
+
36
+ # Apply global options if specified
37
+ if global_options
38
+ validate_match_options!(global_options)
39
+ options.merge!(global_options)
40
+ end
41
+
42
+ # Apply per-call profile if specified (overrides global)
43
+ if match_profile
44
+ profile_opts = get_profile_options(match_profile)
45
+ options.merge!(profile_opts)
46
+ end
47
+
48
+ # Apply per-call preprocessing if specified (overrides profile)
49
+ if preprocessing
50
+ validate_preprocessing!(preprocessing)
51
+ options[:preprocessing] = preprocessing
52
+ end
53
+
54
+ # Apply per-call explicit options if specified (highest priority)
55
+ if match
56
+ validate_match_options!(match)
57
+ options.merge!(match)
58
+ end
59
+
60
+ options
61
+ end
62
+
63
+ # Get format-specific default options
64
+ # Subclasses should override this
65
+ #
66
+ # @param format [Symbol] Format type
67
+ # @return [Hash] Default options for the format
68
+ def format_defaults(format)
69
+ raise NotImplementedError,
70
+ "#{self.class} must implement #format_defaults"
71
+ end
72
+
73
+ # Get options for a named profile
74
+ # Subclasses should override this
75
+ #
76
+ # @param profile [Symbol] Profile name
77
+ # @return [Hash] Profile options
78
+ # @raise [Canon::Error] If profile is unknown
79
+ def get_profile_options(profile)
80
+ raise NotImplementedError,
81
+ "#{self.class} must implement #get_profile_options"
82
+ end
83
+
84
+ # Get valid match dimensions for this format
85
+ # Subclasses should override this
86
+ #
87
+ # @return [Array<Symbol>] Valid dimensions
88
+ def match_dimensions
89
+ raise NotImplementedError,
90
+ "#{self.class} must implement #match_dimensions"
91
+ end
92
+
93
+ protected
94
+
95
+ # Validate preprocessing option
96
+ #
97
+ # @param preprocessing [Symbol] Preprocessing option
98
+ # @raise [Canon::Error] If invalid
99
+ def validate_preprocessing!(preprocessing)
100
+ unless MatchOptions::PREPROCESSING_OPTIONS.include?(preprocessing)
101
+ raise Canon::Error,
102
+ "Unknown preprocessing option: #{preprocessing}. " \
103
+ "Valid options: #{MatchOptions::PREPROCESSING_OPTIONS.join(', ')}"
104
+ end
105
+ end
106
+
107
+ # Validate match options
108
+ #
109
+ # @param match_options [Hash] Options to validate
110
+ # @raise [Canon::Error] If invalid dimension or behavior
111
+ def validate_match_options!(match_options)
112
+ # Special options that don't need validation as dimensions
113
+ special_options = %i[
114
+ preprocessing
115
+ semantic_diff
116
+ similarity_threshold
117
+ hash_matching
118
+ similarity_matching
119
+ propagation
120
+ ]
121
+
122
+ match_options.each do |dimension, behavior|
123
+ # Skip special options (validated elsewhere or passed through)
124
+ next if special_options.include?(dimension)
125
+
126
+ unless match_dimensions.include?(dimension)
127
+ raise Canon::Error,
128
+ "Unknown match dimension: #{dimension}. " \
129
+ "Valid dimensions: #{match_dimensions.join(', ')}"
130
+ end
131
+
132
+ unless MatchOptions::MATCH_BEHAVIORS.include?(behavior)
133
+ raise Canon::Error,
134
+ "Unknown match behavior: #{behavior} for #{dimension}. " \
135
+ "Valid behaviors: #{MatchOptions::MATCH_BEHAVIORS.join(', ')}"
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_resolver"
4
+
5
+ module Canon
6
+ module Comparison
7
+ module MatchOptions
8
+ # JSON-specific match options resolver
9
+ class JsonResolver < BaseResolver
10
+ # Format defaults for JSON
11
+ FORMAT_DEFAULTS = {
12
+ json: {
13
+ preprocessing: :none,
14
+ text_content: :strict,
15
+ structural_whitespace: :ignore,
16
+ key_order: :ignore,
17
+ },
18
+ }.freeze
19
+
20
+ # Predefined match profiles for JSON
21
+ MATCH_PROFILES = {
22
+ # Strict: Match exactly
23
+ strict: {
24
+ preprocessing: :none,
25
+ text_content: :strict,
26
+ structural_whitespace: :strict,
27
+ key_order: :strict,
28
+ },
29
+
30
+ # Spec-friendly: Formatting and order don't matter
31
+ spec_friendly: {
32
+ preprocessing: :normalize,
33
+ text_content: :strict,
34
+ structural_whitespace: :ignore,
35
+ key_order: :ignore,
36
+ },
37
+
38
+ # Content-only: Only values matter
39
+ content_only: {
40
+ preprocessing: :normalize,
41
+ text_content: :normalize,
42
+ structural_whitespace: :ignore,
43
+ key_order: :ignore,
44
+ },
45
+ }.freeze
46
+
47
+ class << self
48
+ # Matching dimensions for JSON (collectively exhaustive)
49
+ def match_dimensions
50
+ %i[
51
+ text_content
52
+ structural_whitespace
53
+ key_order
54
+ ].freeze
55
+ end
56
+
57
+ # Get format-specific default options
58
+ #
59
+ # @param format [Symbol] Format type (:json)
60
+ # @return [Hash] Default options for the format
61
+ def format_defaults(format)
62
+ FORMAT_DEFAULTS[format]&.dup || FORMAT_DEFAULTS[:json].dup
63
+ end
64
+
65
+ # Get options for a named profile
66
+ #
67
+ # @param profile [Symbol] Profile name
68
+ # @return [Hash] Profile options
69
+ # @raise [Canon::Error] If profile is unknown
70
+ def get_profile_options(profile)
71
+ unless MATCH_PROFILES.key?(profile)
72
+ raise Canon::Error,
73
+ "Unknown match profile: #{profile}. " \
74
+ "Valid profiles: #{MATCH_PROFILES.keys.join(', ')}"
75
+ end
76
+ MATCH_PROFILES[profile].dup
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_resolver"
4
+
5
+ module Canon
6
+ module Comparison
7
+ module MatchOptions
8
+ # XML/HTML-specific match options resolver
9
+ class XmlResolver < BaseResolver
10
+ # Format-specific defaults for XML/HTML
11
+ FORMAT_DEFAULTS = {
12
+ html: {
13
+ preprocessing: :rendered,
14
+ text_content: :normalize,
15
+ structural_whitespace: :normalize,
16
+ attribute_presence: :strict,
17
+ attribute_order: :ignore,
18
+ attribute_values: :strict,
19
+ element_position: :ignore,
20
+ comments: :ignore,
21
+ },
22
+ xml: {
23
+ preprocessing: :none,
24
+ text_content: :strict,
25
+ structural_whitespace: :strict,
26
+ attribute_presence: :strict,
27
+ attribute_order: :ignore,
28
+ attribute_values: :strict,
29
+ element_position: :strict,
30
+ comments: :strict,
31
+ },
32
+ }.freeze
33
+
34
+ # Predefined match profiles for XML/HTML
35
+ MATCH_PROFILES = {
36
+ # Strict: Match exactly as written in source (XML default)
37
+ strict: {
38
+ preprocessing: :none,
39
+ text_content: :strict,
40
+ structural_whitespace: :strict,
41
+ attribute_presence: :strict,
42
+ attribute_order: :strict,
43
+ attribute_values: :strict,
44
+ element_position: :strict,
45
+ comments: :strict,
46
+ },
47
+
48
+ # Rendered: Match rendered output (HTML default)
49
+ # Mimics CSS whitespace collapsing
50
+ rendered: {
51
+ preprocessing: :none,
52
+ text_content: :normalize,
53
+ structural_whitespace: :normalize,
54
+ attribute_presence: :strict,
55
+ attribute_order: :strict,
56
+ attribute_values: :strict,
57
+ element_position: :strict,
58
+ comments: :ignore,
59
+ },
60
+
61
+ # HTML4: Match HTML4 rendered output
62
+ # HTML4 rendering normalizes attribute whitespace
63
+ html4: {
64
+ preprocessing: :rendered,
65
+ text_content: :normalize,
66
+ structural_whitespace: :normalize,
67
+ attribute_presence: :strict,
68
+ attribute_order: :strict,
69
+ attribute_values: :normalize,
70
+ element_position: :ignore,
71
+ comments: :ignore,
72
+ },
73
+
74
+ # HTML5: Match HTML5 rendered output (same as rendered)
75
+ html5: {
76
+ preprocessing: :rendered,
77
+ text_content: :normalize,
78
+ structural_whitespace: :normalize,
79
+ attribute_presence: :strict,
80
+ attribute_order: :strict,
81
+ attribute_values: :strict,
82
+ element_position: :ignore,
83
+ comments: :ignore,
84
+ },
85
+
86
+ # Spec-friendly: Formatting doesn't matter
87
+ # Uses :rendered preprocessing for HTML which normalizes via to_html
88
+ spec_friendly: {
89
+ preprocessing: :rendered,
90
+ text_content: :normalize,
91
+ structural_whitespace: :ignore,
92
+ attribute_presence: :strict,
93
+ attribute_order: :ignore,
94
+ attribute_values: :normalize,
95
+ element_position: :ignore,
96
+ comments: :ignore,
97
+ },
98
+
99
+ # Content-only: Only content matters
100
+ content_only: {
101
+ preprocessing: :c14n,
102
+ text_content: :normalize,
103
+ structural_whitespace: :ignore,
104
+ attribute_presence: :strict,
105
+ attribute_order: :ignore,
106
+ attribute_values: :normalize,
107
+ element_position: :ignore,
108
+ comments: :ignore,
109
+ },
110
+ }.freeze
111
+
112
+ class << self
113
+ # Matching dimensions for XML/HTML (collectively exhaustive)
114
+ def match_dimensions
115
+ %i[
116
+ text_content
117
+ structural_whitespace
118
+ attribute_presence
119
+ attribute_order
120
+ attribute_values
121
+ element_position
122
+ comments
123
+ ].freeze
124
+ end
125
+
126
+ # Get format-specific default options
127
+ #
128
+ # @param format [Symbol] Format type (:xml or :html)
129
+ # @return [Hash] Default options for the format
130
+ def format_defaults(format)
131
+ FORMAT_DEFAULTS[format]&.dup || FORMAT_DEFAULTS[:xml].dup
132
+ end
133
+
134
+ # Get options for a named profile
135
+ #
136
+ # @param profile [Symbol] Profile name
137
+ # @return [Hash] Profile options
138
+ # @raise [Canon::Error] If profile is unknown
139
+ def get_profile_options(profile)
140
+ unless MATCH_PROFILES.key?(profile)
141
+ raise Canon::Error,
142
+ "Unknown match profile: #{profile}. " \
143
+ "Valid profiles: #{MATCH_PROFILES.keys.join(', ')}"
144
+ end
145
+ MATCH_PROFILES[profile].dup
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_resolver"
4
+
5
+ module Canon
6
+ module Comparison
7
+ module MatchOptions
8
+ # YAML-specific match options resolver
9
+ class YamlResolver < BaseResolver
10
+ # Format defaults for YAML
11
+ FORMAT_DEFAULTS = {
12
+ yaml: {
13
+ preprocessing: :none,
14
+ text_content: :strict,
15
+ structural_whitespace: :ignore,
16
+ key_order: :ignore,
17
+ comments: :ignore,
18
+ },
19
+ }.freeze
20
+
21
+ # Predefined match profiles for YAML
22
+ MATCH_PROFILES = {
23
+ # Strict: Match exactly
24
+ strict: {
25
+ preprocessing: :none,
26
+ text_content: :strict,
27
+ structural_whitespace: :strict,
28
+ key_order: :strict,
29
+ comments: :strict,
30
+ },
31
+
32
+ # Spec-friendly: Formatting and order don't matter
33
+ spec_friendly: {
34
+ preprocessing: :normalize,
35
+ text_content: :strict,
36
+ structural_whitespace: :ignore,
37
+ key_order: :ignore,
38
+ comments: :ignore,
39
+ },
40
+
41
+ # Content-only: Only values matter
42
+ content_only: {
43
+ preprocessing: :normalize,
44
+ text_content: :normalize,
45
+ structural_whitespace: :ignore,
46
+ key_order: :ignore,
47
+ comments: :ignore,
48
+ },
49
+ }.freeze
50
+
51
+ class << self
52
+ # Matching dimensions for YAML (collectively exhaustive)
53
+ def match_dimensions
54
+ %i[
55
+ text_content
56
+ structural_whitespace
57
+ key_order
58
+ comments
59
+ ].freeze
60
+ end
61
+
62
+ # Get format-specific default options
63
+ #
64
+ # @param format [Symbol] Format type (:yaml)
65
+ # @return [Hash] Default options for the format
66
+ def format_defaults(format)
67
+ FORMAT_DEFAULTS[format]&.dup || FORMAT_DEFAULTS[:yaml].dup
68
+ end
69
+
70
+ # Get options for a named profile
71
+ #
72
+ # @param profile [Symbol] Profile name
73
+ # @return [Hash] Profile options
74
+ # @raise [Canon::Error] If profile is unknown
75
+ def get_profile_options(profile)
76
+ unless MATCH_PROFILES.key?(profile)
77
+ raise Canon::Error,
78
+ "Unknown match profile: #{profile}. " \
79
+ "Valid profiles: #{MATCH_PROFILES.keys.join(', ')}"
80
+ end
81
+ MATCH_PROFILES[profile].dup
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end