1. Purpose

This document explains Canon’s 4-layer comparison architecture and how documents flow through preprocessing, algorithm selection, semantic matching, and diff rendering.

For a guided walkthrough of choosing configurations, see Choosing Configuration.

For detailed 4-layer pipeline documentation, see Comparison Pipeline.

2. Overview

Canon uses a 4-layer architecture that separates concerns for clean, maintainable comparison logic:

  1. Layer 1 - Preprocessing: Optional document normalization

  2. Layer 2 - Algorithm Selection: Choose comparison strategy (DOM vs Semantic)

  3. Layer 3 - Match Options: Content comparison with configurable dimensions (algorithm-specific)

  4. Layer 4 - Diff Formatting: Formatted output with visualization (algorithm-specific)

Each layer is independent and configurable, allowing fine-grained control over comparison behavior.

Key Insight: Layers 3 and 4 are algorithm-specific - they behave differently depending on which algorithm (DOM or Semantic) is chosen in Layer 2.

3. System architecture diagram

graph TD
    A[Input Documents] --> B[Layer 1: Preprocessing]
    B --> C[Layer 2: Algorithm Selection]
    C --> D[Layer 3: Match Options]
    D --> E[Layer 4: Diff Formatting]
    E --> F[Output]

    C -->|DOM Algorithm| D1[Positional Matching]
    C -->|Semantic Algorithm| D2[Signature Matching]

    D1 --> E1[Line-based Diff]
    D2 --> E2[Operation-based Diff]

    style B fill:#e1f5ff
    style C fill:#fff4e1
    style D fill:#ffe1f5
    style E fill:#e1ffe1

4. Layer 1: Preprocessing

4.1. Purpose

Transform documents into a normalized form before comparison. This eliminates format-specific variations that should not affect semantic equivalence.

4.2. Options

none (default)

No preprocessing - compare documents as-is

c14n

Canonical form:

  • XML: W3C Canonical XML 1.1

  • HTML: Normalized HTML structure

  • JSON: Sorted keys, normalized whitespace

  • YAML: Sorted keys, standard format

normalize

Normalize whitespace:

  • Collapse multiple whitespace to single space

  • Trim leading/trailing whitespace

  • Normalize line endings

format

Pretty-print with standard formatting:

  • 2-space indentation

  • One element/property per line

  • Consistent structure

4.3. Usage

Example 1. Ruby API
Canon::Comparison.equivalent?(doc1, doc2,
  preprocessing: :normalize
)
Example 2. CLI
$ canon diff file1.xml file2.xml --preprocessing normalize

See Preprocessing documentation for details.

5. Layer 2: Algorithm selection

5.1. Purpose

Choose the comparison strategy. This is a critical decision because it determines how Layers 3 and 4 behave.

5.2. Options

dom (default)

DOM-based positional comparison

  • Fast, stable, well-tested

  • Position-based element matching

  • No move detection

  • Best for similar documents

semantic (experimental)

Tree-based semantic diff

  • Slower but more intelligent

  • Signature-based matching

  • Detects moves, merges, splits

  • Best for restructured documents

5.3. Algorithm characteristics

Feature DOM Semantic

Stability

Stable (production-ready)

Experimental

Performance

Fast (linear)

Slower (quadratic worst case)

Move Detection

No

Yes

Match Strategy

Positional

Signature-based

Layer 3 Behavior

Element-by-element comparison

Signature calculation

Layer 4 Behavior

Line-based differences

Operation-based (INSERT, DELETE, UPDATE, MOVE)

Best For

Similar documents

Restructured documents

5.4. Usage

Example 3. Ruby API
# DOM algorithm (default)
Canon::Comparison.equivalent?(doc1, doc2,
  diff_algorithm: :dom
)

# Semantic algorithm
Canon::Comparison.equivalent?(doc1, doc2,
  diff_algorithm: :semantic
)
Example 4. CLI
# DOM algorithm
$ canon diff file1.xml file2.xml --diff-algorithm dom

# Semantic algorithm
$ canon diff file1.xml file2.xml --diff-algorithm semantic

