1. Purpose

Configuration profiles bundle many settings into a single named preset, eliminating repetitive configuration blocks across multiple gems. Instead of 60+ lines of per-format settings, use one line:

Canon::Config.instance.profile = :metanorma

Profiles are defined in YAML files and support inheritance, so a debug variant can extend a base profile with only the differences.

2. Built-in profiles

Profile Description

:metanorma

Standard Metanorma spec configuration. Sets preprocessing to :format, match profile to :spec_friendly, whitespace_type to :normalize (so that Unicode whitespace variants like space vs NBSP are treated as equivalent for backward compatibility), diff algorithm to :dom, canonical display format, normalized pretty-print display preprocessing, and XML-specific whitespace element lists.

:metanorma_debug

Extends :metanorma with debug output enabled (show_prettyprint_received: true).

List all available profiles programmatically:

Canon::Config::ProfileLoader.available_profiles
# => [:metanorma, :metanorma_debug]

3. Element-level whitespace classification

The metanorma profile’s key feature is its element-level whitespace classification. This controls how whitespace differences within specific elements are treated:

Three-way classification:

  • Preserve (:preserve) — Every whitespace character is significant. Use for elements where exact whitespace matters (like <pre>, <code>).

  • Collapse (:collapse) — Presence matters but whitespace form doesn’t. " hello " equals "hello". Differences are formatting-only (informative). Use for elements like <p>, <li>, <td> in prose documents.

  • Strip (:strip) — Whitespace is structural noise, dropped entirely. The default for XML elements not in any list.

Metanorma profile element lists:

# In metanorma profile
Canon::Config.instance.profile = :metanorma
Canon::Config.instance.xml.match.collapse_whitespace_elements
# => ["p", "title", "name", "td", "th", "dt", "dd", "li", ...]

Canon::Config.instance.xml.match.preserve_whitespace_elements
# => ["body", "passthrough"]

How it works with text_content: :normalize:

# With metanorma profile: <p> is in collapse_whitespace_elements
Canon::Config.instance.profile = :metanorma

# These are EQUIVALENT (whitespace in <p> is formatting-only)
Canon::Comparison.equivalent?('<p>  hello  </p>', '<p>hello</p>')
# => true

# But <body> is in preserve_whitespace_elements — every character matters
# These are NOT EQUIVALENT (whitespace in <body> is normative)
Canon::Comparison.equivalent?('<body>  hello  </body>', '<body>hello</body>')
# => false

Why this matters:

In Metanorma/DocBook documents, elements like <p>, <li>, <td> contain prose where whitespace formatting (extra spaces, line breaks) is irrelevant. But <body> or <passthrough> contain code or exact whitespace that matters.

Without element-level classification, you’d have to choose: - text_content: :normalize — ignores ALL whitespace, too permissive - text_content: :strict — requires exact match everywhere, too strict

Element-level classification gives you fine-grained control.

4. Usage

4.1. Programmatic (Ruby API)

Use a Symbol for built-in profiles and a String for file paths:

# Built-in profile (Symbol)
Canon::Config.instance.profile = :metanorma

# Local YAML file (String)
Canon::Config.instance.profile = "/path/to/my_profile.yml"
Canon::Config.instance.profile = "~/my_canon_profile.yml"
Canon::Config.instance.profile = "config/canon_profile.yml"

# Or in a configure block
Canon::Config.configure do |cfg|
  cfg.profile = :metanorma
  # Override individual settings after profile if needed
  cfg.xml.diff.verbose_diff = true
end

# Clear the profile (revert to defaults + programmatic values)
Canon::Config.instance.profile = nil
The type of the value determines how it is resolved: Symbols are looked up as built-in profile names; Strings are treated as file paths (with ~ expansion and relative path resolution against the working directory).

Local YAML files can inherit from built-in profiles (see Profile inheritance).

4.2. Environment variable

Set CANON_CONFIG_PROFILE to apply a profile automatically on initialization:

# Built-in profile
CANON_CONFIG_PROFILE=metanorma bundle exec rspec

# File path
CANON_CONFIG_PROFILE=~/my_profile.yml bundle exec rspec
CANON_CONFIG_PROFILE is distinct from CANON_PROFILE, which controls the match profile (comparison behavior). The config profile controls all settings at once.

5. Priority chain

With profiles, the resolution chain becomes four layers:

+------------------------------------+
| 1. Environment Variables           | <- Highest Priority
|    (CANON_XML_DIFF_ALGORITHM)      |
+------------------------------------+
               | overrides
+------------------------------------+
| 2. Programmatic Configuration      |
|    (config.xml.diff.algorithm=)    |
+------------------------------------+
               | overrides
+------------------------------------+
| 3. Profile Values                  |
|    (from YAML profile file)        |
+------------------------------------+
               | overrides
+------------------------------------+
| 4. Default Values                  | <- Lowest Priority
|    (defined in Canon::Config)      |
+------------------------------------+

This means:

  • ENV variables always win (useful for CI overrides)

  • Programmatic setter calls override profile values

  • Profile values override built-in defaults

  • Clearing the profile (cfg.profile = nil) removes only layer 3

6. Profile inheritance

A profile can inherit from another using the inherits key:

name: my_debug
inherits: metanorma

shared:
  diff:
    verbose_diff: true
    show_prettyprint_received: true

Inheritance rules:

  • Parent values are loaded first, then child values are deep-merged on top

  • Hashes are merged recursively (child keys override parent keys)

  • Arrays are replaced entirely (not concatenated)

  • Single-parent inheritance only

  • Cycle detection prevents infinite loops

  • Local files can inherit from built-in profiles by name

7. Creating custom profiles

7.1. YAML file format

---
name: my_profile              (1)
description: My custom config (2)
inherits: metanorma           (3)

shared:                        (4)
  preprocessing: format
  match:
    profile: spec_friendly
  diff:
    algorithm: dom
    context_lines: 5
    verbose_diff: false

formats:                       (5)
  xml:
    match:
      collapse_whitespace_elements:
        - p
        - title
        - td
      preserve_whitespace_elements:
        - body
        - passthrough
  html:
    diff:
      show_raw_inputs: true
1 Profile name (metadata)
2 Description (metadata)
3 Optional: inherit from another profile (name or path)
4 shared settings apply to all formats (xml, html, json, yaml, string)
5 formats.<name> settings override shared for that specific format

7.2. Attribute mapping

Profile YAML keys map directly to Canon configuration accessors:

YAML path Ruby equivalent

shared.preprocessing

cfg.xml.preprocessing = :format

shared.match.profile

cfg.xml.match.profile = :spec_friendly

shared.diff.algorithm

cfg.xml.diff.algorithm = :dom

formats.xml.diff.context_lines

cfg.xml.diff.context_lines = 5

All DiffConfig and MatchConfig attributes documented in Options Across Interfaces are supported.

8. See also