1. Purpose

This page describes Canon’s XML format support, including W3C Canonical XML implementation, namespace handling, and XML-specific features.

2. Canonicalization

Canon implements the W3C Canonical XML Version 1.1 specification.

Key features:

  • Namespace declaration ordering (lexicographic by prefix)

  • Attribute ordering (lexicographic by namespace URI, then local name)

  • Character encoding normalization to UTF-8

  • Special character encoding in text and attributes

  • Removal of superfluous namespace declarations

  • Support for xml:base, xml:lang, xml:space, and xml:id attributes

  • Processing instruction and comment handling

  • Document subset support with attribute inheritance

Example 1. XML canonicalization example
xml = <<~XML
  <root xmlns:b="http://b.com" xmlns:a="http://a.com">
    <item b:attr="2" a:attr="1">
      Text   content
    </item>
  </root>
XML

Canon.format(xml, :xml)
# => Namespace prefixes sorted, attributes sorted, whitespace normalized

3. Format defaults

Dimension Default Behavior

text_content

:strict

structural_whitespace

:strict

attribute_whitespace

:strict

attribute_order

:ignore

attribute_values

:strict

comments

:strict

Default diff mode: :by_object (tree-based semantic diff)

XML attribute_order defaults to :ignore because the XML specification states that attribute order is not significant. Use the strict profile if you need to enforce specific attribute ordering.

4. Match profiles for XML

Canon provides predefined profiles optimized for XML documents. Each profile configures preprocessing, match options, diff algorithm, and formatting.

4.1. strict profile

Purpose: Character-perfect XML matching

Configuration:

{
  preprocessing: :none,
  diff_algorithm: :dom,      # DOM-based positional diff
  diff_mode: :by_object,     # Tree-based diff output
  match: {
    text_content: :strict,
    structural_whitespace: :strict,
    attribute_whitespace: :strict,
    attribute_order: :strict,
    attribute_values: :strict,
    comments: :strict
  }
}

Use when: Testing exact serializer output, verifying XML formatting compliance, character-perfect matching required.

4.2. rendered profile

Purpose: Browser-rendered equivalence

Configuration:

{
  preprocessing: :none,
  diff_algorithm: :dom,
  diff_mode: :by_line,       # Line-based diff output
  match: {
    text_content: :normalize,
    structural_whitespace: :normalize,
    attribute_whitespace: :normalize,
    attribute_order: :ignore,
    attribute_values: :strict,
    comments: :ignore
  }
}

Use when: Comparing how content would render (XHTML), ignoring formatting that doesn’t affect display.

4.3. spec_friendly profile

Purpose: Test-friendly comparison for RSpec

Configuration:

{
  preprocessing: :normalize,  # Applies whitespace normalization
  diff_algorithm: :dom,
  diff_mode: :by_object,
  match: {
    text_content: :normalize,
    structural_whitespace: :ignore,
    attribute_whitespace: :normalize,
    attribute_order: :ignore,
    attribute_values: :strict,
    comments: :ignore
  }
}

Use when: Writing RSpec tests, testing semantic correctness, ignoring pretty-printing differences. Most common for testing.

4.4. content_only profile

Purpose: Maximum tolerance - only data matters

Configuration:

{
  preprocessing: :normalize,
  diff_algorithm: :dom,
  diff_mode: :by_object,
  match: {
    text_content: :normalize,
    structural_whitespace: :ignore,
    attribute_whitespace: :ignore,
    attribute_order: :ignore,
    attribute_values: :ignore,
    comments: :ignore
  }
}

Use when: Only structural equivalence needed, maximum flexibility for formatting differences.

5. XML-specific features

5.1. XML declaration handling

The XML declaration (<?xml version="1.0" encoding="UTF-8"?>) is handled differently depending on the operation:

Operation XML Declaration

Canon.format_xml (pretty)

Preserved

Canon.format_xml (c14n)

Removed (per W3C C14N spec)

Canon::Comparison.equivalent?

Stripped during preprocessing

RSpec matchers

Stripped during preprocessing

This means documents with and without XML declarations are considered equivalent when using the comparison API.

5.1.1. Case-insensitive declaration attributes

Per the XML specification, certain XML declaration attributes have case-insensitive values. Canon normalizes these during formatting comparison so that differences in letter casing are treated as formatting-only rather than normative differences.

Case-insensitive attributes:

  • encoding — e.g., encoding="UTF-8" equals encoding="utf-8"

  • standalone — e.g., standalone="Yes" equals standalone="yes"

