1. Purpose

This document explains how to extend Canon with custom functionality, including creating custom comparators, formatters, and adapters for different document formats.

2. Overview

Canon is designed to be extensible at multiple layers:

  • Layer 1: Custom preprocessing/normalization

  • Layer 2: Custom comparison algorithms

  • Layer 3: Custom match options and dimensions

  • Layer 4: Custom diff formatters and renderers

3. Adapter Pattern

Canon uses an adapter pattern to work with different parsing libraries (Nokogiri, Moxml, etc.).

3.1. Adapter Structure

module Canon
  module Adapters
    class NokogiriAdapter
      def parse(input)
        # Parse with Nokogiri
        Nokogiri::XML(input)
      end

      def serialize(node)
        # Serialize with Nokogiri
        node.to_xml
      end
    end
  end
end

3.2. Creating a Custom Adapter

To add support for a new parsing library:

  1. Create an adapter class that implements parse and serialize methods

  2. Register the adapter with the format system

  3. Add tests for the new adapter

4. Custom Comparators

4.1. Creating a Custom Comparison Algorithm

module Canon
  module Comparison
    class CustomComparator < BaseComparator
      def compare(node1, node2, opts)
        # Your comparison logic here
        differences = []

        # Your algorithm implementation

        differences
      end
    end
  end
end

4.2. Registering Your Algorithm

Canon::Comparison.register_algorithm(:custom, CustomComparator)

Then use it:

Canon::Comparison.equivalent?(doc1, doc2, diff_algorithm: :custom)

5. Custom Formatters

5.1. Creating a Custom Diff Formatter

module Canon
  class DiffFormatter
    class CustomFormatter
      def format(differences, opts)
        # Your formatting logic here
        formatted_output = ""

        differences.each do |diff|
          formatted_output += format_difference(diff, opts)
        end

        formatted_output
      end
    end
  end
end

5.2. Using Your Formatter

result = Canon::Comparison.equivalent?(doc1, doc2, verbose: true)

formatter = Canon::DiffFormatter::CustomFormatter.new
output = formatter.format(result.differences, use_color: true)
puts output

6. Custom Match Options

6.1. Defining Custom Dimensions

module Canon
  module Comparison
    class CustomDimension
      def self.key
        :custom_dimension
      end

      def self.compare(node1, node2, behavior, opts)
        # Your comparison logic for this dimension
        case behavior
        when :strict
          node1 == node2
        when :normalize
          normalize(node1) == normalize(node2)
        when :ignore
          true
        end
      end
    end
  end
end

Register your dimension:

Canon::Comparison.register_dimension(CustomDimension)

7. Best Practices

7.1. Testing Your Extensions

  1. Write comprehensive tests for your extensions

  2. Use the existing test helpers and fixtures

  3. Test edge cases and error conditions

7.2. Performance Considerations

  1. Cache expensive computations

  2. Use lazy evaluation where appropriate

  3. Avoid unnecessary node cloning

7.3. Error Handling

  1. Provide clear error messages

  2. Use Canon’s error classes consistently

  3. Document error conditions

9. See Also