Table of Contents

1. Purpose

This section provides a complete reference for Canon’s match options, including match dimensions, behaviors, and predefined profiles.

Match options control Layer 3 (Match Options) of Canon’s 4-layer comparison architecture. See Comparison Pipeline for the complete flow.

2. Overview

Match options control which aspects of documents are compared and how strictly they are compared. Canon provides:

  • Match dimensions: Independent aspects of documents (text, whitespace, attributes, etc.)

  • Dimension behaviors: How each dimension is compared (:strict, :normalize, :ignore)

  • Match profiles: Predefined combinations for common scenarios

Important: Match options behave differently with each algorithm. See Algorithm-Specific Behavior for details.

3. Child Pages

4. Match dimensions overview

Match dimensions are orthogonal aspects that can be configured independently.

4.1. text_content

Applies to: All formats

Purpose: Controls how text content within elements/values is compared.

Behaviors:

:strict

Text must match exactly, character-for-character including all whitespace

:normalize

Whitespace is normalized (collapsed/trimmed) before comparison. Formatting-only differences (e.g., extra spaces around text) are classified as informative rather than normative. This means documents with only whitespace differences in text content are considered equivalent.

:ignore

Text content is completely ignored in comparison

Example 1. Using text_content: :normalize
# These are equivalent with :normalize
# Whitespace differences are formatting-only (informative)
Canon.equivalent?(
  '<p>  text  </p>',
  '<p>text</p>',
  match: { text_content: :normalize }
)
# => true

# These differ in :strict mode
Canon.equivalent?(
  '<p>  text  </p>',
  '<p>text</p>',
  match: { text_content: :strict }
)
# => false

4.2. structural_whitespace

Applies to: All formats

Purpose: Controls how whitespace between elements (indentation, newlines) is handled.

Behaviors:

:strict

All structural whitespace must match exactly

:normalize

Structural whitespace is normalized

:ignore

Structural whitespace is completely ignored

4.3. whitespace_type

Applies to: XML, HTML

Purpose: Controls whether different Unicode whitespace characters (space, NBSP, ideographic space, etc.) are treated as equivalent or distinct.

Behaviors:

:strict

(default) Different Unicode whitespace types are significant. Space (U+0020) and NBSP (U+00A0) are treated as different characters. This is useful for catching accidental insertion of wrong whitespace types (e.g., a pasted NBSP where a regular space was intended).

:normalize

All Unicode whitespace characters are collapsed to a single space before comparison. Space, NBSP, ideographic space (U+3000), and other Unicode whitespace characters are treated as equivalent.

Example 2. Using whitespace_type: :strict (default)
# By default, space and NBSP are different
xml1 = '<root><span>ISO</span> <span>712</span></root>'
xml2 = '<root><span>ISO</span>&#xa0;<span>712</span></root>'

Canon::Comparison.equivalent?(xml1, xml2,
  match_profile: :spec_friendly
)
# => false (NBSP detected as different from space)

# Opt into treating all whitespace types as equivalent
Canon::Comparison.equivalent?(xml1, xml2,
  match_profile: :spec_friendly,
  match: { whitespace_type: :normalize }
)
# => true

4.4. Whitespace sensitivity at element level

4.4.1. General

In XML, whitespace sensitivity can vary by schema and element:

  • Elements that apply xml:space="preserve" are whitespace-sensitive.

  • Other elements may be defined as sensitive by schema (e.g. xs:space="preserve" in XML Schema) or unannounced conventions, such as for mixed content.

In HTML, elements like <pre> and <code> preserve whitespace, while others like <div> and <p> do not.

In the unannounced cases, the developer must indicate which elements are whitespace-sensitive.

In Canon, you can control whitespace sensitivity at the element level using structural_whitespace: :strict or text_content: :normalize.

Element-level sensitivity controls both:

  • structural_whitespace: Whether whitespace between elements in the element is preserved

  • text_content: Whether whitespace within text nodes of the element is normalized

Options for controlling element-level sensitivity include:

  • xml:space attribute - XML standard for declaring whitespace sensitivity in documents

  • whitelist/blacklist options - User-specified element lists

  • Format defaults - HTML has built-in sensitive elements

  • respect_xml_space option - Control whether xml:space is honored

For elements marked as sensitive, whitespace differences are always normative.

For non-sensitive elements using text_content: :normalize, whitespace differences are classified as formatting-only (informative).

4.4.2. xml:space attribute support

The xml:space attribute is the XML standard way to declare whitespace sensitivity in XML instance documents:

<!-- Preserve whitespace in this element -->
<code xml:space="preserve">
  Indentation and newlines matter here