These attributes are normalized to lowercase during formatting-level comparison in Canon::Diff::FormattingDetector. This applies when determining whether a line-level difference is formatting-only (e.g., in by_line diff mode).

5.2. Comment handling

XML comments are preserved in canonical form unless --with-comments is explicitly set.

Example 2. Comment handling example
xml_with_comments = <<~XML
  <root>
    <!-- Important note -->
    <item>Value</item>
  </root>
XML

# Comments preserved by default
Canon.format(xml_with_comments, :xml)

# Ignore comments in comparison
Canon::Comparison.equivalent?(xml1, xml2,
  match: { comments: :ignore }
)

5.3. Namespace normalization

Namespace declarations are sorted and duplicate declarations are removed.

Example 3. Namespace normalization example
<!-- Before -->
<root xmlns:z="http://z.com" xmlns:a="http://a.com">
  <item xmlns:z="http://z.com">Content</item>
</root>

<!-- After canonicalization -->
<root xmlns:a="http://a.com" xmlns:z="http://z.com">
  <item>Content</item>
</root>

Namespaces are sorted alphabetically by prefix, and redundant declarations are removed.

=== Namespace comparison semantics

Canon compares XML elements using their namespace URI and local name, following the XML specification. This means elements are identified by the pair {namespace_uri, local_name} rather than by their qualified name (prefix:local_name).

Key principles:

  • Elements with different prefixes but the same namespace URI are considered equivalent

  • Namespace prefixes themselves have no semantic meaning

  • Inherited namespaces are treated the same as explicitly declared namespaces

  • The diff output shows namespace information when namespaces differ

# These are semantically equivalent
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>'

Canon::Comparison.equivalent?(xml1, xml2)
# => true

# Same local name, different namespace URIs - NOT equivalent
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>'

Canon::Comparison.equivalent?(xml3, xml4)
# => false
<!-- Inherited namespace -->
<root xmlns="http://example.com">
  <item>value</item>  <!-- item is in http://example.com namespace -->
</root>

<!-- Explicit namespace -->
<root>
  <item xmlns="http://example.com">value</item>
</root>

<!-- Both items have namespace_uri = "http://example.com" -->
<!-- Canon considers them equivalent in namespace comparison -->

Diff output with namespaces:

When elements differ in namespace, the diff output includes namespace annotations:

- <item> [namespace: http://example.com] "value"
+ <item> [namespace: http://other.com] "value"

This makes it clear when namespace differences are causing comparison failures.

=== xml: attributes

Special attributes like xml:lang, xml:space, xml:id, and xml:base are properly handled per specification.

xml:space example
[source,xml]

<root xml:space="preserve"> <pre> Whitespace preserved </pre> </root>

When `xml:space="preserve"` is set, whitespace is preserved in descendants.

=== Cross-encoding comparison

Canon automatically normalizes XML character encodings before comparison, enabling cross-encoding comparisons to work correctly.

Supported encodings: UTF-8, UTF-16 (all variants), Shift_JIS, EUC-JP, ISO-8859-1, and more.

How it works:

  1. Extract the declared encoding from the XML declaration (e.g., encoding="Shift_JIS")

  2. If declared encoding differs from UTF-8, transcode to UTF-8

  3. Handle cases where the declared encoding doesn’t match actual bytes

  4. Use safe transcoding with replacement characters for invalid sequences

# UTF-8 vs Shift_JIS - automatically normalized
xml1 = "<root>日本語</root>"  # UTF-8
xml2 = "<root>日本語</root>".encode("Shift_JIS")  # Shift_JIS

Canon::Comparison.equivalent?(xml1, xml2)
# => true (automatically transcoded to UTF-8 before comparison)

# ASCII content works across all encodings
xml3 = "<root>hello</root>"
xml4 = "<root>hello</root>".encode("ISO-8859-1")

Canon::Comparison.equivalent?(xml3, xml4)
# => true

This means you can compare XML files from different sources or systems without worrying about their native encoding.

== Usage examples

=== Basic XML comparison

xml1 = File.read("file1.xml")
xml2 = File.read("file2.xml")

Canon::Comparison.equivalent?(xml1, xml2)

=== Test-friendly XML comparison

expect(actual_xml).to be_xml_equivalent_to(expected_xml)
  .with_profile(:spec_friendly)

=== Using XML comparator directly

Canon::Comparison::XmlComparator.equivalent?(xml1, xml2,
  match: { attribute_order: :ignore }
)

=== CLI usage

# Basic comparison
canon diff file1.xml file2.xml --verbose

# With spec-friendly profile
canon diff expected.xml actual.xml \
  --match-profile spec_friendly \
  --verbose

== See also