See Algorithm documentation for details.

6. Layer 3: Match options

6.1. Purpose

Configure what to compare and how strictly. Match options are format-specific - each format (XML, HTML, JSON, YAML) has its own set of dimensions based on its structure.

6.2. Key architectural principle

Dimensions are format-specific, NOT algorithm-specific.

The comparison architecture works as follows:

Aspect Description Examples

Format

Determines which dimensions exist

XML has attributes, JSON has keys

Dimensions

WHAT to compare (format-specific)

text_content, attribute_values, key_order

Profile

Configures dimension behaviors for a format

text_content: :normalize, comments: :ignore

Algorithm

HOW nodes are matched (format-independent)

DOM: position-based, Semantic: signature-based

Critical distinction:

  • Format → Dimensions: XML has attribute_values, JSON has key_order

  • Profile → Behaviors: Configures HOW dimensions are compared (:strict, :normalize, :ignore)

  • Algorithm → Matching Strategy: DOM (position) vs Semantic (signature) - works with ANY format

6.3. Format-specific dimensions

Different formats have different dimensions based on their structure:

XML/HTML dimensions:

text_content

Text within elements

structural_whitespace

Whitespace between elements

attribute_presence

Which attributes exist

attribute_order

Order of attributes

attribute_values

Attribute value content

element_position

Position in tree

comments

Comment nodes

JSON dimensions:

text_content

Value text

structural_whitespace

Whitespace

key_order

Order of object keys

YAML dimensions:

text_content

Value text

structural_whitespace

Whitespace

key_order

Order of keys

comments

Comments

6.4. Dimension behaviors

Each dimension supports behaviors:

  • :strict - Must match exactly

  • :normalize - Match after normalization

  • :ignore - Don’t compare

6.5. Match profiles

Profiles are predefined combinations of dimension settings for common scenarios.

Important: Profiles are format-specific. Each format (Xml, Html, Json, Yaml) has its own set of profiles configured for its dimensions.

See Available Preset Profiles for complete profile reference.

6.6. Available preset profiles

Canon provides preset profiles optimized for different comparison scenarios. Each format has its own set of profiles with appropriate dimension configurations.

6.6.1. XML/HTML profiles

Profile: :strict

Exact matching - all dimensions use :strict behavior (XML default).

Dimension Behavior Description

preprocessing

:none

No preprocessing - compare as-is

text_content

:strict

Must match exactly

structural_whitespace

:strict

Whitespace must match exactly

attribute_presence

:strict

All attributes must be present

attribute_order

:strict

Attribute order must match

attribute_values

:strict

Attribute values must match exactly

element_position

:strict

Element positions must match

comments

:strict

Comments must match exactly

Use when: You need exact byte-for-byte matching (e.g., validating serialization).

Profile: :rendered

Browser rendering - ignores formatting that doesn’t affect display (HTML default).

Dimension Behavior Description

preprocessing

:none

No preprocessing

text_content

:normalize

Normalize text (collapse whitespace)

structural_whitespace

:normalize

Normalize whitespace

attribute_presence

:strict

All attributes must be present

attribute_order

:strict

Attribute order must match

attribute_values

:strict

Attribute values must match exactly

element_position

:strict

Element positions must match

comments

:ignore

Comments are ignored

Use when: You care about what the browser displays, not source formatting.

Profile: :html4

HTML4 rendered output - HTML4 normalizes attribute whitespace.

Dimension Behavior Description

preprocessing

:rendered

Rendered HTML preprocessing

text_content

:normalize

Normalize text

structural_whitespace

:normalize

Normalize whitespace

attribute_presence

:strict

All attributes must be present

attribute_order

:strict

Attribute order must match

attribute_values

:normalize

Normalize attribute values

element_position

:ignore

Element position doesn’t matter

comments

:ignore

Comments are ignored

Use when: Testing HTML4 output where attribute whitespace may vary.

Profile: :html5

HTML5 rendered output - same as :rendered.

Dimension Behavior Description

preprocessing

:rendered

Rendered HTML preprocessing

