1. Purpose
Canon’s diff system follows a strict separation of concerns with each layer having a single, well-defined responsibility. This architecture ensures clean code, maintainability, and testability.
2. Pipeline Layers
Canon processes comparisons through six distinct layers:
Input Documents
↓
[1] Comparison → Creates semantic differences (DiffNodes)
↓
[2] Classification → Marks as normative/informative
↓
[3] Mapping → Maps semantic diffs to text lines
↓
[4] Blocking → Groups contiguous changed lines
↓
[5] Contexting → Adds surrounding unchanged lines
↓
[6] Formatting → Renders to colored output
↓
Display Output
3. Layer 1: Comparison
Responsibility: Create semantic differences
Input: Two documents (doc1, doc2) + match options
Process: Compare DOM/JSON trees semantically
Output: Array of DiffNode objects
# DiffNode structure
{
node1: Element from doc1,
node2: Element from doc2,
dimension: :text_content | :attribute_order | etc.,
reason: "Description of difference",
normative: nil # Set by classifier
}
Key Point: Comparators only detect semantic differences based on match options. They don’t know about text lines or formatting.
4. Layer 2: Classification
Responsibility: Mark differences as normative or informative
Input: DiffNode array + match options
Process: Check each dimension’s behavior (:strict, :normalize, :ignore)
Output: Same DiffNodes with normative flag set
classifier = DiffClassifier.new(match_options)
diff_nodes.each do |node|
behavior = match_options.behavior_for(node.dimension)
node.normative = (behavior != :ignore)
end
Classification Rules:
* :ignore → informative (cyan, doesn’t affect equivalence)
* :strict or :normalize → normative (red/green, affects equivalence)
5. Layer 3: Mapping
Responsibility: Enrich semantic diffs with character-level positions and map to text lines
Input: DiffNode array + original text documents
Process: 1. Enrichment (DiffNodeEnricher): For each DiffNode, locate serialized content in source text and decompose into character ranges - SourceLocator: Find serialized content position (char offset, line, column) - TextDecomposer: Split into common prefix / changed / common suffix - Create DiffCharRange objects with exact character positions 2. Assembly (DiffLineBuilder): Build DiffLine sequence from enriched DiffNodes - Map DiffCharRanges to source lines - Fill in unchanged lines between changes - Detect reflow (lines that moved but content unchanged)
Output: Array of DiffLine objects, each carrying DiffCharRange arrays
# DiffLine structure
{
line_number: 5,
content: "<p>Changed text</p>",
new_content: "<p>New text</p>",
type: :changed, # :added, :removed, :unchanged
diff_node: DiffNode reference,
char_ranges: [DiffCharRange, ...], # text1 side
new_char_ranges: [DiffCharRange, ...], # text2 side
normative: true # from diff_node
}
# DiffCharRange structure
{
line_number: 5,
start_col: 3,
end_col: 14,
side: :old,
status: :changed_old, # :unchanged, :changed_new, :removed, :added
role: :changed, # :before, :changed, :after
diff_node: DiffNode reference
}
Key Point: This layer bridges semantic differences to their textual representation with precise character-level positions. The two-phase approach (enrich then assemble) ensures the formatter receives pre-computed data requiring no further computation.
6. Layer 4: Blocking
Responsibility: Group contiguous changed lines into blocks
Input: DiffLine array + show_diffs option
Process:
1. Identify runs of consecutive changed lines
2. Create DiffBlock for each run
3. Set block.normative based on contained lines
4. Filter blocks by show_diffs setting
Output: Array of DiffBlock objects
# DiffBlock structure
{
start_idx: 10,
end_idx: 15,
types: ['-', '+'],
diff_lines: [DiffLine, ...],
diff_node: DiffNode (if all from same node),
normative: true # true if ANY line is normative
}
Filtering:
* show_diffs: :normative → keep only normative blocks
* show_diffs: :informative → keep only informative blocks
* show_diffs: :all → keep all blocks
7. Layer 5: Contexting
Responsibility: Add surrounding context lines
Input: DiffBlock array + context/grouping options
Process:
1. Group nearby blocks (within diff_grouping_lines)
2. Expand each group with context_lines before/after
3. Create DiffContext for each group
Output: Array of DiffContext objects
# DiffContext structure
{
start_idx: 7, # includes context before
end_idx: 18, # includes context after
blocks: [DiffBlock, ...]
}
Key Point: This layer controls how much unchanged content is shown around changes.
8. Layer 6: Formatting
Responsibility: Render to colored string output
Input: Array of DiffContext objects
Process: * Apply line numbers * Add color codes (red/green/cyan) * Visualize whitespace characters * Format for terminal display
Output: Formatted string ready for display
Key Point: Formatters are pure display - no business logic, no filtering, no decisions.
9. Data Flow Example
9.1. Scenario: Attribute order normalized away
Input:
doc1: <div class="TOC" id="_">
doc2: <div id="_" class="TOC">
match_options: { attribute_order: :ignore }
Layer 1 - Comparison:
XmlComparator sees attribute order differs
BUT match option is :ignore
→ NO DiffNode created (semantically equivalent)
Layer 2 - Classification:
No DiffNodes to classify
→ Skip
Layer 3 - Mapping:
No DiffNodes to map
→ No DiffLines created
Layer 4 - Blocking:
No DiffLines to block
→ No DiffBlocks created
Layer 5 - Contexting:
No DiffBlocks to contextualize
→ No DiffContexts created
Layer 6 - Formatting:
No contexts to format
→ Returns empty string (no diff shown)
Result: Files are equivalent, no output
9.2. Scenario: Real text difference
Input:
doc1: <p>Test 1</p>
doc2: <p>Test 2</p>
match_options: { text_content: :strict }
Layer 1 - Comparison:
XmlComparator finds text differs
Creates: DiffNode(dimension: :text_content)
Layer 2 - Classification:
text_content: :strict → normative
Sets: diff_node.normative = true
Layer 3 - Mapping:
Maps to line 1 (changed)
Creates: DiffLine(type: :changed, normative: true)
Layer 4 - Blocking:
Groups line into block
Creates: DiffBlock([DiffLine], normative: true)
Filter: show_diffs: :normative → keeps block
Layer 5 - Contexting:
Adds context lines (0 before, 0 after if short file)
Creates: DiffContext([DiffBlock])
Layer 6 - Formatting:
Renders with colors:
1| - | <p>Test 1</p>
| 1+ | <p>Test 2</p>
Result: Files differ, diff shown in red/green
10. Class Responsibilities
10.1. Comparison Layer
- [
XmlComparator](../../lib/canon/comparison/xml_comparator.rb) -
Compares DOM nodes semantically, creates DiffNodes
- [
DiffClassifier](../../lib/canon/diff/diff_classifier.rb) -
Classifies DiffNodes as normative/informative
10.2. Processing Layers
- [
DiffNodeEnricher](../../lib/canon/diff/diff_node_enricher.rb) -
Phase 1: Enriches DiffNodes with character-level positions (SourceLocator + TextDecomposer)
- [
DiffLineBuilder](../../lib/canon/diff/diff_line_builder.rb) -
Phase 2: Assembles DiffLines from enriched DiffNodes (replaces DiffNodeMapper)
- [
DiffCharRange](../../lib/canon/diff/diff_char_range.rb) -
Value object: a character range within a source line linked to a DiffNode
- [
TextDecomposer](../../lib/canon/diff/text_decomposer.rb) -
Decomposes two strings into common prefix / changed / common suffix
- [
SourceLocator](../../lib/canon/diff/source_locator.rb) -
Locates serialized content within source text, returns char offset + line/col
- [
DiffBlockBuilder](../../lib/canon/diff/diff_block_builder.rb) -
Groups contiguous lines into blocks, filters by show_diffs
- [
DiffContextBuilder](../../lib/canon/diff/diff_context_builder.rb) -
Adds context lines, groups nearby blocks
- [
DiffReportBuilder](../../lib/canon/diff/diff_report_builder.rb) -
Orchestrates the full pipeline
10.3. Formatting Layer
- [
ByLine::XmlFormatter](../../lib/canon/diff_formatter/by_line/xml_formatter.rb) -
Renders line-by-line XML diffs
- [
ByLine::HtmlFormatter](../../lib/canon/diff_formatter/by_line/html_formatter.rb) -
Renders line-by-line HTML diffs
- [
ByObject::XmlFormatter](../../lib/canon/diff_formatter/by_object/xml_formatter.rb) -
Renders tree-based XML diffs
11. Key Principles
11.1. Single Responsibility
Each class does ONE thing:
-
Comparator: Compares → DiffNodes
-
Classifier: Classifies → normative flags
-
Enricher: Enriches → char positions on DiffNodes
-
LineBuilder: Assembles → DiffLines with char ranges
-
BlockBuilder: Groups lines → blocks
-
ContextBuilder: Adds context → contexts
-
Formatter: Renders → string
11.2. Separation of Concerns
Business Logic (Layers 1-5):
* Lives in lib/canon/diff/ and lib/canon/comparison/
* No knowledge of rendering or colors
* Pure data transformations
Presentation (Layer 6):
* Lives in lib/canon/diff_formatter/
* No business logic
* Just renders what it’s given
12. Benefits
Testability: Each layer tested independently
Maintainability: Clear responsibilities, easy to understand
Extensibility: Easy to add new filtering, grouping, or rendering strategies
Correctness: When DiffNodes are empty (all normalized), entire pipeline produces no output
13. See Also
-
Diff Classification - Normative vs informative
-
Semantic Diff Report - High-level diff summary
-
Comparison Pipeline - User-facing overview
-
Architecture - System design