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
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|
|
Preserved |
|
Removed (per W3C C14N spec) |
|
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"equalsencoding="utf-8" -
standalone— e.g.,standalone="Yes"equalsstandalone="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.
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.
<!-- 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.
[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:
-
Extract the declared encoding from the XML declaration (e.g.,
encoding="Shift_JIS") -
If declared encoding differs from UTF-8, transcode to UTF-8
-
Handle cases where the declared encoding doesn’t match actual bytes
-
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
-
Comparison Pipeline - Understanding the 4 layers
-
Match Options - All matching options
-
Choosing Configuration - Decision guide
-
Format Support - Overview of all formats
-
HTML Format - HTML-specific features