</code>

<!-- Use default behavior -->
<text xml:space="default">
  Whitespace handling follows configured behavior
</text>

The xml:space attribute affects both structural whitespace and text content:

  • Structural whitespace (whitespace-only text nodes between child elements)

  • Text content whitespace (whitespace within text nodes)

Example 3. xml:space with structural_whitespace
# With xml:space="preserve", structural whitespace is preserved
xml1 = "<root xml:space='preserve'>\n  <text>Hello</text>\n</root>"
xml2 = "<root xml:space='preserve'><text>Hello</text></root>"

# These are NOT equivalent (structural whitespace differs)
Canon::Comparison.equivalent?(xml1, xml2)
# => false
Example 4. xml:space with text_content
# With xml:space="preserve", text content whitespace is preserved
xml1 = '<root xml:space="preserve"><code>  indented  </code></root>'
xml2 = '<root xml:space="preserve"><code>indented</code></root>'

# These are NOT equivalent (text whitespace differs)
Canon::Comparison.equivalent?(xml1, xml2,
  match: { text_content: :strict }
)
# => false

4.4.3. Element classification options

You can explicitly classify elements for whitespace handling:

Canon::Comparison.equivalent?(xml1, xml2,
  match: {
    structural_whitespace: :strict,
    preserve_whitespace_elements: ["pre", "code", "sample"],
    collapse_whitespace_elements: ["p", "li", "td"],
    strip_whitespace_elements: ["div", "span"]
  }
)

Element names are strings (not symbols) for consistency with XML/HTML conventions.

Priority order: strip > preserve > collapse > format defaults.

4.4.4. respect_xml_space option

Control whether xml:space attributes in the document are honored:

# Honor xml:space (default)
Canon::Comparison.equivalent?(xml1, xml2,
  match: {
    structural_whitespace: :strict,
    respect_xml_space: true  # Use xml:space attributes in document
  }
)

# Ignore xml:space, use only user configuration
Canon::Comparison.equivalent?(xml1, xml2,
  match: {
    structural_whitespace: :strict,
    respect_xml_space: false  # Override document declarations
  }
)

4.4.5. Priority order

When determining if an element is whitespace-sensitive, Canon uses this priority:

1. respect_xml_space: false → User config only (ignore xml:space)
   ↓
2. Ancestor walk (strip wins; then preserve; then collapse)
   ↓
3. xml:space="preserve" → Element classified as :preserve
   ↓
4. xml:space="default" → Use configured behaviour
   ↓
5. Format defaults (HTML: preserve + collapse; XML: strip)

==== Format-specific defaults

**HTML preserve**:: `["pre", "textarea", "script", "style"]` - Every whitespace character significant
**HTML collapse**:: `["p", "li", "td", "th", "dt", "dd", "h1"-"h6", ...]` - Whitespace collapsed
**XML**:: No defaults — all elements are :strip unless explicitly configured

==== Two types of whitespace sensitivity

Canon handles two distinct whitespace concerns:

**1. Structural whitespace stripping** — whitespace-only text nodes between sibling elements (indentation, newlines). These are never semantically meaningful and are stripped by default for XML to enable ElementMatcher to work correctly.

**2. Text content comparison** — how non-whitespace text content is compared. Controlled by `structural_whitespace` and `text_content` dimension behaviors (`:strict`, `:normalize`, `:ignore`).

The `preserve_whitespace_elements`, `collapse_whitespace_elements`, and `strip_whitespace_elements` options control both concerns:

[source,ruby]

For XML: structural whitespace is stripped by default

Use preserve_whitespace_elements to preserve whitespace in specific elements

xml1 = "<root><item>Test</item></root>" xml2 = "<root>\n <item>Test</item>\n</root>"

With preserve_whitespace_elements, whitespace inside <item> is preserved

Canon::Comparison.equivalent?(xml1, xml2, match: { structural_whitespace: :strict, preserve_whitespace_elements: ["item"] } ) # ⇒ true

**Precedence**: `strip_whitespace_elements` > `preserve_whitespace_elements` > `collapse_whitespace_elements` > format defaults

**Ancestor-based**: The closest matching ancestor determines classification.

==== Examples

.Using xml:space="preserve" for structural whitespace
[source,ruby]

xml1 = "<root xml:space='preserve'>\n <text>Hello</text>\n</root>" xml2 = "<root xml:space='preserve'><text>Hello</text></root>"

Structural whitespace differs - NOT equivalent

Canon::Comparison.equivalent?(xml1, xml2) # ⇒ false

.Using xml:space="preserve" for text content
[source,ruby]