text_content

:normalize

Normalize text

structural_whitespace

:normalize

Normalize whitespace

attribute_presence

:strict

All attributes must be present

attribute_order

:strict

Attribute order must match

attribute_values

:strict

Attribute values must match exactly

element_position

:ignore

Element position doesn’t matter

comments

:ignore

Comments are ignored

Use when: Testing HTML5 output.

Profile: :spec_friendly

Test-friendly - ignores formatting, focuses on content.

Dimension Behavior Description

preprocessing

:rendered

Rendered HTML preprocessing

text_content

:normalize

Normalize text

structural_whitespace

:ignore

Whitespace ignored

attribute_presence

:strict

All attributes must be present

attribute_order

:ignore

Attribute order ignored

attribute_values

:normalize

Normalize attribute values

element_position

:ignore

Element position ignored

comments

:ignore

Comments ignored

Use when: Writing tests where formatting changes are acceptable.

Profile: :content_only

Maximum tolerance - only semantic content matters.

Dimension Behavior Description

preprocessing

:c14n

Canonical XML preprocessing

text_content

:normalize

Normalize text

structural_whitespace

:ignore

Whitespace ignored

attribute_presence

:strict

All attributes must be present

attribute_order

:ignore

Attribute order ignored

attribute_values

:normalize

Normalize attribute values

element_position

:ignore

Element position ignored

comments

:ignore

Comments ignored

Use when: You only care about semantic content, not structure or formatting.

6.6.2. JSON profiles

JSON has 3 preset profiles: :strict, :spec_friendly, and :content_only.

Dimension :strict :spec_friendly :content_only

preprocessing

:none

:normalize

:normalize

text_content

:strict

:strict

:normalize

structural_whitespace

:strict

:ignore

:ignore

key_order

:strict

:ignore

:ignore

Use cases:

  • :strict - Exact JSON matching (order-sensitive)

  • :spec_friendly - Order-independent JSON comparison

  • :content_only - Normalized values, order and formatting ignored

6.6.3. YAML profiles

YAML has 3 preset profiles: :strict, :spec_friendly, and :content_only.

Dimension :strict :spec_friendly :content_only

preprocessing

:none

:normalize

:normalize

text_content

:strict

:strict

:normalize

structural_whitespace

:strict

:ignore

:ignore

key_order

:strict

:ignore

:ignore

comments

:strict

:ignore

:ignore

Use cases:

  • :strict - Exact YAML matching (order and comments matter)

  • :spec_friendly - Order-independent, comments ignored

  • :content_only - Maximum tolerance, only values matter

6.7. Customizing profiles

Canon provides two ways to customize comparison behavior: inline custom profiles and named custom profiles.

6.7.1. Inline custom profiles

For one-off comparisons, pass a Hash directly to the profile parameter:

Canon::Comparison.equivalent?(html1, html2,
  profile: {
    text_content: :normalize,
    structural_whitespace: :ignore,
    comments: :ignore
  }
)

Validation: Inline profiles are validated at comparison time. Invalid dimensions or behaviors will raise a Canon::Error.

# This raises Canon::Error
Canon::Comparison.equivalent?(html1, html2,
  profile: {
    unknown_dimension: :strict  # => Error: Unknown dimension: unknown_dimension
  }
)

6.7.2. Named custom profiles (Profile DSL)

For reusable custom profiles, define them using the Profile DSL:

# Define a custom profile
Canon::Comparison.define_profile(:content_focused) do
  text_content :normalize
  comments :ignore
  structural_whitespace :ignore
  attribute_values :normalize
  preprocessing :rendered
end

# Use the custom profile
Canon::Comparison.equivalent?(html1, html2, profile: :content_focused)

# List all available profiles (includes custom profiles)
Canon::Comparison.available_profiles
# => [:strict, :rendered, :html4, :html5, :spec_friendly, :content_only, :content_focused]

