canon 0.1.3 → 0.1.5
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.yml +9 -1
- data/.rubocop_todo.yml +276 -7
- data/README.adoc +203 -138
- data/_config.yml +116 -0
- data/docs/ADVANCED_TOPICS.adoc +20 -0
- data/docs/BASIC_USAGE.adoc +16 -0
- data/docs/CHARACTER_VISUALIZATION.adoc +567 -0
- data/docs/CLI.adoc +493 -0
- data/docs/CUSTOMIZING_BEHAVIOR.adoc +19 -0
- data/docs/DIFF_ARCHITECTURE.adoc +435 -0
- data/docs/DIFF_FORMATTING.adoc +540 -0
- data/docs/FORMATS.adoc +447 -0
- data/docs/INDEX.adoc +222 -0
- data/docs/INPUT_VALIDATION.adoc +477 -0
- data/docs/MATCH_ARCHITECTURE.adoc +463 -0
- data/docs/MATCH_OPTIONS.adoc +719 -0
- data/docs/MODES.adoc +432 -0
- data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +219 -0
- data/docs/OPTIONS.adoc +1387 -0
- data/docs/PREPROCESSING.adoc +491 -0
- data/docs/RSPEC.adoc +605 -0
- data/docs/RUBY_API.adoc +478 -0
- data/docs/SEMANTIC_DIFF_REPORT.adoc +528 -0
- data/docs/UNDERSTANDING_CANON.adoc +17 -0
- data/docs/VERBOSE.adoc +482 -0
- data/exe/canon +7 -0
- data/lib/canon/cli.rb +179 -0
- data/lib/canon/commands/diff_command.rb +195 -0
- data/lib/canon/commands/format_command.rb +113 -0
- data/lib/canon/comparison/base_comparator.rb +39 -0
- data/lib/canon/comparison/comparison_result.rb +79 -0
- data/lib/canon/comparison/html_comparator.rb +410 -0
- data/lib/canon/comparison/json_comparator.rb +212 -0
- data/lib/canon/comparison/match_options.rb +616 -0
- data/lib/canon/comparison/xml_comparator.rb +566 -0
- data/lib/canon/comparison/yaml_comparator.rb +93 -0
- data/lib/canon/comparison.rb +239 -0
- data/lib/canon/config.rb +172 -0
- data/lib/canon/diff/diff_block.rb +71 -0
- data/lib/canon/diff/diff_block_builder.rb +105 -0
- data/lib/canon/diff/diff_classifier.rb +46 -0
- data/lib/canon/diff/diff_context.rb +85 -0
- data/lib/canon/diff/diff_context_builder.rb +107 -0
- data/lib/canon/diff/diff_line.rb +77 -0
- data/lib/canon/diff/diff_node.rb +56 -0
- data/lib/canon/diff/diff_node_mapper.rb +148 -0
- data/lib/canon/diff/diff_report.rb +133 -0
- data/lib/canon/diff/diff_report_builder.rb +62 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +407 -0
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +672 -0
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +284 -0
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +190 -0
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +860 -0
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +292 -0
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +199 -0
- data/lib/canon/diff_formatter/by_object/json_formatter.rb +305 -0
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +248 -0
- data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +17 -0
- data/lib/canon/diff_formatter/character_map.yml +197 -0
- data/lib/canon/diff_formatter/debug_output.rb +431 -0
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +551 -0
- data/lib/canon/diff_formatter/legend.rb +141 -0
- data/lib/canon/diff_formatter.rb +520 -0
- data/lib/canon/errors.rb +56 -0
- data/lib/canon/formatters/html4_formatter.rb +17 -0
- data/lib/canon/formatters/html5_formatter.rb +17 -0
- data/lib/canon/formatters/html_formatter.rb +37 -0
- data/lib/canon/formatters/html_formatter_base.rb +163 -0
- data/lib/canon/formatters/json_formatter.rb +3 -0
- data/lib/canon/formatters/xml_formatter.rb +20 -55
- data/lib/canon/formatters/yaml_formatter.rb +4 -1
- data/lib/canon/pretty_printer/html.rb +57 -0
- data/lib/canon/pretty_printer/json.rb +25 -0
- data/lib/canon/pretty_printer/xml.rb +29 -0
- data/lib/canon/rspec_matchers.rb +222 -80
- data/lib/canon/validators/base_validator.rb +49 -0
- data/lib/canon/validators/html_validator.rb +138 -0
- data/lib/canon/validators/json_validator.rb +89 -0
- data/lib/canon/validators/xml_validator.rb +53 -0
- data/lib/canon/validators/yaml_validator.rb +73 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/attribute_handler.rb +80 -0
- data/lib/canon/xml/c14n.rb +36 -0
- data/lib/canon/xml/character_encoder.rb +38 -0
- data/lib/canon/xml/data_model.rb +225 -0
- data/lib/canon/xml/element_matcher.rb +196 -0
- data/lib/canon/xml/line_range_mapper.rb +158 -0
- data/lib/canon/xml/namespace_handler.rb +86 -0
- data/lib/canon/xml/node.rb +32 -0
- data/lib/canon/xml/nodes/attribute_node.rb +54 -0
- data/lib/canon/xml/nodes/comment_node.rb +23 -0
- data/lib/canon/xml/nodes/element_node.rb +56 -0
- data/lib/canon/xml/nodes/namespace_node.rb +38 -0
- data/lib/canon/xml/nodes/processing_instruction_node.rb +24 -0
- data/lib/canon/xml/nodes/root_node.rb +16 -0
- data/lib/canon/xml/nodes/text_node.rb +23 -0
- data/lib/canon/xml/processor.rb +151 -0
- data/lib/canon/xml/whitespace_normalizer.rb +72 -0
- data/lib/canon/xml/xml_base_handler.rb +188 -0
- data/lib/canon.rb +14 -3
- metadata +116 -21
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Diff Architecture
|
|
4
|
+
nav_order: 43
|
|
5
|
+
parent: Advanced Topics
|
|
6
|
+
---
|
|
7
|
+
= Canon Diff Architecture
|
|
8
|
+
:toc:
|
|
9
|
+
:toclevels: 3
|
|
10
|
+
|
|
11
|
+
== Overview
|
|
12
|
+
|
|
13
|
+
Canon's diff system follows a strict separation of concerns with each layer having a single, well-defined responsibility. This document describes the complete architecture from comparison through formatting.
|
|
14
|
+
|
|
15
|
+
== Architecture Layers
|
|
16
|
+
|
|
17
|
+
[source]
|
|
18
|
+
----
|
|
19
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ LAYER 1: COMPARISON │
|
|
21
|
+
│ Creates Semantic Differences │
|
|
22
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
23
|
+
│ │
|
|
24
|
+
│ Input: Two documents (doc1, doc2) │
|
|
25
|
+
│ Process: XmlComparator.equivalent?(doc1, doc2, options) │
|
|
26
|
+
│ Output: DiffNode[] │
|
|
27
|
+
│ │
|
|
28
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
29
|
+
│ │ DiffNode │ │
|
|
30
|
+
│ │ ──────── │ │
|
|
31
|
+
│ │ • node1: Element from doc1 │ │
|
|
32
|
+
│ │ • node2: Element from doc2 │ │
|
|
33
|
+
│ │ • dimension: :text_content, :attribute_whitespace, etc │ │
|
|
34
|
+
│ │ • reason: "7 vs 9" or descriptive text │ │
|
|
35
|
+
│ │ • normative: nil (to be classified) │ │
|
|
36
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
37
|
+
│ │
|
|
38
|
+
│ Then: DiffClassifier.classify_all(diff_nodes, match_options) │
|
|
39
|
+
│ Sets normative=true/false based on match dimension behavior │
|
|
40
|
+
│ │
|
|
41
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
42
|
+
↓
|
|
43
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
44
|
+
│ LAYER 2: MAPPING (DiffNodes → Lines) │
|
|
45
|
+
│ Maps Semantic Diffs to Text Lines │
|
|
46
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
47
|
+
│ │
|
|
48
|
+
│ Input: DiffNode[], text1, text2 │
|
|
49
|
+
│ Process: DiffNodeMapper.map_to_lines(diff_nodes, text1, text2) │
|
|
50
|
+
│ Output: DiffLine[] │
|
|
51
|
+
│ │
|
|
52
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
53
|
+
│ │ DiffLine │ │
|
|
54
|
+
│ │ ──────── │ │
|
|
55
|
+
│ │ • line_number: Integer │ │
|
|
56
|
+
│ │ • content: String (the line text) │ │
|
|
57
|
+
│ │ • type: :unchanged, :added, :removed, :changed │ │
|
|
58
|
+
│ │ • diff_node: DiffNode reference │ │
|
|
59
|
+
│ │ • normative?: boolean (inherited from diff_node) │ │
|
|
60
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
61
|
+
│ │
|
|
62
|
+
│ How it works: │
|
|
63
|
+
│ 1. Run text diff (Diff::LCS) on original text │
|
|
64
|
+
│ 2. For each changed line, find corresponding DiffNode │
|
|
65
|
+
│ 3. Create DiffLine linking line ↔ DiffNode │
|
|
66
|
+
│ 4. Inherit normative/informative from DiffNode │
|
|
67
|
+
│ │
|
|
68
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
69
|
+
↓
|
|
70
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
71
|
+
│ LAYER 3: BLOCKING (DiffLines → DiffBlocks) │
|
|
72
|
+
│ Groups Contiguous Changed Lines │
|
|
73
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
74
|
+
│ │
|
|
75
|
+
│ Input: DiffLine[] │
|
|
76
|
+
│ Process: DiffBlockBuilder.build_blocks(diff_lines, show_diffs) │
|
|
77
|
+
│ Output: DiffBlock[] │
|
|
78
|
+
│ │
|
|
79
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
80
|
+
│ │ DiffBlock │ │
|
|
81
|
+
│ │ ───────── │ │
|
|
82
|
+
│ │ • start_idx: Integer │ │
|
|
83
|
+
│ │ • end_idx: Integer │ │
|
|
84
|
+
│ │ • types: ['-', '+', '!'] │ │
|
|
85
|
+
│ │ • diff_lines: DiffLine[] │ │
|
|
86
|
+
│ │ • diff_node: DiffNode (if all lines from same node) │ │
|
|
87
|
+
│ │ • normative?: true if ANY diff_line is normative │ │
|
|
88
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
89
|
+
│ │
|
|
90
|
+
│ Filtering: │
|
|
91
|
+
│ • show_diffs: :normative → keep only blocks with normative?=true │
|
|
92
|
+
│ • show_diffs: :informative → keep only blocks with normative?=false │
|
|
93
|
+
│ • show_diffs: :all → keep all blocks │
|
|
94
|
+
│ │
|
|
95
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
96
|
+
↓
|
|
97
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
98
|
+
│ LAYER 4: CONTEXTING (DiffBlocks → DiffContexts) │
|
|
99
|
+
│ Adds Surrounding Context Lines │
|
|
100
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
101
|
+
│ │
|
|
102
|
+
│ Input: DiffBlock[], context_lines, grouping_lines │
|
|
103
|
+
│ Process: DiffContextBuilder.build_contexts(blocks, options) │
|
|
104
|
+
│ Output: DiffContext[] │
|
|
105
|
+
│ │
|
|
106
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
107
|
+
│ │ DiffContext │ │
|
|
108
|
+
│ │ ─────────── │ │
|
|
109
|
+
│ │ • start_idx: Integer (includes context before) │ │
|
|
110
|
+
│ │ • end_idx: Integer (includes context after) │ │
|
|
111
|
+
│ │ • blocks: DiffBlock[] │ │
|
|
112
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
113
|
+
│ │
|
|
114
|
+
│ Process: │
|
|
115
|
+
│ 1. Group blocks by proximity (grouping_lines) │
|
|
116
|
+
│ 2. Expand each group with context_lines before/after │
|
|
117
|
+
│ 3. Create DiffContext for each group │
|
|
118
|
+
│ │
|
|
119
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
120
|
+
↓
|
|
121
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
122
|
+
│ LAYER 5: REPORTING (Wrap in DiffReport) │
|
|
123
|
+
│ Top-Level Container │
|
|
124
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
125
|
+
│ │
|
|
126
|
+
│ Input: DiffContext[], metadata │
|
|
127
|
+
│ Process: DiffReportBuilder.build(diff_nodes, text1, text2, opts) │
|
|
128
|
+
│ Output: DiffReport │
|
|
129
|
+
│ │
|
|
130
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
131
|
+
│ │ DiffReport │ │
|
|
132
|
+
│ │ ────────── │ │
|
|
133
|
+
│ │ • element_name: String │ │
|
|
134
|
+
│ │ • file1_name: String │ │
|
|
135
|
+
│ │ • file2_name: String │ │
|
|
136
|
+
│ │ • contexts: DiffContext[] │ │
|
|
137
|
+
│ │ • has_differences?: boolean │ │
|
|
138
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
139
|
+
│ │
|
|
140
|
+
│ This is the SINGLE source of truth for what to display │
|
|
141
|
+
│ │
|
|
142
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
143
|
+
↓
|
|
144
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
145
|
+
│ LAYER 6: FORMATTING (Render to String) │
|
|
146
|
+
│ Display Only │
|
|
147
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
148
|
+
│ │
|
|
149
|
+
│ Input: DiffReport │
|
|
150
|
+
│ Process: Formatter.format(diff_report) │
|
|
151
|
+
│ Output: Formatted string │
|
|
152
|
+
│ │
|
|
153
|
+
│ Formatters: │
|
|
154
|
+
│ • ByLine::HtmlFormatter - HTML with DOM-aware display │
|
|
155
|
+
│ • ByLine::XmlFormatter - XML with token highlighting │
|
|
156
|
+
│ • ByLine::JsonFormatter - JSON pretty-printed │
|
|
157
|
+
│ • ByObject::XmlFormatter - Tree visualization │
|
|
158
|
+
│ │
|
|
159
|
+
│ Formatter responsibilities: │
|
|
160
|
+
│ • Render DiffContext objects │
|
|
161
|
+
│ • Apply visualization (colors, symbols) │
|
|
162
|
+
│ • Format line numbers │
|
|
163
|
+
│ • NO comparison, NO filtering, NO business logic │
|
|
164
|
+
│ │
|
|
165
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
166
|
+
----
|
|
167
|
+
|
|
168
|
+
== Data Flow Example
|
|
169
|
+
|
|
170
|
+
=== Example: Attribute Order Difference (Normalized Away)
|
|
171
|
+
|
|
172
|
+
[source]
|
|
173
|
+
----
|
|
174
|
+
Input:
|
|
175
|
+
doc1: <div class="TOC" id="_">Content</div>
|
|
176
|
+
doc2: <div id="_" class="TOC">Content</div>
|
|
177
|
+
|
|
178
|
+
Layer 1 - Comparison:
|
|
179
|
+
XmlComparator compares → NO DiffNode created
|
|
180
|
+
(attributes normalized, so equivalent)
|
|
181
|
+
|
|
182
|
+
Layer 2 - Mapping:
|
|
183
|
+
DiffNodeMapper receives empty DiffNode[]
|
|
184
|
+
No DiffLines created
|
|
185
|
+
|
|
186
|
+
Layer 3 - Blocking:
|
|
187
|
+
DiffBlockBuilder receives empty DiffLine[]
|
|
188
|
+
No DiffBlocks created
|
|
189
|
+
|
|
190
|
+
Layer 4 - Contexting:
|
|
191
|
+
DiffContextBuilder receives empty DiffBlock[]
|
|
192
|
+
No DiffContexts created
|
|
193
|
+
|
|
194
|
+
Layer 5 - Reporting:
|
|
195
|
+
DiffReport(contexts: [])
|
|
196
|
+
has_differences? → false
|
|
197
|
+
|
|
198
|
+
Layer 6 - Formatting:
|
|
199
|
+
Formatter sees empty contexts
|
|
200
|
+
Returns empty string (no diff to show)
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
=== Example: Real Text Difference
|
|
204
|
+
|
|
205
|
+
[source]
|
|
206
|
+
----
|
|
207
|
+
Input:
|
|
208
|
+
doc1: <p>Test 1</p>
|
|
209
|
+
doc2: <p>Test 2</p>
|
|
210
|
+
|
|
211
|
+
Layer 1 - Comparison:
|
|
212
|
+
XmlComparator finds text differs
|
|
213
|
+
Creates: DiffNode(dimension: :text_content, normative: true)
|
|
214
|
+
|
|
215
|
+
Layer 2 - Mapping:
|
|
216
|
+
DiffNodeMapper maps to text lines
|
|
217
|
+
Creates: DiffLine(type: :changed, diff_node: DiffNode, normative: true)
|
|
218
|
+
|
|
219
|
+
Layer 3 - Blocking:
|
|
220
|
+
DiffBlockBuilder groups lines
|
|
221
|
+
Creates: DiffBlock(diff_lines: [DiffLine], normative: true)
|
|
222
|
+
With show_diffs: :normative → keeps this block
|
|
223
|
+
|
|
224
|
+
Layer 4 - Contexting:
|
|
225
|
+
DiffContextBuilder adds context
|
|
226
|
+
Creates: DiffContext(blocks: [DiffBlock], start_idx: 0, end_idx: 2)
|
|
227
|
+
|
|
228
|
+
Layer 5 - Reporting:
|
|
229
|
+
DiffReport(contexts: [DiffContext])
|
|
230
|
+
has_differences? → true
|
|
231
|
+
|
|
232
|
+
Layer 6 - Formatting:
|
|
233
|
+
Formatter renders:
|
|
234
|
+
1| - | <p>Test 1</p>
|
|
235
|
+
| 1+ | <p>Test 2</p>
|
|
236
|
+
----
|
|
237
|
+
|
|
238
|
+
=== Example: Mixed Normative and Informative Diffs
|
|
239
|
+
|
|
240
|
+
[source]
|
|
241
|
+
----
|
|
242
|
+
Input:
|
|
243
|
+
doc1: <div class="foo" id="x"><p>Old</p></div>
|
|
244
|
+
doc2: <div id="x" class="foo"><p>New</p></div>
|
|
245
|
+
|
|
246
|
+
Layer 1 - Comparison:
|
|
247
|
+
• Attribute order normalized → NO DiffNode for <div>
|
|
248
|
+
• Text differs → DiffNode(dimension: :text_content) for <p>
|
|
249
|
+
Result: 1 DiffNode (normative)
|
|
250
|
+
|
|
251
|
+
Layer 2 - Mapping:
|
|
252
|
+
Maps <p> text difference to line 1
|
|
253
|
+
Creates: 1 DiffLine for <p> change
|
|
254
|
+
<div> line has NO DiffLine (no DiffNode for it)
|
|
255
|
+
|
|
256
|
+
Layer 3 - Blocking:
|
|
257
|
+
Creates: 1 DiffBlock for <p> line
|
|
258
|
+
|
|
259
|
+
Layer 4 - Contexting:
|
|
260
|
+
Creates: 1 DiffContext (might include <div> line as context)
|
|
261
|
+
|
|
262
|
+
Layer 6 - Formatting:
|
|
263
|
+
Shows only <p> change:
|
|
264
|
+
1| - | <p>Old</p>
|
|
265
|
+
| 1+ | <p>New</p>
|
|
266
|
+
|
|
267
|
+
Does NOT show <div> with attribute order diff ✓
|
|
268
|
+
----
|
|
269
|
+
|
|
270
|
+
== Class Responsibilities
|
|
271
|
+
|
|
272
|
+
=== Comparison Layer
|
|
273
|
+
|
|
274
|
+
**XmlComparator**::
|
|
275
|
+
**Single Responsibility**: Compare DOM nodes semantically
|
|
276
|
+
**Creates**: DiffNode objects for semantic differences
|
|
277
|
+
**Does NOT**: Handle text lines, formatting, filtering
|
|
278
|
+
|
|
279
|
+
**DiffClassifier**::
|
|
280
|
+
**Single Responsibility**: Classify DiffNodes as normative/informative
|
|
281
|
+
**Input**: DiffNode[], MatchOptions
|
|
282
|
+
**Output**: Same DiffNodes with `normative` set
|
|
283
|
+
**Logic**: normative = (dimension behavior != :ignore)
|
|
284
|
+
|
|
285
|
+
=== Mapping Layer
|
|
286
|
+
|
|
287
|
+
**DiffNodeMapper** (ENHANCE)::
|
|
288
|
+
**Single Responsibility**: Map semantic diffs to text line positions
|
|
289
|
+
**Input**: DiffNode[], text1, text2
|
|
290
|
+
**Output**: DiffLine[]
|
|
291
|
+
**Process**:
|
|
292
|
+
1. Run Diff::LCS.sdiff on text
|
|
293
|
+
2. For each changed line, find owning DiffNode
|
|
294
|
+
3. Create DiffLine linking line ↔ DiffNode
|
|
295
|
+
4. Inherit normative/informative from DiffNode
|
|
296
|
+
|
|
297
|
+
=== Processing Layer
|
|
298
|
+
|
|
299
|
+
**DiffBlockBuilder** (NEW)::
|
|
300
|
+
**Single Responsibility**: Group contiguous DiffLines into blocks
|
|
301
|
+
**Input**: DiffLine[], show_diffs option
|
|
302
|
+
**Output**: DiffBlock[]
|
|
303
|
+
**Process**:
|
|
304
|
+
1. Identify runs of changed lines
|
|
305
|
+
2. Create DiffBlock for each run
|
|
306
|
+
3. Set block.normative based on diff_lines
|
|
307
|
+
4. Filter blocks by show_diffs
|
|
308
|
+
|
|
309
|
+
**DiffContextBuilder** (NEW)::
|
|
310
|
+
**Single Responsibility**: Add context lines around blocks
|
|
311
|
+
**Input**: DiffBlock[], context_lines, grouping_lines
|
|
312
|
+
**Output**: DiffContext[]
|
|
313
|
+
**Process**:
|
|
314
|
+
1. Group nearby blocks (within grouping_lines)
|
|
315
|
+
2. Expand each group with context_lines
|
|
316
|
+
3. Create DiffContext for each group
|
|
317
|
+
|
|
318
|
+
**DiffReportBuilder** (NEW)::
|
|
319
|
+
**Single Responsibility**: Orchestrate the pipeline
|
|
320
|
+
**Input**: DiffNode[], text1, text2, options
|
|
321
|
+
**Output**: DiffReport
|
|
322
|
+
**Process**:
|
|
323
|
+
1. DiffNodeMapper → DiffLines
|
|
324
|
+
2. DiffBlockBuilder → DiffBlocks
|
|
325
|
+
3. DiffContextBuilder → DiffContexts
|
|
326
|
+
4. Wrap in DiffReport
|
|
327
|
+
|
|
328
|
+
=== Formatting Layer
|
|
329
|
+
|
|
330
|
+
**Formatters** (REFACTOR)::
|
|
331
|
+
**Single Responsibility**: Render DiffReport to string
|
|
332
|
+
**Input**: DiffReport (NOT raw text!)
|
|
333
|
+
**Output**: Formatted string
|
|
334
|
+
**Does**: Apply colors, line numbers, visualization
|
|
335
|
+
**Does NOT**: Compare, map, filter, or create blocks
|
|
336
|
+
|
|
337
|
+
== Key Principles
|
|
338
|
+
|
|
339
|
+
=== Single Responsibility
|
|
340
|
+
|
|
341
|
+
Each class does ONE thing:
|
|
342
|
+
- **Comparator**: Compares → DiffNodes
|
|
343
|
+
- **Mapper**: Maps nodes → lines
|
|
344
|
+
- **BlockBuilder**: Groups lines → blocks
|
|
345
|
+
- **ContextBuilder**: Adds context → contexts
|
|
346
|
+
- **ReportBuilder**: Orchestrates pipeline
|
|
347
|
+
- **Formatter**: Renders report → string
|
|
348
|
+
|
|
349
|
+
=== Separation of Concerns
|
|
350
|
+
|
|
351
|
+
**Business Logic** (Comparison, Mapping, Blocking):
|
|
352
|
+
- Lives in `lib/canon/diff/` and `lib/canon/comparison/`
|
|
353
|
+
- No knowledge of rendering or colors
|
|
354
|
+
- Pure data transformations
|
|
355
|
+
|
|
356
|
+
**Presentation** (Formatting):
|
|
357
|
+
- Lives in `lib/canon/diff_formatter/`
|
|
358
|
+
- No business logic
|
|
359
|
+
- Just renders what it's given
|
|
360
|
+
|
|
361
|
+
=== Information Expert
|
|
362
|
+
|
|
363
|
+
Each object knows about its own data:
|
|
364
|
+
- `DiffNode.normative?` - knows if it's semantically different
|
|
365
|
+
- `DiffLine.normative?` - knows via its DiffNode
|
|
366
|
+
- `DiffBlock.normative?` - knows via its DiffLines
|
|
367
|
+
- `DiffContext` - knows about its blocks
|
|
368
|
+
|
|
369
|
+
=== Tell, Don't Ask
|
|
370
|
+
|
|
371
|
+
Don't ask objects for data to make decisions elsewhere:
|
|
372
|
+
```ruby
|
|
373
|
+
# BAD (Ask)
|
|
374
|
+
if diff_node.dimension == :attribute_whitespace &&
|
|
375
|
+
match_options[:attribute_whitespace] == :ignore
|
|
376
|
+
# make decision here
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# GOOD (Tell)
|
|
380
|
+
if diff_node.normative?
|
|
381
|
+
# decision already made by DiffNode/DiffClassifier
|
|
382
|
+
end
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
== Implementation Plan
|
|
386
|
+
|
|
387
|
+
=== Phase 1: Enhance DiffNodeMapper
|
|
388
|
+
|
|
389
|
+
Make it actually map DiffNodes to line positions.
|
|
390
|
+
|
|
391
|
+
=== Phase 2: Create Builders
|
|
392
|
+
|
|
393
|
+
**DiffBlockBuilder**:
|
|
394
|
+
- Groups DiffLines into DiffBlocks
|
|
395
|
+
- Filters by show_diffs
|
|
396
|
+
- Sets normative? on blocks
|
|
397
|
+
|
|
398
|
+
**DiffContextBuilder**:
|
|
399
|
+
- Groups DiffBlocks by proximity
|
|
400
|
+
- Expands with context lines
|
|
401
|
+
- Creates DiffContexts
|
|
402
|
+
|
|
403
|
+
**DiffReportBuilder**:
|
|
404
|
+
- Orchestrates the full pipeline
|
|
405
|
+
- Single entry point
|
|
406
|
+
|
|
407
|
+
=== Phase 3: Refactor Formatters
|
|
408
|
+
|
|
409
|
+
Remove all comparison/mapping/blocking logic.
|
|
410
|
+
Accept DiffReport and just render it.
|
|
411
|
+
|
|
412
|
+
=== Phase 4: Update Callers
|
|
413
|
+
|
|
414
|
+
Change from:
|
|
415
|
+
```ruby
|
|
416
|
+
formatter.format(doc1, doc2) # OLD - formatters did everything
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
To:
|
|
420
|
+
```ruby
|
|
421
|
+
report = DiffReportBuilder.build(diff_nodes, text1, text2, options)
|
|
422
|
+
formatter.format(report) # NEW - just renders
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
== Benefits
|
|
426
|
+
|
|
427
|
+
**Fixes Issue 1**: When DiffNodes are empty (all normalized), DiffReport has no contexts → no output
|
|
428
|
+
|
|
429
|
+
**Fixes Issue 2**: Empty diffs handled at DiffReport level, not formatter hacks
|
|
430
|
+
|
|
431
|
+
**Testable**: Each class tested independently
|
|
432
|
+
|
|
433
|
+
**Maintainable**: Clear responsibilities, easy to understand
|
|
434
|
+
|
|
435
|
+
**Extensible**: Easy to add new filtering, grouping, or rendering strategies
|