- 1. Purpose
- 2. Overview
- 3. Child Pages
- 4. Match dimensions overview
- For XML: structural whitespace is stripped by default
- Use preserve_whitespace_elements to preserve whitespace in specific elements
- With preserve_whitespace_elements, whitespace inside <item> is preserved
- Structural whitespace differs - NOT equivalent
- Text content whitespace differs - NOT equivalent with text_content: :strict
- Make <sample> elements preserve whitespace exactly (strings, not symbols)
- Make <script> NOT whitespace-preserved (override HTML default)
- HTML defaults: pre, code, textarea, script, style are :preserve
- Adding code to strip list removes it from preserve
- With :code in strip list, whitespace in <code> is normalized (formatting-only)
- HTML uses text_content: :normalize by default
- Without blacklisting, <code> is sensitive (whitespace matters)
- These are equivalent (same namespace URI, different prefixes)
- These are NOT equivalent (different namespace URIs)
- Use specific dimensions
- Use a profile
- Profile with dimension overrides
- Use semantic dimensions
- Use profile
- Override specific dimensions
- Combine profile with overrides
- Use semantic algorithm with flexible positioning
- Global configuration
- Per-test override
- Per-test dimension override
- Semantic algorithm with flexible hierarchy
- Element-level whitespace sensitivity (strings, not symbols)
- Override HTML default whitespace-sensitive elements
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
-
Match Dimensions - Detailed reference for all dimensions
-
Match Profiles - Predefined configurations
-
Algorithm-Specific Behavior - How DOM and Semantic algorithms interpret options differently
-
HTML-Specific Policies - HTML format-specific comparison policies
-
Pretty-Printed Fixture Support - Comparing compact generated XML against hand-indented fixture heredocs without spurious whitespace differences
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
# 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.
# By default, space and NBSP are different
xml1 = '<root><span>ISO</span> <span>712</span></root>'
xml2 = '<root><span>ISO</span> <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)
# 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
# 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]
-
Per-comparison explicit options (highest) ↓
-
Per-comparison profile ↓
-
Global configuration explicit options ↓
-
Global configuration profile ↓
-
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
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
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
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
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 |
|---|---|---|
|
|
Comment diffs hidden (informative) |
|
|
Comment diffs shown in cyan (informative) |
|
|
Comment diffs shown in red/green (normative) |
|
|
Comment diffs hidden (normative) |
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`