Available DSL methods:

  • text_content - Text within elements

  • structural_whitespace - Whitespace between elements

  • attribute_presence - Which attributes exist

  • attribute_order - Order of attributes

  • attribute_values - Attribute value content

  • element_position - Position of elements

  • comments - Comment content and placement

  • preprocessing - Preprocessing option (:none, :c14n, :normalize, :format, :rendered)

Behaviors for each dimension:

  • :strict - Must match exactly

  • :normalize - Match after normalization

  • :ignore - Don’t compare

  • :strip - (attribute_values only) Strip leading/trailing whitespace

  • :compact - (attribute_values only) Collapse internal whitespace

Validation at definition time:

The Profile DSL validates immediately when you define the profile:

# This raises Canon::Error at definition time
Canon::Comparison.define_profile(:invalid) do
  unknown_dimension :strict  # => Error: Unknown dimension: unknown_dimension
  text_content :invalid_behavior  # => Error: Invalid behavior 'invalid_behavior'
end

This prevents invalid profiles from ever being used in comparisons.

Removing custom profiles:

# Remove a custom profile
Canon::Comparison.remove_profile(:content_focused)

Profile best practices:

  • Use preset profiles when possible - they’re well-tested and documented

  • Name custom profiles descriptively (e.g., :content_focused, :seo_test)

  • Define profiles at application startup, not during request handling

  • Document why a custom profile is needed in comments

6.8. Algorithm interaction with match options

Both algorithms (DOM and Semantic) work with ALL formats. The algorithm determines HOW nodes are matched, not WHAT is compared:

  • DOM algorithm: Position-based matching (element at position 0 matches element at position 0)

  • Semantic algorithm: Signature-based matching (nodes with similar signatures match)

Once nodes are matched, both algorithms use the same dimension comparisons configured by the profile.

6.9. Usage

Example 5. Using the new unified profile parameter
# Using a preset profile
Canon::Comparison.equivalent?(doc1, doc2,
  profile: :spec_friendly
)

# Using an inline custom profile
Canon::Comparison.equivalent?(doc1, doc2,
  profile: {
    text_content: :normalize,
    structural_whitespace: :ignore,
    comments: :ignore
  }
)

# Defining and using a custom profile
Canon::Comparison.define_profile(:my_custom) do
  text_content :normalize
  comments :ignore
  preprocessing :rendered
end

Canon::Comparison.equivalent?(doc1, doc2,
  profile: :my_custom
)
Example 6. Using dimensions (deprecated - use profile instead)
Canon::Comparison.equivalent?(doc1, doc2,
  match: {
    text_content: :normalize,
    structural_whitespace: :ignore,
    comments: :ignore
  }
)

See Match Options for complete reference.

7. Layer 4: Diff formatting

7.1. Purpose

Control how differences are displayed. This layer is algorithm-specific - each algorithm generates different output types.

7.2. Diff modes

by_line

Traditional line-by-line diff

  • Natural fit for DOM algorithm

  • Shows positional changes

  • Traditional diff format

by_object

Tree-based semantic diff

  • Natural fit for Semantic algorithm

  • Shows structural operations

  • Visual tree representation

7.3. Algorithm-specific output

Critical: Each algorithm produces fundamentally different output!

  • DOM algorithm: Generates line-based differences

  • Semantic algorithm: Generates operation-based differences (INSERT, DELETE, UPDATE, MOVE)

See Algorithm-Specific Output for detailed comparison.

7.4. Diff options

use_color

Enable/disable ANSI color codes (default: true)

context_lines

Number of unchanged lines around changes (default: 3)

diff_grouping_lines

Group changes within N lines (default: nil)

See Diff Formatting for details.

7.5. Usage

Example 7. Ruby API
Canon::Comparison.equivalent?(doc1, doc2,
  verbose: true,
  diff_mode: :by_line,
  use_color: true,
  context_lines: 5,
  diff_grouping_lines: 10
)
Example 8. CLI
$ canon diff file1.xml file2.xml \
  --verbose \
  --diff-mode by-line \
  --context-lines 5 \
  --diff-grouping-lines 10

8. Complete example: All 4 layers

Here’s a full configuration showing all 4 layers working together:

