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

11.3. Information Expert

Each object knows about its own data:

  • DiffNode.normative? - knows if semantically different

  • DiffLine.normative? - knows via its DiffNode

  • DiffBlock.normative? - knows via its DiffLines

  • DiffContext - knows about its blocks

11.4. Tell, Don’t Ask

Don’t ask objects for data to make decisions elsewhere:

# BAD (Ask)
if diff_node.dimension == :attribute_order &&
   match_options[:attribute_order] == :ignore
  # make decision here
end

# GOOD (Tell)
if diff_node.normative?
  # decision already made
end

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