1. Overview

Canon replaces invisible characters (spaces, tabs, non-breaking spaces, etc.) with visible Unicode symbols before rendering diff output. This makes it easy to spot whitespace differences that would otherwise be indistinguishable from surrounding content.

Example 1. Example output with character visualization enabled (default)
    |   3- | ░░<item>Alpha</item>
    |   3+ | ░░<item>Beta</item>

The characters represent U+0020 spaces used for indentation.

2. Configuration

character_visualization is a DiffConfig option accessible via Canon::Config, environment variables, or the DiffFormatter constructor.

2.1. Values

Value Behaviour

true

(default) The full default visualization map is applied. Spaces appear as , tabs as , non-breaking spaces as , and so on.

false

All visualization is disabled. Characters appear exactly as they are stored in the document. Useful when copying failure-message output into a text editor, or when downstream tooling cannot handle Unicode symbols.

:content_only

Reserved for future use — currently behaves identically to true. Future intent: apply visualization only to text-node content, leaving structural indentation whitespace plain so that pretty-printed diffs remain visually uncluttered. See the [future-work] section below.

2.2. Via Canon::Config (RSpec spec_helper.rb)

Canon::Config.configure do |cfg|
  # Disable for all XML diffs in this test suite
  cfg.xml.diff.character_visualization = false

  # Or disable for HTML as well
  cfg.html.diff.character_visualization = false
end

The setting is format-specific: cfg.xml, cfg.html, cfg.json, cfg.yaml, and cfg.string each have their own diff.character_visualization.

2.3. Via Environment Variable

# Disable globally
export CANON_CHARACTER_VISUALIZATION=false

# Disable for XML only
export CANON_XML_DIFF_CHARACTER_VISUALIZATION=false

# Set to :content_only for XML only
export CANON_XML_DIFF_CHARACTER_VISUALIZATION=content_only

2.4. Via DiffFormatter directly

formatter = Canon::DiffFormatter.new(
  use_color: false,
  mode: :by_line,
  character_visualization: false,
)

3. Default Visualization Map

The following characters are visualized by default:

Name Unicode Visualization Notes

Space

U+0020

Most common; appears as indentation and between tokens

Tab

U+0009

Structural indentation in tab-indented files

No-Break Space

U+00A0

Common in HTML ( ) and word-processor output

Thin Space

U+2009

·

Typographic use

En Space

U+2002

Em Space

U+2003

Line Feed

U+000A

End-of-line (shown in certain modes)

Carriage Return

U+000D

Windows line endings

Zero-Width Space

U+200B

Invisible in output; often a bug

The complete map is loaded from lib/canon/diff_formatter/character_map.yml at startup.

4. Interaction with display_preprocessing

Character visualization is applied after display preprocessing.

When display_preprocessing: :pretty_print is active, Canon runs both documents through Canon::PrettyPrinter::Xml before the line diff. The pretty-printer produces only:

  • ASCII U+0020 spaces for indentation

  • ASCII U+000A newlines between elements

Because the default visualization map visualizes U+0020 spaces as , structural indentation introduced by the pretty-printer will appear as characters in context lines. This is intentional: the diff output stays consistent regardless of how indentation was introduced.

Example 2. Example (display_preprocessing: :pretty_print, character_visualization: true)
    |   1  | <root>
    |   2- | ░░<item>Alpha</item>
    |   2+ | ░░<item>Beta</item>
    |   3  | </root>

The ░░ represents the 2-space indentation added by the pretty-printer.

If you want structure-free visualization (e.g. indentation stays as plain spaces but content whitespace is still visualized), set character_visualization: :content_only once that feature is implemented. See [future-work].

5. Combining with context_lines

Context lines (unchanged lines shown around a diff hunk) are also subject to character visualization. Reducing context_lines limits the number of visualized lines displayed:

Canon::Config.configure do |cfg|
  cfg.xml.diff.context_lines = 1       # show only 1 context line
  cfg.xml.diff.character_visualization = true
end

6. Future Work: :content_only

The :content_only value is reserved for a planned DOM-level pre-serialization pass that would:

  1. Walk the parsed DOM tree before serialization.

  2. Apply character visualization only to text node content.

  3. Leave structural whitespace (indentation, element-separator newlines) plain.

This would allow pretty-printed output to have clean indentation while still making content whitespace visible.

The constraint that blocks this today: visualization is currently applied as a post-serialization string substitution by the by-line formatters. Implementing :content_only requires moving the substitution step into a DOM walk before Nokogiri::XML::Node#to_xml is called.

When implemented, :content_only will become the recommended value for suites that use display_preprocessing: :pretty_print.