xml1 = '<root><code xml:space="preserve"> multiple spaces </code></root>' xml2 = '<root><code xml:space="preserve">multiple spaces</code></root>'

Text content whitespace differs - NOT equivalent with text_content: :strict

Canon::Comparison.equivalent?(xml1, xml2, match: { text_content: :strict } ) # ⇒ false

.Using preserve_whitespace_elements
[source,ruby]

Make <sample> elements preserve whitespace exactly (strings, not symbols)

xml1 = "<sample>\n content\n</sample>" xml2 = "<sample>content</sample>"

Canon::Comparison.equivalent?(xml1, xml2, match: { structural_whitespace: :strict, preserve_whitespace_elements: ["sample"] } ) # ⇒ false (structural whitespace differs in <sample>)

.Overriding HTML defaults
[source,ruby]

Make <script> NOT whitespace-preserved (override HTML default)

Canon::Comparison.equivalent?(html1, html2, format: :html, match: { structural_whitespace: :strict, strip_whitespace_elements: ["script"] } )

.Using text_content: :normalize with strip_whitespace_elements
[source,ruby]

HTML defaults: pre, code, textarea, script, style are :preserve

Adding code to strip list removes it from preserve

html1 = '<root><pre> indented </pre><code> code </code></root>' html2 = '<root><pre> indented </pre><code>code</code></root>'

With :code in strip list, whitespace in <code> is normalized (formatting-only)

HTML uses text_content: :normalize by default

Canon::Comparison.equivalent?(html1, html2, format: :html, match: { strip_whitespace_elements: [:code], } ) # ⇒ true (whitespace differences in <code> are formatting-only)

Without blacklisting, <code> is sensitive (whitespace matters)

Canon::Comparison.equivalent?(html1, html2, format: :html, match: { structural_whitespace: :strict, } ) # ⇒ false (whitespace in <code> is normative)

=== attribute_whitespace

**Applies to**: XML, HTML only

**Purpose**: Controls how whitespace in attribute values is handled.

**Behaviors**:

`:strict`:: Attribute value whitespace must match exactly

`:normalize`:: Whitespace in attribute values is normalized

`:ignore`:: Whitespace in attribute values is ignored

=== attribute_order

**Applies to**: XML, HTML only

**Purpose**: Controls whether attribute order matters.

**Behaviors**:

`:strict`:: Attributes must appear in the same order

`:ignore`:: Attribute order doesn't matter (set-based comparison)

=== attribute_values

**Applies to**: XML, HTML only

**Purpose**: Controls how attribute values are compared.

**Behaviors**:

`:strict`:: Attribute values must match exactly

`:normalize`:: Whitespace in values is normalized

`:ignore`:: Only attribute presence is checked, values ignored

=== key_order

**Applies to**: JSON, YAML only

**Purpose**: Controls whether object key order matters.

**Behaviors**:

`:strict`:: Keys must appear in the same order

`:ignore`:: Key order doesn't matter (unordered comparison)

=== comments