result = Canon::Comparison.equivalent?(doc1, doc2,
  # Layer 1: Preprocessing
  preprocessing: :normalize,

  # Layer 2: Algorithm
  diff_algorithm: :semantic,

  # Layer 3: Match Options (new unified profile API)
  profile: :spec_friendly,

  # Layer 4: Diff Formatting
  verbose: true,
  diff_mode: :by_object,
  use_color: true,
  context_lines: 3
)

See Comparison Pipeline for layer-by-layer examples.

8.1. DiffNode: Representation of differences

8.1.1. Purpose

DiffNode objects represent individual differences between documents. Each DiffNode carries complete information about what changed, where it changed, and how to display it.

8.1.2. DiffNode structure

class DiffNode
  # Core properties
  attr_reader :node1, :node2           # Raw node references
  attr_accessor :dimension, :reason    # What changed and why
  attr_accessor :normative, :formatting # Classification

  # Location and display information
  attr_accessor :path                  # Canonical path with ordinal indices
  attr_accessor :serialized_before     # Serialized "before" content
  attr_accessor :serialized_after      # Serialized "after" content
  attr_accessor :attributes_before     # Normalized "before" attributes
  attr_accessor :attributes_after      # Normalized "after" attributes

  # Character-level enrichment (populated by DiffNodeEnricher)
  attr_accessor :char_ranges           # Array<DiffCharRange> char positions
  attr_accessor :line_range_before     # [start_line, end_line] in text1
  attr_accessor :line_range_after      # [start_line, end_line] in text2
end
Properties explained

Core properties:

node1, node2

Raw node references from the original documents

dimension

What type of difference (:text_content, :attribute_values, :element_structure, etc.)

reason

Human-readable explanation of the difference

normative

Whether this difference affects semantic equivalence (true) or is just formatting (false)

formatting

Whether this is a purely cosmetic whitespace difference

Location and display properties:

path

