1. Scope
This document describes how to use Canon from Ruby code. It covers formatting, parsing, and comparison APIs.
For command-line usage, see CLI documentation.
For RSpec testing, see RSpec documentation.
2. Choosing the right API
Canon provides two main categories of APIs with different purposes.
3. General
Canon provides a unified Ruby API for working with XML, HTML, JSON, and YAML formats. All methods follow consistent patterns across formats.
4. Formatting
4.1. Canonical formatting
The Canon.format method produces canonical output (compact, normalized).
Syntax:
Canon.format(content, format)
Canon.format_{format}(content) # Format-specific shorthand
Where:
content-
The input string
format-
The format type (
:xml,:html,:json, or:yaml)
require 'canon'
# XML - compact canonical form
xml = '<root><b>2</b><a>1</a></root>'
Canon.format(xml, :xml)
# => "<root><a>1</a><b>2</b></root>"
Canon.format_xml(xml) # Shorthand
# => "<root><a>1</a><b>2</b></root>"
# HTML - compact canonical form
html = '<div><p>Hello</p></div>'
Canon.format(html, :html)
Canon.format_html(html) # Shorthand
# JSON - canonical with sorted keys
json = '{"z":3,"a":1,"b":2}'
Canon.format(json, :json)
# => {"a":1,"b":2,"z":3}
# YAML - canonical with sorted keys
yaml = "z: 3\na: 1\nb: 2"
Canon.format(yaml, :yaml)
4.2. Pretty-print formatting
For human-readable output with indentation, use format-specific pretty printer classes.
Syntax:
Canon::{Format}::PrettyPrinter.new(indent: n, indent_type: type).format(content)
Where:
{Format}-
The format module (
Xml,Html,Json) n-
Number of spaces (default: 2) or tabs (use 1 for tabs)
type-
Indentation type:
'space'(default) or'tab' fixture_ready-
(HTML only) When
true, emit indented XHTML-shaped output that strips structural whitespace before formatting. Designed for copy-paste into RSpec heredoc fixtures. Default:false. content-
The input string
require 'canon/pretty_printer/xml'
require 'canon/pretty_printer/html'
require 'canon/pretty_printer/json'
xml_input = '<root><b>2</b><a>1</a></root>'
# XML with 2-space indentation (default)
Canon::Xml::PrettyPrinter.new(indent: 2).format(xml_input)
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <root>
# <a>1</a>
# <b>2</b>
# </root>
# XML with 4-space indentation
Canon::Xml::PrettyPrinter.new(indent: 4).format(xml_input)
# XML with tab indentation
Canon::Xml::PrettyPrinter.new(
indent: 1,
indent_type: 'tab'
).format(xml_input)
# HTML with 2-space indentation
html_input = '<div><p>Hello</p></div>'
Canon::Html::PrettyPrinter.new(indent: 2).format(html_input)
# HTML fixture-ready mode: produces indented XHTML-shaped output
# suitable for pasting into RSpec heredoc fixtures. Strips stray
# structural whitespace (inter-element text nodes) so libxml's FORMAT
# flag can indent block-level siblings that would otherwise be treated
# as mixed content. Whitespace inside <pre>, <script>, <style>, and
# <textarea> is preserved.
Canon::Html::PrettyPrinter.new(indent: 2, fixture_ready: true)
.format('<html><body><div>a</div> <div>b</div></body></html>')
# =>
# <html xmlns="http://www.w3.org/1999/xhtml">
# <head>...</head>
# <body>
# <div>a</div>
# <div>b</div>
# </body>
# </html>
# JSON with 2-space indentation
json_input = '{"z":3,"a":{"b":1}}'
Canon::Json::PrettyPrinter.new(indent: 2).format(json_input)
# JSON with tab indentation
Canon::Json::PrettyPrinter.new(
indent: 1,
indent_type: 'tab'
).format(json_input)
5. Parsing
The Canon.parse method parses content into Ruby objects or Nokogiri
documents.
Syntax:
Canon.parse(content, format)
Canon.parse_{format}(content) # Format-specific shorthand
Where:
content-
The input string
format-
The format type (
:xml,:html,:json, or:yaml)
# Parse XML → Nokogiri::XML::Document
xml_doc = Canon.parse(xml_input, :xml)
xml_doc = Canon.parse_xml(xml_input)
# Parse HTML → Nokogiri::HTML5::Document (or XML::Document for XHTML)
html_doc = Canon.parse(html_input, :html)
html_doc = Canon.parse_html(html_input)
# Parse JSON → Ruby Hash/Array
json_obj = Canon.parse(json_input, :json)
json_obj = Canon.parse_json(json_input)
# Parse YAML → Ruby Hash/Array
yaml_obj = Canon.parse(yaml_input, :yaml)
yaml_obj = Canon.parse_yaml(yaml_input)
6. Comparison
6.1. General
The Canon::Comparison.equivalent? method compares two documents semantically.
The comparison uses depth-first traversal of DOM trees (XML/HTML) or object graphs (JSON/YAML), comparing nodes/values based on configurable match dimensions.
See Match options for details on match dimensions and profiles.
6.2. Basic comparison
Syntax:
Canon::Comparison.equivalent?(obj1, obj2, options = {})
Where:
obj1-
First document (String, Nokogiri document, or Ruby object)
obj2-
Second document (String, Nokogiri document, or Ruby object)
options-
Hash of comparison options (optional)
Returns:
-
trueif documents are semantically equivalent -
falseif documents differ -
ComparisonResultobject ifverbose: true
Options:
-
diff_algorithm::dom(default) or:semantic- chooses the diff algorithm -
verbose:trueorfalse- returns detailed results when true -
match: Hash of match dimension options -
match_profile: Symbol specifying a predefined profile
require 'canon/comparison'
# HTML comparison - ignores whitespace by default
html1 = '<div><p>Hello</p></div>'
html2 = '<div> <p> Hello </p> </div>'
Canon::Comparison.equivalent?(html1, html2)
# => true
# XML comparison - element order doesn't matter for children
xml1 = '<root><a>1</a><b>2</b></root>'
xml2 = '<root> <b>2</b> <a>1</a> </root>'
Canon::Comparison.equivalent?(xml1, xml2)
# => true
# JSON comparison
json1 = '{"a":1,"b":2}'
json2 = '{"b":2,"a":1}'
Canon::Comparison.equivalent?(json1, json2)
# => true
# With Nokogiri documents
doc1 = Nokogiri::HTML5(html1)
doc2 = Nokogiri::HTML5(html2)
Canon::Comparison.equivalent?(doc1, doc2)
# => true
6.3. Comparison with match options
Match options control which aspects of documents are compared and how strictly.
Syntax:
Canon::Comparison.equivalent?(obj1, obj2,
match: {
dimension1: behavior1,
dimension2: behavior2,
...
}
)
See Match options for complete dimension reference.
# Normalize whitespace in text content
Canon::Comparison.equivalent?(xml1, xml2,
match: {
text_content: :normalize,
structural_whitespace: :ignore
}
)
# Ignore comments
Canon::Comparison.equivalent?(xml1, xml2,
match: {
comments: :ignore
}
)
# Strict attribute order
Canon::Comparison.equivalent?(xml1, xml2,
match: {
attribute_order: :strict
}
)
# Multiple dimensions
Canon::Comparison.equivalent?(html1, html2,
match: {
text_content: :normalize,
structural_whitespace: :ignore,
attribute_order: :ignore,
comments: :ignore
}
)
6.4. Using match profiles
Match profiles are predefined combinations of match dimension settings.
Syntax:
Canon::Comparison.equivalent?(obj1, obj2,
match_profile: :profile_name
)
Available profiles:
:strict-
Exact matching - all dimensions use
:strictbehavior :rendered-
Mimics browser rendering - ignores formatting differences
:spec_friendly-
Test-friendly - ignores most formatting, focuses on content
:content_only-
Maximum tolerance - only semantic content matters
# Use spec_friendly profile (common for tests)
Canon::Comparison.equivalent?(xml1, xml2,
match_profile: :spec_friendly
)
# Use rendered profile (for HTML)
Canon::Comparison.equivalent?(html1, html2,
match_profile: :rendered
)
# Override profile with specific dimension
Canon::Comparison.equivalent?(xml1, xml2,
match_profile: :spec_friendly,
match: {
comments: :strict # Override profile setting
}
)
6.5. Verbose mode
When verbose: true is specified, the method returns detailed comparison
results instead of a boolean.
Syntax:
result = Canon::Comparison.equivalent?(obj1, obj2, verbose: true)
Returns:
A Hash with two keys:
:differences-
Array of difference objects (empty if equivalent)
:preprocessed-
Two-element array of preprocessed documents
# Get detailed diff information
result = Canon::Comparison.equivalent?(xml1, xml2, verbose: true)
if result[:differences].empty?
puts "Documents are equivalent"
else
puts "Found #{result[:differences].size} differences"
result[:differences].each do |diff|
puts "Difference: #{diff}"
end
end
# Access preprocessed content
preprocessed1, preprocessed2 = result[:preprocessed]
# Verbose with custom options
result = Canon::Comparison.equivalent?(xml1, xml2,
verbose: true,
match: {
text_content: :normalize,
comments: :ignore
}
)
6.6. Display filtering
Use the show_diffs parameter to control which differences appear in formatted
output.
Syntax:
Canon::Comparison.equivalent?(obj1, obj2,
verbose: true,
show_diffs: :mode
)
Where mode is one of:
:all-
Show all differences (default)
:normative-
Show only differences that affect equivalence
:informative-
Show only differences that don’t affect equivalence
# Show all differences (default)
result = Canon::Comparison.equivalent?(xml1, xml2,
verbose: true,
show_diffs: :all
)
# Show only normative differences
result = Canon::Comparison.equivalent?(xml1, xml2,
verbose: true,
match: { comments: :ignore },
show_diffs: :normative
)
# Show only informative differences
result = Canon::Comparison.equivalent?(xml1, xml2,
verbose: true,
match: { comments: :ignore },
show_diffs: :informative
)
# Use with DiffFormatter directly
formatter = Canon::DiffFormatter.new(
use_color: true,
mode: :by_object,
show_diffs: :normative
)
output = formatter.format(result, :xml)
Display filtering does NOT affect equivalence determination. It
only controls which differences appear in formatted output. Equivalence is
always based on normative differences only, regardless of the show_diffs
setting.
|
xml1 = <<~XML
<root>
<!-- Old comment -->
<element>value1</element>
</root>
XML
xml2 = <<~XML
<root>
<!-- New comment -->
<element>value2</element>
</root>
XML
# With comments: :ignore and show_diffs: :normative
result = Canon::Comparison.equivalent?(xml1, xml2,
verbose: true,
match: { comments: :ignore },
show_diffs: :normative
)
# Result:
# - equivalent? => false (element value differs)
# - Diff output shows: only the element value change
# - Diff output hides: comment differences (informative)
See Display filtering for complete details.
6.7. Format-specific comparators
You can use format-specific comparator classes directly.
Syntax:
Canon::Comparison::XmlComparator.equivalent?(obj1, obj2, options)
Canon::Comparison::HtmlComparator.equivalent?(obj1, obj2, options)
Canon::Comparison::JsonComparator.equivalent?(obj1, obj2, options)
Canon::Comparison::YamlComparator.equivalent?(obj1, obj2, options)
# XML comparison with strict attribute order
Canon::Comparison::XmlComparator.equivalent?(xml1, xml2,
match: {
attribute_order: :strict
}
)
# HTML comparison with rendered profile
Canon::Comparison::HtmlComparator.equivalent?(html1, html2,
match_profile: :rendered
)
# JSON comparison ignoring key order
Canon::Comparison::JsonComparator.equivalent?(json1, json2,
match: {
key_order: :ignore
}
)
== Validation
Canon validates input before processing and raises Canon::ValidationError
for malformed input.
require 'canon'
malformed_xml = '<root><unclosed>'
begin
Canon.format(malformed_xml, :xml)
rescue Canon::ValidationError => e
puts e.message
# => XML Validation Error: Premature end of data in tag unclosed line 1
# Line: 1
# Column: 18
puts "Format: #{e.format}" # => :xml
puts "Line: #{e.line}" # => 1
puts "Column: #{e.column}" # => 18
end
See Input validation for details.
== See also