canon 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +50 -26
- data/README.adoc +8 -3
- data/docs/advanced/diff-pipeline.adoc +36 -9
- data/docs/features/diff-formatting/colors-and-symbols.adoc +82 -0
- data/docs/features/diff-formatting/index.adoc +12 -0
- data/docs/features/diff-formatting/themes.adoc +353 -0
- data/docs/features/environment-configuration/index.adoc +23 -0
- data/docs/internals/diff-char-range-pipeline.adoc +249 -0
- data/docs/internals/diffnode-enrichment.adoc +1 -0
- data/docs/internals/index.adoc +52 -4
- data/docs/reference/environment-variables.adoc +6 -0
- data/docs/understanding/architecture.adoc +5 -0
- data/examples/show_themes.rb +217 -0
- data/lib/canon/comparison/comparison_result.rb +9 -4
- data/lib/canon/config/env_schema.rb +3 -1
- data/lib/canon/config.rb +11 -0
- data/lib/canon/diff/diff_block.rb +7 -0
- data/lib/canon/diff/diff_block_builder.rb +2 -2
- data/lib/canon/diff/diff_char_range.rb +140 -0
- data/lib/canon/diff/diff_line.rb +42 -4
- data/lib/canon/diff/diff_line_builder.rb +907 -0
- data/lib/canon/diff/diff_node.rb +5 -1
- data/lib/canon/diff/diff_node_enricher.rb +1418 -0
- data/lib/canon/diff/diff_node_mapper.rb +54 -0
- data/lib/canon/diff/source_locator.rb +105 -0
- data/lib/canon/diff/text_decomposer.rb +103 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +264 -24
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +35 -20
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +36 -19
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +33 -19
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +583 -98
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +36 -19
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +62 -13
- data/lib/canon/diff_formatter/by_object/json_formatter.rb +59 -24
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +74 -34
- data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +4 -5
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +1 -1
- data/lib/canon/diff_formatter/legend.rb +4 -2
- data/lib/canon/diff_formatter/theme.rb +864 -0
- data/lib/canon/diff_formatter.rb +11 -6
- data/lib/canon/tree_diff/matchers/hash_matcher.rb +16 -1
- data/lib/canon/tree_diff/matchers/similarity_matcher.rb +10 -0
- data/lib/canon/tree_diff/operations/operation_detector.rb +5 -1
- data/lib/canon/tree_diff/tree_diff_integrator.rb +1 -1
- data/lib/canon/version.rb +1 -1
- metadata +11 -2
data/docs/internals/index.adoc
CHANGED
|
@@ -24,6 +24,9 @@ These documents are intended for:
|
|
|
24
24
|
link:diffnode-enrichment[**DiffNode Enrichment**]::
|
|
25
25
|
How DiffNode objects carry location, serialized content, and attribute metadata through the comparison pipeline. Covers PathBuilder (canonical paths with ordinal indices), NodeSerializer (library-agnostic serialization), and how Layer 2 algorithms populate metadata for Layer 4 rendering.
|
|
26
26
|
|
|
27
|
+
link:diff-char-range-pipeline[**DiffCharRange Pipeline**]::
|
|
28
|
+
How enriched DiffNodes are processed into character-level diff ranges for precise rendering. Covers DiffNodeEnricher (Phase 1: locating serialized content and decomposing changes), DiffLineBuilder (Phase 2: assembling DiffLines from enriched DiffNodes), DiffCharRange (character position value objects), TextDecomposer (prefix/suffix decomposition), and SourceLocator (finding content positions in source text).
|
|
29
|
+
|
|
27
30
|
link:../advanced/dom-diff-internals[**DOM Diff Internals**]::
|
|
28
31
|
Deep dive into Canon's default DOM diff algorithm: position-based matching, operation detection, and diff generation.
|
|
29
32
|
|
|
@@ -80,17 +83,23 @@ graph LR
|
|
|
80
83
|
B -->|Enriches| D[DiffNode.path]
|
|
81
84
|
C -->|Enriches| E[DiffNode.serialized_before/after]
|
|
82
85
|
C -->|Enriches| F[DiffNode.attributes_before/after]
|
|
83
|
-
D --> G[
|
|
86
|
+
D --> G[Phase 1: DiffNodeEnricher]
|
|
84
87
|
E --> G
|
|
85
88
|
F --> G
|
|
86
|
-
G
|
|
89
|
+
G -->|Enriches| H[DiffNode.char_ranges]
|
|
90
|
+
G -->|Enriches| I[DiffNode.line_range_before/after]
|
|
91
|
+
H --> J[Phase 2: DiffLineBuilder]
|
|
92
|
+
I --> J
|
|
93
|
+
J --> K[DiffLine with DiffCharRange[]]
|
|
94
|
+
K --> L[Layer 4: Rendering]
|
|
87
95
|
|
|
88
96
|
style A fill:#fff4e1
|
|
89
97
|
style D fill:#e1f5ff
|
|
90
|
-
style G fill:#
|
|
98
|
+
style G fill:#ffe1f5
|
|
99
|
+
style K fill:#e1ffe1
|
|
91
100
|
----
|
|
92
101
|
|
|
93
|
-
Key insight: Metadata is captured at diff creation time (Layer 2)
|
|
102
|
+
Key insight: Metadata is captured at diff creation time (Layer 2), enriched with character positions (Phase 1), assembled into display lines (Phase 2), and rendered without further computation (Layer 4).
|
|
94
103
|
|
|
95
104
|
== Data Structures
|
|
96
105
|
|
|
@@ -112,11 +121,50 @@ class DiffNode
|
|
|
112
121
|
attr_accessor :serialized_after # Serialized "after" content
|
|
113
122
|
attr_accessor :attributes_before # Normalized "before" attributes
|
|
114
123
|
attr_accessor :attributes_after # Normalized "after" attributes
|
|
124
|
+
|
|
125
|
+
# Character-level enrichment (Phase 1 enrichment pipeline)
|
|
126
|
+
attr_accessor :char_ranges # Array<DiffCharRange>
|
|
127
|
+
attr_accessor :line_range_before # [start_line, end_line] in text1
|
|
128
|
+
attr_accessor :line_range_after # [start_line, end_line] in text2
|
|
115
129
|
end
|
|
116
130
|
----
|
|
117
131
|
|
|
118
132
|
See link:diffnode-enrichment[DiffNode Enrichment] for details.
|
|
119
133
|
|
|
134
|
+
=== DiffLine
|
|
135
|
+
|
|
136
|
+
Represents a single line in the diff output, linked to semantic DiffNode and character-level DiffCharRanges:
|
|
137
|
+
|
|
138
|
+
[source,ruby]
|
|
139
|
+
----
|
|
140
|
+
class DiffLine
|
|
141
|
+
attr_reader :line_number, :new_position, :content, :type, :diff_node,
|
|
142
|
+
:char_ranges, :new_char_ranges, :new_content
|
|
143
|
+
attr_accessor :old_content, :formatting
|
|
144
|
+
|
|
145
|
+
# type: :unchanged, :added, :removed, :changed
|
|
146
|
+
# char_ranges: text1 side DiffCharRange[]
|
|
147
|
+
# new_char_ranges: text2 side DiffCharRange[]
|
|
148
|
+
# new_content: text2 line text (for :changed lines)
|
|
149
|
+
end
|
|
150
|
+
----
|
|
151
|
+
|
|
152
|
+
=== DiffCharRange
|
|
153
|
+
|
|
154
|
+
Character-level position within a source line, linked to a DiffNode:
|
|
155
|
+
|
|
156
|
+
[source,ruby]
|
|
157
|
+
----
|
|
158
|
+
class DiffCharRange
|
|
159
|
+
attr_reader :line_number, :start_col, :end_col, :side, :status, :role,
|
|
160
|
+
:diff_node
|
|
161
|
+
|
|
162
|
+
# side: :old (text1) or :new (text2)
|
|
163
|
+
# status: :unchanged, :removed, :added, :changed_old, :changed_new
|
|
164
|
+
# role: :before, :changed, :after
|
|
165
|
+
end
|
|
166
|
+
----
|
|
167
|
+
|
|
120
168
|
=== TreeNode (Semantic Diff)
|
|
121
169
|
|
|
122
170
|
Canonical node representation from semantic diff:
|
|
@@ -142,6 +142,12 @@ export CANON_JSON_FORMAT_PREPROCESSING=normalize
|
|
|
142
142
|
|`10000`
|
|
143
143
|
|Maximum number of diff output lines
|
|
144
144
|
|All formats
|
|
145
|
+
|
|
146
|
+
|`CANON_DIFF_THEME`
|
|
147
|
+
|symbol
|
|
148
|
+
|`:dark`
|
|
149
|
+
|Diff display theme: `light`, `dark`, `retro`, `claude`, `cyberpunk`
|
|
150
|
+
|All formats
|
|
145
151
|
|===
|
|
146
152
|
|
|
147
153
|
=== Match Configuration Variables
|
|
@@ -889,6 +889,11 @@ class DiffNode
|
|
|
889
889
|
attr_accessor :serialized_after # Serialized "after" content
|
|
890
890
|
attr_accessor :attributes_before # Normalized "before" attributes
|
|
891
891
|
attr_accessor :attributes_after # Normalized "after" attributes
|
|
892
|
+
|
|
893
|
+
# Character-level enrichment (populated by DiffNodeEnricher)
|
|
894
|
+
attr_accessor :char_ranges # Array<DiffCharRange> char positions
|
|
895
|
+
attr_accessor :line_range_before # [start_line, end_line] in text1
|
|
896
|
+
attr_accessor :line_range_after # [start_line, end_line] in text2
|
|
892
897
|
end
|
|
893
898
|
----
|
|
894
899
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example script demonstrating all available diff display themes.
|
|
5
|
+
# Run with: bundle exec ruby examples/show_themes.rb
|
|
6
|
+
#
|
|
7
|
+
# This script shows how the same XML diff renders under each theme:
|
|
8
|
+
# - :light - Light terminal backgrounds
|
|
9
|
+
# - :dark - Dark terminal backgrounds (default)
|
|
10
|
+
# - :retro - Amber CRT phosphor look
|
|
11
|
+
# - :claude - Claude Code diff style (red/green backgrounds)
|
|
12
|
+
# - :cyberpunk - Neon on black, high contrast, futuristic
|
|
13
|
+
|
|
14
|
+
require "bundler/setup"
|
|
15
|
+
require "canon"
|
|
16
|
+
require "canon/diff_formatter"
|
|
17
|
+
|
|
18
|
+
# Sample XML documents with meaningful differences
|
|
19
|
+
# These documents have:
|
|
20
|
+
# - A deleted element (the <old-item>)
|
|
21
|
+
# - An added element (the <new-item>)
|
|
22
|
+
# - A changed attribute (version="1.0" -> version="2.0")
|
|
23
|
+
# - A changed text content ("Old Text" -> "New Text")
|
|
24
|
+
# - Whitespace-only change (formatting)
|
|
25
|
+
# - An informative comment difference
|
|
26
|
+
|
|
27
|
+
XML1 = <<~XML
|
|
28
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
29
|
+
<document version="1.0">
|
|
30
|
+
<header>
|
|
31
|
+
<!-- Original comment -->
|
|
32
|
+
<title>Document Title</title>
|
|
33
|
+
</header>
|
|
34
|
+
<content>
|
|
35
|
+
<old-item name="thing1">Old Text</old-item>
|
|
36
|
+
<common-item>Shared content</common-item>
|
|
37
|
+
<common-item>More shared content</common-item>
|
|
38
|
+
</content>
|
|
39
|
+
<footer>
|
|
40
|
+
<note>Footer note</note>
|
|
41
|
+
</footer>
|
|
42
|
+
</document>
|
|
43
|
+
XML
|
|
44
|
+
|
|
45
|
+
XML2 = <<~XML
|
|
46
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
47
|
+
<document version="2.0">
|
|
48
|
+
<header>
|
|
49
|
+
<!-- Updated comment -->
|
|
50
|
+
<title>Document Title</title>
|
|
51
|
+
</header>
|
|
52
|
+
<content>
|
|
53
|
+
<new-item name="thing2">New Text</new-item>
|
|
54
|
+
<common-item>Shared content</common-item>
|
|
55
|
+
<common-item>More shared content</common-item>
|
|
56
|
+
<extra-item>Additional item</extra-item>
|
|
57
|
+
</content>
|
|
58
|
+
<footer>
|
|
59
|
+
<note>Footer note</note>
|
|
60
|
+
</footer>
|
|
61
|
+
</document>
|
|
62
|
+
XML
|
|
63
|
+
|
|
64
|
+
# Helper to run diff with a specific theme and display mode
|
|
65
|
+
def run_diff_with_theme(theme_name, xml1, xml2, display_mode: :separate)
|
|
66
|
+
Canon::Config.reset!
|
|
67
|
+
Canon::Config.configure do |config|
|
|
68
|
+
config.xml.diff.theme = theme_name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Use semantic diff to get all differences reported, even when documents
|
|
72
|
+
# are not equivalent. DOM diff stops at the first difference.
|
|
73
|
+
result = Canon::Comparison.equivalent?(xml1, xml2, diff_algorithm: :semantic,
|
|
74
|
+
verbose: true)
|
|
75
|
+
|
|
76
|
+
formatter = Canon::DiffFormatter.new(
|
|
77
|
+
use_color: true,
|
|
78
|
+
mode: :by_line,
|
|
79
|
+
show_diffs: :all,
|
|
80
|
+
diff_mode: display_mode,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
formatter.format(result, :xml, doc1: xml1, doc2: xml2)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Helper to print section header
|
|
87
|
+
def print_header(title)
|
|
88
|
+
puts
|
|
89
|
+
puts "=" * 70
|
|
90
|
+
puts title
|
|
91
|
+
puts "=" * 70
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Helper to strip ANSI codes for plain text display
|
|
95
|
+
def strip_ansi(text)
|
|
96
|
+
text.gsub(/\e\[[0-9;]*m/, "")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Main demonstration
|
|
100
|
+
puts
|
|
101
|
+
puts "DIFF DISPLAY THEME DEMONSTRATION"
|
|
102
|
+
puts "=" * 70
|
|
103
|
+
puts
|
|
104
|
+
puts "This script demonstrates how the same XML diff renders under"
|
|
105
|
+
puts "each of the 5 available themes. Each theme has different:"
|
|
106
|
+
puts " - Color schemes (light vs dark backgrounds)"
|
|
107
|
+
puts " - Marker styles ([, ], <, >, -, +)"
|
|
108
|
+
puts " - Visual emphasis (backgrounds vs foreground colors)"
|
|
109
|
+
puts
|
|
110
|
+
puts "Sample documents have these differences:"
|
|
111
|
+
puts " - Attribute change: version=\"1.0\" -> version=\"2.0\""
|
|
112
|
+
puts " - Text change: <old-item> -> <new-item>"
|
|
113
|
+
puts " - Content change: \"Old Text\" -> \"New Text\""
|
|
114
|
+
puts " - Addition: <extra-item> added"
|
|
115
|
+
puts " - Comment change: informative difference"
|
|
116
|
+
puts
|
|
117
|
+
|
|
118
|
+
# Demonstrate each theme in both display modes
|
|
119
|
+
themes = {
|
|
120
|
+
light: "Light Theme (light backgrounds, red/green markers with light bg)",
|
|
121
|
+
dark: "Dark Theme (dark backgrounds, saturated red/green foregrounds)",
|
|
122
|
+
retro: "Retro Theme (amber CRT phosphor, monochrome amber with inverse video)",
|
|
123
|
+
claude: "Claude Theme (red/green backgrounds + white text, maximum visual pop)",
|
|
124
|
+
cyberpunk: "Cyberpunk Theme (neon magenta/cyan on black, electric, futuristic)",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
display_modes = {
|
|
128
|
+
separate: "Separate lines (- / + on separate lines)",
|
|
129
|
+
inline: "Inline mode (* on same line, old→new)",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
themes.each do |theme_name, description|
|
|
133
|
+
print_header("#{theme_name.upcase} THEME: #{description}")
|
|
134
|
+
|
|
135
|
+
puts
|
|
136
|
+
puts "Theme configuration:"
|
|
137
|
+
puts " Canon::Config.xml.diff.theme = :#{theme_name}"
|
|
138
|
+
|
|
139
|
+
display_modes.each do |mode, mode_description|
|
|
140
|
+
puts
|
|
141
|
+
puts "-" * 70
|
|
142
|
+
puts "DISPLAY MODE: #{mode.upcase} - #{mode_description}"
|
|
143
|
+
puts "-" * 70
|
|
144
|
+
puts "COLOR OUTPUT (with ANSI escape sequences):"
|
|
145
|
+
|
|
146
|
+
output = run_diff_with_theme(theme_name, XML1, XML2, display_mode: mode)
|
|
147
|
+
puts output
|
|
148
|
+
|
|
149
|
+
puts
|
|
150
|
+
puts "-" * 70
|
|
151
|
+
puts "PLAIN TEXT OUTPUT (without ANSI codes):"
|
|
152
|
+
puts strip_ansi(output)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Summary table
|
|
157
|
+
print_header("THEME SUMMARY")
|
|
158
|
+
puts
|
|
159
|
+
puts "| Theme | Best For | Key Characteristics |"
|
|
160
|
+
puts "|------------|-------------------------------|--------------------------------|"
|
|
161
|
+
puts "| :light | Light terminal backgrounds | Light marker backgrounds |"
|
|
162
|
+
puts "| :dark | Dark terminals (default) | Saturated foreground colors |"
|
|
163
|
+
puts "| :retro | Low blue light / accessibility| Amber monochrome + inverse |"
|
|
164
|
+
puts "| :claude | Maximum visual pop | Red/green backgrounds |"
|
|
165
|
+
puts "| :cyberpunk | Neon / futuristic terminals | Magenta/cyan neon on black |"
|
|
166
|
+
puts
|
|
167
|
+
|
|
168
|
+
# How to use programmatically
|
|
169
|
+
print_header("HOW TO USE IN CODE")
|
|
170
|
+
puts
|
|
171
|
+
puts "# Set theme via configuration:"
|
|
172
|
+
puts "Canon::Config.configure do |config|"
|
|
173
|
+
puts " config.xml.diff.theme = :claude"
|
|
174
|
+
puts "end"
|
|
175
|
+
puts
|
|
176
|
+
puts "# Or via ENV variable:"
|
|
177
|
+
puts "ENV['CANON_DIFF_THEME'] = 'claude'"
|
|
178
|
+
puts
|
|
179
|
+
puts "# Or for a single diff, pass theme to formatter:"
|
|
180
|
+
puts "formatter = Canon::DiffFormatter.new("
|
|
181
|
+
puts " use_color: true,"
|
|
182
|
+
puts " mode: :by_line,"
|
|
183
|
+
puts " show_diffs: :all,"
|
|
184
|
+
puts " theme: :retro # if supported by the formatter"
|
|
185
|
+
puts ")"
|
|
186
|
+
puts
|
|
187
|
+
|
|
188
|
+
# Theme inheritance example
|
|
189
|
+
print_header("THEME INHERITANCE (advanced)")
|
|
190
|
+
puts
|
|
191
|
+
puts "# Create custom theme inheriting from :dark with overrides:"
|
|
192
|
+
puts "Canon::Config.configure do |config|"
|
|
193
|
+
puts " config.xml.diff.theme_inheritance = {"
|
|
194
|
+
puts " base: :dark,"
|
|
195
|
+
puts " overrides: {"
|
|
196
|
+
puts " diff: {"
|
|
197
|
+
puts " removed: { content: { bg: :light_red } }"
|
|
198
|
+
puts " }"
|
|
199
|
+
puts " }"
|
|
200
|
+
puts " }"
|
|
201
|
+
puts "end"
|
|
202
|
+
puts
|
|
203
|
+
|
|
204
|
+
# Note about Rainbow gem limitations
|
|
205
|
+
print_header("NOTE: RAINBOW GEM LIMITATIONS")
|
|
206
|
+
puts
|
|
207
|
+
puts "The Rainbow gem (used for terminal colors) doesn't support"
|
|
208
|
+
puts ":bright_black or :bright_white in standard 16-color mode."
|
|
209
|
+
puts "Themes substitute compatible colors:"
|
|
210
|
+
puts " - DARK theme uses :white instead of :bright_white"
|
|
211
|
+
puts " - LIGHT theme uses :black instead of :bright_black"
|
|
212
|
+
puts " - Comments use :cyan or :magenta instead of :bright_black"
|
|
213
|
+
puts
|
|
214
|
+
|
|
215
|
+
puts "=" * 70
|
|
216
|
+
puts "END OF THEME DEMONSTRATION"
|
|
217
|
+
puts "=" * 70
|
|
@@ -90,9 +90,11 @@ html_version: nil, match_options: nil, algorithm: :dom, original_strings: nil)
|
|
|
90
90
|
# @param context_lines [Integer] Number of context lines to show
|
|
91
91
|
# @param diff_grouping_lines [Integer] Maximum gap for grouping diffs
|
|
92
92
|
# @param show_diffs [Symbol] Which diffs to show (:all, :normative, :informative)
|
|
93
|
+
# @param diff_mode [Symbol] Diff display mode (:separate, :inline)
|
|
94
|
+
# @param legacy_terminal [Boolean] Force legacy mode (no ANSI, separate-line only)
|
|
93
95
|
# @return [String] Formatted diff output
|
|
94
96
|
def diff(use_color: true, context_lines: 3, diff_grouping_lines: nil,
|
|
95
|
-
show_diffs: :all)
|
|
97
|
+
show_diffs: :all, diff_mode: :separate, legacy_terminal: false)
|
|
96
98
|
require_relative "../diff_formatter"
|
|
97
99
|
|
|
98
100
|
formatter = Canon::DiffFormatter.new(
|
|
@@ -101,13 +103,16 @@ show_diffs: :all)
|
|
|
101
103
|
context_lines: context_lines,
|
|
102
104
|
diff_grouping_lines: diff_grouping_lines,
|
|
103
105
|
show_diffs: show_diffs,
|
|
106
|
+
diff_mode: diff_mode,
|
|
107
|
+
legacy_terminal: legacy_terminal,
|
|
104
108
|
)
|
|
105
109
|
|
|
110
|
+
# Pass self (ComparisonResult) so formatter can check equivalent? status
|
|
106
111
|
formatter.format(
|
|
107
|
-
|
|
112
|
+
self,
|
|
108
113
|
@format,
|
|
109
|
-
doc1: @
|
|
110
|
-
doc2: @
|
|
114
|
+
doc1: @preprocessed_strings[0],
|
|
115
|
+
doc2: @preprocessed_strings[1],
|
|
111
116
|
html_version: @html_version,
|
|
112
117
|
)
|
|
113
118
|
end
|
|
@@ -18,6 +18,7 @@ module Canon
|
|
|
18
18
|
show_preprocessed_inputs: :boolean,
|
|
19
19
|
show_line_numbered_inputs: :boolean,
|
|
20
20
|
display_format: :symbol,
|
|
21
|
+
theme: :symbol,
|
|
21
22
|
|
|
22
23
|
# MatchConfig attributes
|
|
23
24
|
profile: :symbol,
|
|
@@ -47,7 +48,8 @@ module Canon
|
|
|
47
48
|
def all_diff_attributes
|
|
48
49
|
%i[mode use_color context_lines grouping_lines show_diffs
|
|
49
50
|
verbose_diff algorithm show_raw_inputs show_preprocessed_inputs
|
|
50
|
-
show_line_numbered_inputs display_format max_file_size max_node_count max_diff_lines
|
|
51
|
+
show_line_numbered_inputs display_format max_file_size max_node_count max_diff_lines
|
|
52
|
+
theme]
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def all_match_attributes
|
data/lib/canon/config.rb
CHANGED
|
@@ -262,6 +262,15 @@ module Canon
|
|
|
262
262
|
@resolver.set_programmatic(:algorithm, value)
|
|
263
263
|
end
|
|
264
264
|
|
|
265
|
+
# Theme name (:light, :dark, :retro, :claude)
|
|
266
|
+
def theme
|
|
267
|
+
@resolver.resolve(:theme)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def theme=(value)
|
|
271
|
+
@resolver.set_programmatic(:theme, value)
|
|
272
|
+
end
|
|
273
|
+
|
|
265
274
|
# File size limit in bytes (default 5MB)
|
|
266
275
|
def max_file_size
|
|
267
276
|
@resolver.resolve(:max_file_size)
|
|
@@ -306,6 +315,7 @@ module Canon
|
|
|
306
315
|
max_file_size: max_file_size,
|
|
307
316
|
max_node_count: max_node_count,
|
|
308
317
|
max_diff_lines: max_diff_lines,
|
|
318
|
+
theme: theme,
|
|
309
319
|
}
|
|
310
320
|
end
|
|
311
321
|
|
|
@@ -327,6 +337,7 @@ module Canon
|
|
|
327
337
|
max_file_size: 5_242_880, # 5MB in bytes
|
|
328
338
|
max_node_count: 10_000, # Maximum nodes in tree
|
|
329
339
|
max_diff_lines: 10_000, # Maximum diff output lines
|
|
340
|
+
theme: :dark, # Default theme
|
|
330
341
|
}
|
|
331
342
|
|
|
332
343
|
env = format ? EnvProvider.load_diff_for_format(format) : {}
|
|
@@ -42,6 +42,13 @@ diff_node: nil)
|
|
|
42
42
|
!normative?
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# @return [Boolean] true if all lines in this block are formatting-only
|
|
46
|
+
def formatting?
|
|
47
|
+
return false if diff_lines.empty?
|
|
48
|
+
|
|
49
|
+
diff_lines.all?(&:formatting?)
|
|
50
|
+
end
|
|
51
|
+
|
|
45
52
|
# Check if this block contains a specific type of change
|
|
46
53
|
def includes_type?(type)
|
|
47
54
|
types.include?(type)
|
|
@@ -95,9 +95,9 @@ module Canon
|
|
|
95
95
|
when :normative
|
|
96
96
|
blocks.select(&:normative?)
|
|
97
97
|
when :informative
|
|
98
|
-
blocks.select
|
|
98
|
+
blocks.select { |b| b.informative? && !b.formatting? }
|
|
99
99
|
else # :all
|
|
100
|
-
blocks
|
|
100
|
+
blocks.reject(&:formatting?)
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Diff
|
|
5
|
+
# Represents a character range within a source line, linked to a DiffNode.
|
|
6
|
+
#
|
|
7
|
+
# DiffCharRange is the core abstraction for character-level diff rendering.
|
|
8
|
+
# Each DiffNode produces one or more DiffCharRanges (before-text, changed-text,
|
|
9
|
+
# after-text) that tell the formatter exactly which characters to highlight.
|
|
10
|
+
#
|
|
11
|
+
# The formatter reads DiffCharRanges and applies colors — no computation needed.
|
|
12
|
+
#
|
|
13
|
+
# @example Text change "Hello World" → "Hello Universe"
|
|
14
|
+
# # Before-text (unchanged):
|
|
15
|
+
# DiffCharRange.new(line_number: 0, start_col: 9, end_col: 15,
|
|
16
|
+
# side: :old, status: :unchanged, role: :before, diff_node: dn)
|
|
17
|
+
# # Changed-text (old side):
|
|
18
|
+
# DiffCharRange.new(line_number: 0, start_col: 15, end_col: 20,
|
|
19
|
+
# side: :old, status: :changed_old, role: :changed, diff_node: dn)
|
|
20
|
+
# # Changed-text (new side):
|
|
21
|
+
# DiffCharRange.new(line_number: 0, start_col: 15, end_col: 23,
|
|
22
|
+
# side: :new, status: :changed_new, role: :changed, diff_node: dn)
|
|
23
|
+
class DiffCharRange
|
|
24
|
+
# @return [Integer] 0-based line index in text1 or text2
|
|
25
|
+
attr_reader :line_number
|
|
26
|
+
|
|
27
|
+
# @return [Integer] 0-based column offset (inclusive start)
|
|
28
|
+
attr_reader :start_col
|
|
29
|
+
|
|
30
|
+
# @return [Integer] exclusive end (half-open [start_col, end_col))
|
|
31
|
+
attr_reader :end_col
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] :old (text1) or :new (text2)
|
|
34
|
+
attr_reader :side
|
|
35
|
+
|
|
36
|
+
# @return [Symbol] :unchanged, :removed, :added, :changed_old, :changed_new
|
|
37
|
+
attr_reader :status
|
|
38
|
+
|
|
39
|
+
# @return [Symbol] :before, :changed, :after
|
|
40
|
+
attr_reader :role
|
|
41
|
+
|
|
42
|
+
# @return [DiffNode, nil] the originating DiffNode
|
|
43
|
+
attr_reader :diff_node
|
|
44
|
+
|
|
45
|
+
# @param line_number [Integer] 0-based line index
|
|
46
|
+
# @param start_col [Integer] 0-based column (inclusive)
|
|
47
|
+
# @param end_col [Integer] exclusive end
|
|
48
|
+
# @param side [Symbol] :old or :new
|
|
49
|
+
# @param status [Symbol] :unchanged, :removed, :added, :changed_old, :changed_new
|
|
50
|
+
# @param role [Symbol] :before, :changed, :after
|
|
51
|
+
# @param diff_node [DiffNode, nil] originating DiffNode
|
|
52
|
+
def initialize(line_number:, start_col:, end_col:, side:, status:,
|
|
53
|
+
role: nil, diff_node: nil)
|
|
54
|
+
@line_number = line_number
|
|
55
|
+
@start_col = start_col
|
|
56
|
+
@end_col = end_col
|
|
57
|
+
@side = side
|
|
58
|
+
@status = status
|
|
59
|
+
@role = role
|
|
60
|
+
@diff_node = diff_node
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Boolean] true if this range has zero length
|
|
64
|
+
def empty?
|
|
65
|
+
start_col >= end_col
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Integer] number of characters in this range
|
|
69
|
+
def length
|
|
70
|
+
end_col - start_col
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param line_length [Integer] total length of the line
|
|
74
|
+
# @return [Boolean] true if this range covers the entire line
|
|
75
|
+
def covers_entire_line?(line_length)
|
|
76
|
+
start_col.zero? && end_col >= line_length
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Extract the substring this range covers from a line
|
|
80
|
+
# @param line_text [String] the full line text
|
|
81
|
+
# @return [String] the substring, or "" if out of bounds
|
|
82
|
+
def extract_from(line_text)
|
|
83
|
+
return "" if line_text.nil? || empty?
|
|
84
|
+
|
|
85
|
+
line_text[start_col...end_col] || ""
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] true if this is an old-side (text1) range
|
|
89
|
+
def old_side?
|
|
90
|
+
side == :old
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Boolean] true if this is a new-side (text2) range
|
|
94
|
+
def new_side?
|
|
95
|
+
side == :new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @return [Boolean] true if this range represents unchanged content
|
|
99
|
+
def unchanged?
|
|
100
|
+
status == :unchanged
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [Boolean] true if this range represents a change on the old side
|
|
104
|
+
def changed_old?
|
|
105
|
+
status == :changed_old
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Boolean] true if this range represents a change on the new side
|
|
109
|
+
def changed_new?
|
|
110
|
+
status == :changed_new
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @return [Boolean] true if this range should be highlighted
|
|
114
|
+
def highlighted?
|
|
115
|
+
%i[changed_old changed_new removed added].include?(status)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def to_h
|
|
119
|
+
{
|
|
120
|
+
line_number: line_number,
|
|
121
|
+
start_col: start_col,
|
|
122
|
+
end_col: end_col,
|
|
123
|
+
side: side,
|
|
124
|
+
status: status,
|
|
125
|
+
role: role,
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ==(other)
|
|
130
|
+
other.is_a?(DiffCharRange) &&
|
|
131
|
+
line_number == other.line_number &&
|
|
132
|
+
start_col == other.start_col &&
|
|
133
|
+
end_col == other.end_col &&
|
|
134
|
+
side == other.side &&
|
|
135
|
+
status == other.status &&
|
|
136
|
+
role == other.role
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/canon/diff/diff_line.rb
CHANGED
|
@@ -3,26 +3,61 @@
|
|
|
3
3
|
module Canon
|
|
4
4
|
module Diff
|
|
5
5
|
# Represents a single line in the diff output
|
|
6
|
-
# Links textual representation to semantic DiffNode
|
|
6
|
+
# Links textual representation to semantic DiffNode and DiffCharRanges
|
|
7
7
|
class DiffLine
|
|
8
|
-
attr_reader :line_number, :new_position, :content, :type, :diff_node
|
|
8
|
+
attr_reader :line_number, :new_position, :content, :type, :diff_node,
|
|
9
|
+
:char_ranges, :new_char_ranges, :new_content
|
|
10
|
+
attr_accessor :old_content
|
|
9
11
|
attr_writer :formatting
|
|
10
12
|
|
|
11
13
|
# @param line_number [Integer] The 0-based line number in text1 (old text)
|
|
12
14
|
# @param new_position [Integer, nil] The 0-based line number in text2 (new text),
|
|
13
15
|
# used for :changed lines where old and new positions differ
|
|
14
|
-
# @param content [String] The text content of the line
|
|
16
|
+
# @param content [String] The text content of the line (from text1)
|
|
15
17
|
# @param type [Symbol] The type of line (:unchanged, :added, :removed, :changed)
|
|
16
18
|
# @param diff_node [DiffNode, nil] The semantic diff node this line belongs to
|
|
17
19
|
# @param formatting [Boolean] Whether this is a formatting-only difference
|
|
20
|
+
# @param char_ranges [Array<DiffCharRange>] Character ranges for text1 side
|
|
21
|
+
# @param new_char_ranges [Array<DiffCharRange>] Character ranges for text2 side
|
|
22
|
+
# @param new_content [String, nil] The text2 line content (for :changed lines)
|
|
23
|
+
# @param old_content [String, nil] Deprecated: multi-line old content
|
|
18
24
|
def initialize(line_number:, content:, type:, diff_node: nil,
|
|
19
|
-
formatting: false, new_position: nil
|
|
25
|
+
formatting: false, new_position: nil, old_content: nil,
|
|
26
|
+
char_ranges: nil, new_char_ranges: nil, new_content: nil)
|
|
20
27
|
@line_number = line_number
|
|
21
28
|
@new_position = new_position
|
|
22
29
|
@content = content
|
|
23
30
|
@type = type
|
|
24
31
|
@diff_node = diff_node
|
|
25
32
|
@formatting = formatting
|
|
33
|
+
@old_content = old_content
|
|
34
|
+
@char_ranges = char_ranges || []
|
|
35
|
+
@new_char_ranges = new_char_ranges || []
|
|
36
|
+
@new_content = new_content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add a character range for the text1 (old) side
|
|
40
|
+
# @param char_range [DiffCharRange]
|
|
41
|
+
def add_char_range(char_range)
|
|
42
|
+
@char_ranges << char_range
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Add a character range for the text2 (new) side
|
|
46
|
+
# @param char_range [DiffCharRange]
|
|
47
|
+
def add_new_char_range(char_range)
|
|
48
|
+
@new_char_ranges << char_range
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Boolean] true if this line has any character ranges
|
|
52
|
+
def has_char_ranges?
|
|
53
|
+
!@char_ranges.empty? || !@new_char_ranges.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get character ranges for a specific side
|
|
57
|
+
# @param side [Symbol] :old or :new
|
|
58
|
+
# @return [Array<DiffCharRange>]
|
|
59
|
+
def char_ranges_for_side(side)
|
|
60
|
+
side == :old ? @char_ranges : @new_char_ranges
|
|
26
61
|
end
|
|
27
62
|
|
|
28
63
|
# @return [Boolean] true if this line represents a normative difference
|
|
@@ -77,11 +112,14 @@ module Canon
|
|
|
77
112
|
line_number: line_number,
|
|
78
113
|
new_position: new_position,
|
|
79
114
|
content: content,
|
|
115
|
+
new_content: new_content,
|
|
80
116
|
type: type,
|
|
81
117
|
diff_node: diff_node&.to_h,
|
|
82
118
|
normative: normative?,
|
|
83
119
|
informative: informative?,
|
|
84
120
|
formatting: formatting?,
|
|
121
|
+
char_ranges: @char_ranges.map(&:to_h),
|
|
122
|
+
new_char_ranges: @new_char_ranges.map(&:to_h),
|
|
85
123
|
}
|
|
86
124
|
end
|
|
87
125
|
|