**Applies to**: XML, HTML, YAML (JSON doesn't support comments in standard spec)

**Purpose**: Controls how comments are compared.

**Behaviors**:

`:strict`:: Comments must match exactly (including whitespace)

`:normalize`:: Whitespace in comments is normalized

`:ignore`:: Comments are completely ignored

=== namespace_uri

**Applies to**: XML only

**Purpose**: Controls how XML element namespaces are compared. Elements are identified by the pair `{namespace_uri, local_name}` according to XML semantics.

**Behaviors**:

`:strict`:: Namespace URIs must match (default and only supported behavior)

**Note**: This dimension is always `:strict` for XML. Namespace prefixes are not significant - only the namespace URI matters. Elements with different prefixes but the same namespace URI are considered equivalent.

.Namespace URI comparison
[example]
====
[source,ruby]

These are equivalent (same namespace URI, different prefixes)

xml1 = '<root xmlns:a="http://example.com"><a:item>value</a:item></root>' xml2 = '<root xmlns:b="http://example.com"><b:item>value</b:item></root>'

These are NOT equivalent (different namespace URIs)

xml3 = '<root xmlns:a="http://example.com"><a:item>value</a:item></root>' xml4 = '<root xmlns:a="http://other.com"><a:item>value</a:item></root>'

====

== Match profiles overview

Profiles are predefined combinations of dimension settings for common scenarios.

=== strict

**Purpose**: Exact matching - all dimensions use `:strict` behavior.

**When to use**:

* Character-perfect matching required
* Testing exact serializer output
* Verifying formatting compliance
* Maximum strictness needed

=== rendered

**Purpose**: Mimics how browsers/CSS engines render content.

**When to use**:

* Comparing HTML rendered output
* Formatting doesn't affect display
* Testing web page generation
* Browser-equivalent comparison

=== spec_friendly

**Purpose**: Test-friendly comparison that ignores most formatting differences.

**When to use**:

* Writing RSpec tests
* Testing semantic correctness
* Ignoring pretty-printing differences
* Most common test scenario

=== content_only

**Purpose**: Only semantic content matters - maximum tolerance for formatting.

**When to use**:

* Only care about data, not presentation
* Maximum flexibility needed
* Comparing across different formats
* Structural equivalence only

== Format defaults

Each format has sensible defaults based on typical usage:

[cols="1,1,1,1,1"]
|===
|Dimension |XML |HTML |JSON |YAML

|`text_content`
|`:strict`
|`:normalize`
|`:strict`
|`:strict`

|`structural_whitespace`
|`:strict`
|`:normalize`
|`:strict`
|`:strict`

|`attribute_whitespace`
|`:strict`
|`:normalize`
|—
|—

|`attribute_order`
|`:ignore`
|`:ignore`
|—
|—

|`attribute_values`
|`:strict`
|`:strict`
|—
|—

|`key_order`
|—
|—
|`:strict`
|`:strict`

|`comments`
|`:strict`
|`:ignore`
|—
|`:strict`

|`namespace_uri`
|`:strict`
|—
|—
|—
|===

== Configuration precedence

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

[source]
  1. Per-comparison explicit options (highest) ↓

  2. Per-comparison profile ↓

  3. Global configuration explicit options ↓

  4. Global configuration profile ↓

  5. Format defaults (lowest)

.Precedence example
[example]
====
**Global configuration**:

[source,ruby]

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

The `:spec_friendly` profile sets:

* `text_content: :normalize`
* `structural_whitespace: :ignore`
* `comments: :ignore`

But the explicit `comments: :strict` overrides the profile setting.

**Per-test usage**:

[source,ruby]

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
====

== Usage examples

=== Ruby API

[source,ruby]

Use specific dimensions

Canon::Comparison.equivalent?(doc1, doc2, match: { text_content: :normalize, structural_whitespace: :ignore, comments: :ignore } )

Use a profile

Canon::Comparison.equivalent?(doc1, doc2, match_profile: :spec_friendly )

Profile with dimension overrides

Canon::Comparison.equivalent?(doc1, doc2, match_profile: :spec_friendly, match: { comments: :strict # Override profile } )

Use semantic dimensions

Canon::Comparison.equivalent?(doc1, doc2, diff_algorithm: :semantic, match: { element_position: :ignore, element_hierarchy: :ignore } )

=== CLI

[source,bash]

Use profile

$ canon diff file1.xml file2.xml \ --match-profile spec_friendly \ --verbose

Override specific dimensions

$ canon diff file1.xml file2.xml \ --text-content normalize \ --structural-whitespace ignore \ --verbose

Combine profile with overrides

$ canon diff file1.xml file2.xml \ --match-profile spec_friendly \ --comments strict \ --verbose

Use semantic algorithm with flexible positioning

$ canon diff file1.xml file2.xml \ --diff-algorithm semantic \ --element-position ignore \ --verbose

=== RSpec

[source,ruby]

Global configuration

Canon::RSpecMatchers.configure do |config| config.xml.match.profile = :spec_friendly config.xml.match.options = { text_content: :normalize, comments: :ignore } end

Per-test override

expect(actual).to be_xml_equivalent_to(expected) .with_profile(:strict)

Per-test dimension override

expect(actual).to be_xml_equivalent_to(expected) .with_options( structural_whitespace: :strict, text_content: :strict )

Semantic algorithm with flexible hierarchy

expect(actual).to be_xml_equivalent_to(expected, diff_algorithm: :semantic ) .with_options( element_position: :ignore, element_hierarchy: :ignore )

Element-level whitespace sensitivity (strings, not symbols)

expect(actual).to be_xml_equivalent_to(expected, match: { structural_whitespace: :strict } ) .with_options( preserve_whitespace_elements: ["pre", "code", "sample"], respect_xml_space: true )

Override HTML default whitespace-sensitive elements

expect(html).to be_html_equivalent_to(expected, match: { structural_whitespace: :strict } ) .with_options( strip_whitespace_elements: ["script", "style"] )

== Comments dimension

The comments dimension controls how comment nodes are matched and how their differences are classified in diff output.

=== Matching behaviors

strict

Comment content must match exactly. Differences are classified as normative (shown in red/green).

normalize

Comment text is normalized (whitespace collapsed) before matching. Differences are still classified as normative.

ignore

Comment content is compared but differences are classified as informative (shown in cyan/blue) rather than normative.

With comments: :ignore, comment nodes still participate in comparison and create DiffNodes. The difference is that these DiffNodes are marked as informative rather than normative. Use the show_diffs option to control visibility of informative diffs in the output.

=== Default values

  • XML: comments: :strict - Comment differences are normative

  • HTML: comments: :ignore - Comment differences are informative

=== Example: Comments as informative differences

Match comments but classify differences as informative
xml1 = '<root><!--comment 1--><child>text</child></root>'
xml2 = '<root><!--comment 2--><child>text</child></root>'

Canon.equivalent?(xml1, xml2,
  format: :xml,
  verbose: true,
  match: { comments: :ignore },  # Comment diffs → informative
  show_diffs: :all                # Show all diffs (cyan for comments)
)

In this example, the comment difference is still detected and included in the diff output, but is shown in cyan color to indicate it’s an informative difference.

=== Example: Hide informative differences

Hide informative differences including comments
Canon.equivalent?(xml1, xml2,
  format: :xml,
  verbose: true,
  match: { comments: :ignore },   # Comment diffs → informative
  show_diffs: :normative          # Hide informative diffs
)
# Returns empty string - no normative diffs to show

== Controlling diff visibility with show_diffs

The show_diffs option controls which differences appear in verbose output. This provides fine-grained control over what is displayed without affecting the comparison algorithm.

=== Values

:all (default)

Show all differences (both normative and informative)

:normative

Show only normative differences (hide informative)

:informative

Show only informative differences (hide normative)

=== Color scheme

Normative differences

Shown in red (removed) and green (added)

Informative differences

Shown in cyan/blue (both removed and added)

=== Three-stage pipeline

The comparison process follows a three-stage pipeline:

┌─────────────────────────────────────────────────────────────┐
│  STAGE 1: MATCHING - Compare all nodes                      │
│  • Parse XML/HTML to DOM or TreeNode                         │
│  • Compare ALL nodes (including comments)                    │
│  • Create DiffNodes for ALL differences                      │
└────────────────────┬────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│  STAGE 2: CLASSIFICATION - Mark normative vs informative    │
│  • DiffClassifier uses match_options                         │
│  • comments: :ignore → comment diffs = informative           │
│  • comments: :strict → comment diffs = normative             │
└────────────────────┬────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│  STAGE 3: RENDERING - Control visibility                    │
│  • show_diffs: :all → show everything                        │
│  • show_diffs: :normative → show only normative              │
│  • show_diffs: :informative → show only informative          │
└─────────────────────────────────────────────────────────────┘

=== Example: Show only normative differences

Show only normative differences (common use case)
result = Canon.equivalent?(xml1, xml2,
  format: :xml,
  verbose: true,
  show_diffs: :normative
)

This is useful when you want to focus on actual semantic differences and hide cosmetic ones like comment changes or whitespace formatting.

=== Example: Show only informative differences

Show only informative differences (debugging use case)
result = Canon.equivalent?(xml1, xml2,
  format: :xml,
  verbose: true,
  show_diffs: :informative
)

This is useful for debugging or reviewing formatting/cosmetic changes separately from semantic ones.

=== Combining comments and show_diffs

The comments match option and show_diffs option work together:

comments show_diffs Result

:ignore

:normative

Comment diffs hidden (informative)

:ignore

:all

Comment diffs shown in cyan (informative)

:strict

:normative

Comment diffs shown in red/green (normative)

:strict

:informative

Comment diffs hidden (normative)

Example: Mixed scenario

xml1 = '<root><!--old comment-→<child>text1</child></root>' xml2 = '<root><!--new comment-→<child>text2</child></root>'

result = Canon.equivalent?(xml1, xml2, format: :xml, verbose: true, match: { comments: :ignore, text_content: :strict }, show_diffs: :normative )

=== CLI usage

[source,bash]

# Show only normative differences canon diff file1.xml file2.xml --show-diffs=normative

# Show all differences (default) canon diff file1.xml file2.xml --show-diffs=all

# Show only informative differences canon diff file1.xml file2.xml --show-diffs=informative

=== RSpec usage

[source,ruby]

RSpec.describe 'XML comparison' do it 'matches despite comment differences' do expect(xml1).to be_xml_equivalent_to(xml2) .with_match(comments: :ignore) .show_diffs(:normative) end end

=== Environment variable

You can set a default value using the `CANON_SHOW_DIFFS` environment variable:

[source,bash]

export CANON_SHOW_DIFFS=normative

Valid values: `all`, `normative`, `informative`