Class: Canon::Comparison::FormatDetector

Inherits:
Object
  • Object
show all
Defined in:
lib/canon/comparison/format_detector.rb

Overview

Format detection service for auto-detecting document formats

Provides format detection for various document types including XML, HTML, JSON, YAML, and plain text. Uses caching for performance optimization.

Examples:

Detect format from a string

FormatDetector.detect("<root>content</root>") # => :xml

Detect format from an object

FormatDetector.detect(Moxml::Document.new) # => :xml

Constant Summary collapse

FORMATS =

Supported format types

%i[xml html json yaml ruby_object string].freeze

Class Method Summary collapse

Class Method Details

.detect(obj) ⇒ Symbol

Detect the format of an object

Parameters:

  • obj (Object)

    Object to detect format of

Returns:

  • (Symbol)

    Format type (:xml, :html, :json, :yaml, :ruby_object, :string)



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/canon/comparison/format_detector.rb', line 24

def detect(obj)
  case obj
  when Moxml::Node, Moxml::Document
    :xml
  when String
    detect_string(obj)
  when Hash, Array
    :ruby_object
  else
    detect_nokogiri(obj)
  end
end

.detect_nokogiri(obj) ⇒ Object

Nokogiri nodes arrive from user input regardless of the active XML engine, so detection is by node type (Opal never sees them — Nokogiri is not loaded there).

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/canon/comparison/format_detector.rb', line 40

def detect_nokogiri(obj)
  return :html if defined?(Nokogiri) && obj.is_a?(Nokogiri::HTML::DocumentFragment)
  return :html if defined?(Nokogiri) && obj.is_a?(Nokogiri::HTML5::DocumentFragment)

  if defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::DocumentFragment)
    return obj.document&.html? ? :html : :xml
  end
  if defined?(Nokogiri) && (obj.is_a?(Nokogiri::XML::Document) || obj.is_a?(Nokogiri::XML::Node))
    return obj.html? ? :html : :xml
  end
  return :html if defined?(Nokogiri) && obj.is_a?(Nokogiri::HTML::Document)
  return :html if defined?(Nokogiri) && obj.is_a?(Nokogiri::HTML5::Document)

  raise Canon::Error, "Unknown format for object: #{obj.class}"
end

.detect_string(str) ⇒ Symbol

Detect the format of a string

Parameters:

  • str (String)

    String to detect format of

Returns:

  • (Symbol)

    Format type



60
61
62
63
64
65
66
# File 'lib/canon/comparison/format_detector.rb', line 60

def detect_string(str)
  # Detection is a handful of prefix checks and one anchored
  # regex — building the SHA256 cache key (plus LRU clock
  # bookkeeping) cost several times the detection itself, so
  # the answer is computed directly.
  detect_string_uncached(str)
end

.detect_string_uncached(str) ⇒ Symbol

Detect the format of a string without caching

Parameters:

  • str (String)

    String to detect format of

Returns:

  • (Symbol)

    Format type



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/canon/comparison/format_detector.rb', line 72

def detect_string_uncached(str)
  # Convert to UTF-8 for consistent handling if possible
  # This handles cases like UTF-16 encoded XML that would otherwise fail string operations
  str_utf8 = if ["UTF-16", "UTF-16BE",
                 "UTF-16LE"].include?(str.encoding.name)
               begin
                 str.encode("UTF-8", str.encoding, invalid: :replace,
                                                   undef: :replace, replace: "?")
               rescue EncodingError
                 str.dup.force_encoding("BINARY").encode("UTF-8")
               end
             else
               str
             end

  trimmed = str_utf8.strip

  # YAML indicators
  return :yaml if trimmed.start_with?("---")
  return :yaml if trimmed.match?(/^[a-zA-Z_]\w*:\s/)

  # JSON indicators
  return :json if trimmed.start_with?("{", "[")

  # HTML indicators
  return :html if trimmed.start_with?("<!DOCTYPE html", "<html",
                                      "<HTML")

  # XML indicators - must start with < and end with >
  return :xml if trimmed.start_with?("<") && trimmed.end_with?(">")

  # Default to plain string for everything else
  :string
end