Canonical XPath-like path with ordinal indices that uniquely identifies the node location (e.g., /#document/div[0]/body[0]/p[1]/span[2])

serialized_before

Serialized content of the "before" state captured at comparison time

serialized_after

Serialized content of the "after" state captured at comparison time

attributes_before

Normalized attribute hash from the "before" state

attributes_after

Normalized attribute hash from the "after" state

8.1.3. Using DiffNode in verbose output

When you enable verbose mode, Canon returns a ComparisonResult containing DiffNode objects:

result = Canon::Comparison.equivalent?(doc1, doc2, verbose: true)

# Access individual differences
result.differences.each do |diff|
  puts "Location: #{diff.path}"
  puts "Dimension: #{diff.dimension}"
  puts "Reason: #{diff.reason}"
  puts "Normative: #{diff.normative?}"
end

8.1.4. Canonical paths with ordinal indices

DiffNode paths use ordinal indices to uniquely identify nodes. Instead of ambiguous paths like:

/#document-fragment/div/p/span/span

Canon generates precise paths like:

/#document-fragment/div[0]/p[1]/span[2]/span[0]

This tells you exactly which element changed: * div[0] - First div element * p[1] - Second paragraph element * span[2] - Third span element * span[0] - First nested span element

8.1.5. Enriched metadata in diff output

Layer 4 (diff formatting) uses enriched metadata to display accurate before/after content:

🔍 DIFFERENCE #1/3 [NORMATIVE]
════════════════════════════════════════════════════════════════════════
Dimension: element_structure
Location:  /#document/div[0]/body[0]/p[1]/span[2]

⊖ Expected (File 1):
   (not present)

⊕ Actual (File 2):
   <span id="new-element">Added content</span>

✨ Changes:
   Element inserted

The Location field shows the enriched path, and the before/after content uses serialized_before and serialized_after to ensure accurate display.

See Internals for implementation details on PathBuilder, NodeSerializer, and how metadata flows through the comparison layers.

9. Configuration precedence

When options are specified in multiple places, Canon resolves them using this hierarchy (highest to lowest priority):

1. Per-comparison explicit options (highest)
   ↓
2. Per-comparison profile
   ↓
3. Global configuration explicit options
   ↓
4. Global configuration profile
   ↓
5. Format defaults (lowest)
Example 9. Precedence example

Global configuration:

Canon::RSpecMatchers.configure do |config|
  config.xml.match.profile = :spec_friendly
  config.xml.match.options = { comments: :strict }
end

Per-test usage:

expect(actual).to be_xml_equivalent_to(expected)
  .with_profile(:rendered)
  .with_options(structural_whitespace: :ignore)

Final resolved options:

  • text_content: :normalize (from :rendered per-test profile)

  • structural_whitespace: :ignore (from per-test explicit option)

  • comments: :strict (from global explicit option)

  • Other dimensions use :rendered profile or format defaults

10. Benefits of 4-layer architecture

Separation of concerns

Each layer has a single responsibility

Composability

Mix and match preprocessing, algorithm, matching, and rendering options

Algorithm flexibility

Choose between speed (DOM) and intelligence (Semantic)

Testability

Each layer can be tested independently

Flexibility

Fine-grained control over comparison behavior

Clarity

Clear data flow from input to output

Extensibility

Easy to add new preprocessing, algorithms, dimensions, or rendering modes

11. Profile DSL and Dimension System

11.1. Overview

Canon 2.0 introduces a Profile DSL and Dimension system for cleaner, more maintainable comparison configuration:

  • Profile DSL - Define custom comparison profiles with validation

  • Dimension Classes - Object-oriented dimension handling with reusable behaviors

11.2. Profile DSL

The Profile DSL provides a clean, validated way to define custom comparison profiles:

# Define a custom profile
Canon::Comparison.define_profile(:content_focused) do
  text_content :normalize
  comments :ignore
  structural_whitespace :ignore
  attribute_values :normalize
  preprocessing :rendered
end

# Use the custom profile
Canon::Comparison.equivalent?(html1, html2, profile: :content_focused)

# List all available profiles
Canon::Comparison.available_profiles
# => [:strict, :rendered, :html4, :html5, :spec_friendly, :content_only, :content_focused]

Available dimensions:

  • text_content - Text within elements/values

  • structural_whitespace - Whitespace between elements

  • attribute_presence - Which attributes exist

  • attribute_order - Order of attributes

  • attribute_values - Attribute value content

  • element_position - Position of elements

  • comments - Comment content and placement

Behaviors for each dimension:

  • :strict - Must match exactly

  • :normalize - Match after normalization

  • :ignore - Don’t compare

Validation:

The Profile DSL validates at definition time:

# This raises an error at definition time
Canon::Comparison.define_profile(:invalid) do
  unknown_dimension :strict  # => Error: Unknown dimension: unknown_dimension
  text_content :invalid_behavior  # => Error: Invalid behavior 'invalid_behavior'
end

11.3. Dimension Classes

Behind the scenes, Canon uses dimension classes that encapsulate comparison logic:

# Each dimension knows how to extract and compare data
dimension = Canon::Comparison::Dimensions::Registry.get(:text_content)

# Extract data from a node
text = dimension.extract_data(node)

# Compare according to behavior
dimension.equivalent?(node1, node2, :normalize)

Available dimension classes:

  • TextContentDimension - Text content comparison

  • CommentsDimension - Comment comparison

  • AttributeValuesDimension - Attribute values comparison

  • AttributePresenceDimension - Attribute presence comparison

  • AttributeOrderDimension - Attribute order comparison

  • ElementPositionDimension - Element position comparison

  • StructuralWhitespaceDimension - Structural whitespace comparison

11.4. Refactored Module Structure

Canon’s internal modules have been reorganized for better separation of concerns:

  • XmlComparatorHelpers - Node parsing, attribute comparison, namespace comparison

  • DiffDetailFormatterHelpers - Location extraction, node utilities, text utilities, dimension formatting

  • Dimensions - Reusable dimension classes for comparison

This refactoring improves: - Maintainability - Each module has a single responsibility - Testability - Modules can be tested independently - Extensibility - New dimensions/formatters can be added easily - Code organization - Related functionality is grouped together

12. See also