canon 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/.rubocop.yml +9 -1
 - data/.rubocop_todo.yml +280 -5
 - data/README.adoc +203 -138
 - data/_config.yml +116 -0
 - data/docs/ADVANCED_TOPICS.adoc +20 -0
 - data/docs/BASIC_USAGE.adoc +16 -0
 - data/docs/CHARACTER_VISUALIZATION.adoc +567 -0
 - data/docs/CLI.adoc +493 -0
 - data/docs/CUSTOMIZING_BEHAVIOR.adoc +19 -0
 - data/docs/DIFF_ARCHITECTURE.adoc +435 -0
 - data/docs/DIFF_FORMATTING.adoc +540 -0
 - data/docs/FORMATS.adoc +447 -0
 - data/docs/INDEX.adoc +222 -0
 - data/docs/INPUT_VALIDATION.adoc +477 -0
 - data/docs/MATCH_ARCHITECTURE.adoc +463 -0
 - data/docs/MATCH_OPTIONS.adoc +719 -0
 - data/docs/MODES.adoc +432 -0
 - data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +219 -0
 - data/docs/OPTIONS.adoc +1387 -0
 - data/docs/PREPROCESSING.adoc +491 -0
 - data/docs/RSPEC.adoc +605 -0
 - data/docs/RUBY_API.adoc +478 -0
 - data/docs/SEMANTIC_DIFF_REPORT.adoc +528 -0
 - data/docs/UNDERSTANDING_CANON.adoc +17 -0
 - data/docs/VERBOSE.adoc +482 -0
 - data/exe/canon +7 -0
 - data/lib/canon/cli.rb +179 -0
 - data/lib/canon/commands/diff_command.rb +195 -0
 - data/lib/canon/commands/format_command.rb +113 -0
 - data/lib/canon/comparison/base_comparator.rb +39 -0
 - data/lib/canon/comparison/comparison_result.rb +79 -0
 - data/lib/canon/comparison/html_comparator.rb +410 -0
 - data/lib/canon/comparison/json_comparator.rb +212 -0
 - data/lib/canon/comparison/match_options.rb +616 -0
 - data/lib/canon/comparison/xml_comparator.rb +566 -0
 - data/lib/canon/comparison/yaml_comparator.rb +93 -0
 - data/lib/canon/comparison.rb +239 -0
 - data/lib/canon/config.rb +172 -0
 - data/lib/canon/diff/diff_block.rb +71 -0
 - data/lib/canon/diff/diff_block_builder.rb +105 -0
 - data/lib/canon/diff/diff_classifier.rb +46 -0
 - data/lib/canon/diff/diff_context.rb +85 -0
 - data/lib/canon/diff/diff_context_builder.rb +107 -0
 - data/lib/canon/diff/diff_line.rb +77 -0
 - data/lib/canon/diff/diff_node.rb +56 -0
 - data/lib/canon/diff/diff_node_mapper.rb +148 -0
 - data/lib/canon/diff/diff_report.rb +133 -0
 - data/lib/canon/diff/diff_report_builder.rb +62 -0
 - data/lib/canon/diff_formatter/by_line/base_formatter.rb +407 -0
 - data/lib/canon/diff_formatter/by_line/html_formatter.rb +672 -0
 - data/lib/canon/diff_formatter/by_line/json_formatter.rb +284 -0
 - data/lib/canon/diff_formatter/by_line/simple_formatter.rb +190 -0
 - data/lib/canon/diff_formatter/by_line/xml_formatter.rb +860 -0
 - data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +292 -0
 - data/lib/canon/diff_formatter/by_object/base_formatter.rb +199 -0
 - data/lib/canon/diff_formatter/by_object/json_formatter.rb +305 -0
 - data/lib/canon/diff_formatter/by_object/xml_formatter.rb +248 -0
 - data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +17 -0
 - data/lib/canon/diff_formatter/character_map.yml +197 -0
 - data/lib/canon/diff_formatter/debug_output.rb +431 -0
 - data/lib/canon/diff_formatter/diff_detail_formatter.rb +551 -0
 - data/lib/canon/diff_formatter/legend.rb +141 -0
 - data/lib/canon/diff_formatter.rb +520 -0
 - data/lib/canon/errors.rb +56 -0
 - data/lib/canon/formatters/html4_formatter.rb +17 -0
 - data/lib/canon/formatters/html5_formatter.rb +17 -0
 - data/lib/canon/formatters/html_formatter.rb +37 -0
 - data/lib/canon/formatters/html_formatter_base.rb +163 -0
 - data/lib/canon/formatters/json_formatter.rb +3 -0
 - data/lib/canon/formatters/xml_formatter.rb +20 -55
 - data/lib/canon/formatters/yaml_formatter.rb +4 -1
 - data/lib/canon/pretty_printer/html.rb +57 -0
 - data/lib/canon/pretty_printer/json.rb +25 -0
 - data/lib/canon/pretty_printer/xml.rb +29 -0
 - data/lib/canon/rspec_matchers.rb +222 -77
 - data/lib/canon/validators/base_validator.rb +49 -0
 - data/lib/canon/validators/html_validator.rb +138 -0
 - data/lib/canon/validators/json_validator.rb +89 -0
 - data/lib/canon/validators/xml_validator.rb +53 -0
 - data/lib/canon/validators/yaml_validator.rb +73 -0
 - data/lib/canon/version.rb +1 -1
 - data/lib/canon/xml/attribute_handler.rb +80 -0
 - data/lib/canon/xml/c14n.rb +36 -0
 - data/lib/canon/xml/character_encoder.rb +38 -0
 - data/lib/canon/xml/data_model.rb +225 -0
 - data/lib/canon/xml/element_matcher.rb +196 -0
 - data/lib/canon/xml/line_range_mapper.rb +158 -0
 - data/lib/canon/xml/namespace_handler.rb +86 -0
 - data/lib/canon/xml/node.rb +32 -0
 - data/lib/canon/xml/nodes/attribute_node.rb +54 -0
 - data/lib/canon/xml/nodes/comment_node.rb +23 -0
 - data/lib/canon/xml/nodes/element_node.rb +56 -0
 - data/lib/canon/xml/nodes/namespace_node.rb +38 -0
 - data/lib/canon/xml/nodes/processing_instruction_node.rb +24 -0
 - data/lib/canon/xml/nodes/root_node.rb +16 -0
 - data/lib/canon/xml/nodes/text_node.rb +23 -0
 - data/lib/canon/xml/processor.rb +151 -0
 - data/lib/canon/xml/whitespace_normalizer.rb +72 -0
 - data/lib/canon/xml/xml_base_handler.rb +188 -0
 - data/lib/canon.rb +14 -3
 - metadata +116 -21
 
| 
         @@ -0,0 +1,284 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "base_formatter"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "../legend"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "strscan"
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Canon
         
     | 
| 
      
 8 
     | 
    
         
            +
              class DiffFormatter
         
     | 
| 
      
 9 
     | 
    
         
            +
                module ByLine
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # JSON formatter with semantic token-level highlighting
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # Pretty-prints JSON before diffing for better structure awareness
         
     | 
| 
      
 12 
     | 
    
         
            +
                  class JsonFormatter < BaseFormatter
         
     | 
| 
      
 13 
     | 
    
         
            +
                    # Format semantic JSON diff with token-level highlighting
         
     | 
| 
      
 14 
     | 
    
         
            +
                    #
         
     | 
| 
      
 15 
     | 
    
         
            +
                    # @param doc1 [String] First JSON document
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # @param doc2 [String] Second JSON document
         
     | 
| 
      
 17 
     | 
    
         
            +
                    # @return [String] Formatted diff
         
     | 
| 
      
 18 
     | 
    
         
            +
                    def format(doc1, doc2)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      output = []
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 22 
     | 
    
         
            +
                        # Pretty print both JSON files
         
     | 
| 
      
 23 
     | 
    
         
            +
                        require "canon/pretty_printer/json"
         
     | 
| 
      
 24 
     | 
    
         
            +
                        formatter = Canon::PrettyPrinter::Json.new(indent: 2)
         
     | 
| 
      
 25 
     | 
    
         
            +
                        pretty1 = formatter.format(doc1)
         
     | 
| 
      
 26 
     | 
    
         
            +
                        pretty2 = formatter.format(doc2)
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                        lines1 = pretty1.split("\n")
         
     | 
| 
      
 29 
     | 
    
         
            +
                        lines2 = pretty2.split("\n")
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                        # Get LCS diff
         
     | 
| 
      
 32 
     | 
    
         
            +
                        diffs = ::Diff::LCS.sdiff(lines1, lines2)
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                        # Format with semantic token highlighting
         
     | 
| 
      
 35 
     | 
    
         
            +
                        output << format_semantic_diff(diffs, lines1, lines2)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      rescue StandardError => e
         
     | 
| 
      
 37 
     | 
    
         
            +
                        output << colorize(
         
     | 
| 
      
 38 
     | 
    
         
            +
                          "Warning: JSON parsing failed (#{e.message}), using simple diff", :yellow
         
     | 
| 
      
 39 
     | 
    
         
            +
                        )
         
     | 
| 
      
 40 
     | 
    
         
            +
                        require_relative "simple_formatter"
         
     | 
| 
      
 41 
     | 
    
         
            +
                        simple = SimpleFormatter.new(
         
     | 
| 
      
 42 
     | 
    
         
            +
                          use_color: @use_color,
         
     | 
| 
      
 43 
     | 
    
         
            +
                          context_lines: @context_lines,
         
     | 
| 
      
 44 
     | 
    
         
            +
                          diff_grouping_lines: @diff_grouping_lines,
         
     | 
| 
      
 45 
     | 
    
         
            +
                          visualization_map: @visualization_map,
         
     | 
| 
      
 46 
     | 
    
         
            +
                        )
         
     | 
| 
      
 47 
     | 
    
         
            +
                        output << simple.format(doc1, doc2)
         
     | 
| 
      
 48 
     | 
    
         
            +
                      end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                      output.join("\n")
         
     | 
| 
      
 51 
     | 
    
         
            +
                    end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                    private
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    # Format semantic diff with token-level highlighting
         
     | 
| 
      
 56 
     | 
    
         
            +
                    #
         
     | 
| 
      
 57 
     | 
    
         
            +
                    # @param diffs [Array] LCS diff array
         
     | 
| 
      
 58 
     | 
    
         
            +
                    # @param lines1 [Array<String>] Lines from first document
         
     | 
| 
      
 59 
     | 
    
         
            +
                    # @param lines2 [Array<String>] Lines from second document
         
     | 
| 
      
 60 
     | 
    
         
            +
                    # @return [String] Formatted diff
         
     | 
| 
      
 61 
     | 
    
         
            +
                    def format_semantic_diff(diffs, lines1, lines2)
         
     | 
| 
      
 62 
     | 
    
         
            +
                      output = []
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                      # Detect non-ASCII characters in the diff
         
     | 
| 
      
 65 
     | 
    
         
            +
                      all_text = (lines1 + lines2).join
         
     | 
| 
      
 66 
     | 
    
         
            +
                      non_ascii = Legend.detect_non_ascii(all_text, @visualization_map)
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                      # Add Unicode legend if any non-ASCII characters detected
         
     | 
| 
      
 69 
     | 
    
         
            +
                      unless non_ascii.empty?
         
     | 
| 
      
 70 
     | 
    
         
            +
                        output << Legend.build_legend(non_ascii, use_color: @use_color)
         
     | 
| 
      
 71 
     | 
    
         
            +
                        output << ""
         
     | 
| 
      
 72 
     | 
    
         
            +
                      end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                      diffs.each do |change|
         
     | 
| 
      
 75 
     | 
    
         
            +
                        old_line = change.old_position ? change.old_position + 1 : nil
         
     | 
| 
      
 76 
     | 
    
         
            +
                        new_line = change.new_position ? change.new_position + 1 : nil
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                        case change.action
         
     | 
| 
      
 79 
     | 
    
         
            +
                        when "="
         
     | 
| 
      
 80 
     | 
    
         
            +
                          # Unchanged line
         
     | 
| 
      
 81 
     | 
    
         
            +
                          output << format_unified_line(old_line, new_line, " ",
         
     | 
| 
      
 82 
     | 
    
         
            +
                                                        change.old_element)
         
     | 
| 
      
 83 
     | 
    
         
            +
                        when "-"
         
     | 
| 
      
 84 
     | 
    
         
            +
                          # Deletion
         
     | 
| 
      
 85 
     | 
    
         
            +
                          output << format_unified_line(old_line, nil, "-",
         
     | 
| 
      
 86 
     | 
    
         
            +
                                                        change.old_element, :red)
         
     | 
| 
      
 87 
     | 
    
         
            +
                        when "+"
         
     | 
| 
      
 88 
     | 
    
         
            +
                          # Addition
         
     | 
| 
      
 89 
     | 
    
         
            +
                          output << format_unified_line(nil, new_line, "+",
         
     | 
| 
      
 90 
     | 
    
         
            +
                                                        change.new_element, :green)
         
     | 
| 
      
 91 
     | 
    
         
            +
                        when "!"
         
     | 
| 
      
 92 
     | 
    
         
            +
                          # Change - show with semantic token highlighting
         
     | 
| 
      
 93 
     | 
    
         
            +
                          old_text = change.old_element
         
     | 
| 
      
 94 
     | 
    
         
            +
                          new_text = change.new_element
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                          # Tokenize JSON
         
     | 
| 
      
 97 
     | 
    
         
            +
                          old_tokens = tokenize_json(old_text)
         
     | 
| 
      
 98 
     | 
    
         
            +
                          new_tokens = tokenize_json(new_text)
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                          # Get token-level diff
         
     | 
| 
      
 101 
     | 
    
         
            +
                          token_diffs = ::Diff::LCS.sdiff(old_tokens, new_tokens)
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                          # Build highlighted versions
         
     | 
| 
      
 104 
     | 
    
         
            +
                          old_highlighted = build_token_highlighted_text(token_diffs, :old)
         
     | 
| 
      
 105 
     | 
    
         
            +
                          new_highlighted = build_token_highlighted_text(token_diffs, :new)
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                          # Format both lines
         
     | 
| 
      
 108 
     | 
    
         
            +
                          output << format_token_diff_line(old_line, new_line,
         
     | 
| 
      
 109 
     | 
    
         
            +
                                                           old_highlighted,
         
     | 
| 
      
 110 
     | 
    
         
            +
                                                           new_highlighted)
         
     | 
| 
      
 111 
     | 
    
         
            +
                        end
         
     | 
| 
      
 112 
     | 
    
         
            +
                      end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                      output.join("\n")
         
     | 
| 
      
 115 
     | 
    
         
            +
                    end
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                    # Format a unified diff line
         
     | 
| 
      
 118 
     | 
    
         
            +
                    #
         
     | 
| 
      
 119 
     | 
    
         
            +
                    # @param old_num [Integer, nil] Line number in old file
         
     | 
| 
      
 120 
     | 
    
         
            +
                    # @param new_num [Integer, nil] Line number in new file
         
     | 
| 
      
 121 
     | 
    
         
            +
                    # @param marker [String] Diff marker
         
     | 
| 
      
 122 
     | 
    
         
            +
                    # @param content [String] Line content
         
     | 
| 
      
 123 
     | 
    
         
            +
                    # @param color [Symbol, nil] Color for diff lines
         
     | 
| 
      
 124 
     | 
    
         
            +
                    # @return [String] Formatted line
         
     | 
| 
      
 125 
     | 
    
         
            +
                    def format_unified_line(old_num, new_num, marker, content, color = nil)
         
     | 
| 
      
 126 
     | 
    
         
            +
                      old_str = old_num ? "%4d" % old_num : "    "
         
     | 
| 
      
 127 
     | 
    
         
            +
                      new_str = new_num ? "%4d" % new_num : "    "
         
     | 
| 
      
 128 
     | 
    
         
            +
                      marker_part = "#{marker} "
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                      visualized_content = if color
         
     | 
| 
      
 131 
     | 
    
         
            +
                                             apply_visualization(content,
         
     | 
| 
      
 132 
     | 
    
         
            +
                                                                 color)
         
     | 
| 
      
 133 
     | 
    
         
            +
                                           else
         
     | 
| 
      
 134 
     | 
    
         
            +
                                             content
         
     | 
| 
      
 135 
     | 
    
         
            +
                                           end
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                      if @use_color
         
     | 
| 
      
 138 
     | 
    
         
            +
                        yellow_old = colorize(old_str, :yellow)
         
     | 
| 
      
 139 
     | 
    
         
            +
                        yellow_pipe1 = colorize("|", :yellow)
         
     | 
| 
      
 140 
     | 
    
         
            +
                        yellow_new = colorize(new_str, :yellow)
         
     | 
| 
      
 141 
     | 
    
         
            +
                        yellow_pipe2 = colorize("|", :yellow)
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                        if color
         
     | 
| 
      
 144 
     | 
    
         
            +
                          colored_marker = colorize(marker, color)
         
     | 
| 
      
 145 
     | 
    
         
            +
                          "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{colored_marker} #{yellow_pipe2} #{visualized_content}"
         
     | 
| 
      
 146 
     | 
    
         
            +
                        else
         
     | 
| 
      
 147 
     | 
    
         
            +
                          "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{marker} #{yellow_pipe2} #{visualized_content}"
         
     | 
| 
      
 148 
     | 
    
         
            +
                        end
         
     | 
| 
      
 149 
     | 
    
         
            +
                      else
         
     | 
| 
      
 150 
     | 
    
         
            +
                        "#{old_str}|#{new_str}#{marker_part}| #{visualized_content}"
         
     | 
| 
      
 151 
     | 
    
         
            +
                      end
         
     | 
| 
      
 152 
     | 
    
         
            +
                    end
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                    # Format token diff lines
         
     | 
| 
      
 155 
     | 
    
         
            +
                    #
         
     | 
| 
      
 156 
     | 
    
         
            +
                    # @param old_line [Integer] Old line number
         
     | 
| 
      
 157 
     | 
    
         
            +
                    # @param new_line [Integer] New line number
         
     | 
| 
      
 158 
     | 
    
         
            +
                    # @param old_highlighted [String] Highlighted old text
         
     | 
| 
      
 159 
     | 
    
         
            +
                    # @param new_highlighted [String] Highlighted new text
         
     | 
| 
      
 160 
     | 
    
         
            +
                    # @return [String] Formatted lines
         
     | 
| 
      
 161 
     | 
    
         
            +
                    def format_token_diff_line(old_line, new_line, old_highlighted,
         
     | 
| 
      
 162 
     | 
    
         
            +
                                                new_highlighted)
         
     | 
| 
      
 163 
     | 
    
         
            +
                      output = []
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
                      if @use_color
         
     | 
| 
      
 166 
     | 
    
         
            +
                        yellow_old = colorize("%4d" % old_line, :yellow)
         
     | 
| 
      
 167 
     | 
    
         
            +
                        yellow_pipe1 = colorize("|", :yellow)
         
     | 
| 
      
 168 
     | 
    
         
            +
                        yellow_new = colorize("%4d" % new_line, :yellow)
         
     | 
| 
      
 169 
     | 
    
         
            +
                        yellow_pipe2 = colorize("|", :yellow)
         
     | 
| 
      
 170 
     | 
    
         
            +
                        red_marker = colorize("-", :red)
         
     | 
| 
      
 171 
     | 
    
         
            +
                        green_marker = colorize("+", :green)
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                        output << "#{yellow_old}#{yellow_pipe1}    #{red_marker} #{yellow_pipe2} #{old_highlighted}"
         
     | 
| 
      
 174 
     | 
    
         
            +
                        output << "    #{yellow_pipe1}#{yellow_new}#{green_marker} #{yellow_pipe2} #{new_highlighted}"
         
     | 
| 
      
 175 
     | 
    
         
            +
                      else
         
     | 
| 
      
 176 
     | 
    
         
            +
                        output << "#{'%4d' % old_line}|    - | #{old_highlighted}"
         
     | 
| 
      
 177 
     | 
    
         
            +
                        output << "    |#{'%4d' % new_line}+ | #{new_highlighted}"
         
     | 
| 
      
 178 
     | 
    
         
            +
                      end
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                      output.join("\n")
         
     | 
| 
      
 181 
     | 
    
         
            +
                    end
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
                    # Tokenize JSON line into meaningful tokens
         
     | 
| 
      
 184 
     | 
    
         
            +
                    #
         
     | 
| 
      
 185 
     | 
    
         
            +
                    # @param line [String] JSON line to tokenize
         
     | 
| 
      
 186 
     | 
    
         
            +
                    # @return [Array<String>] Tokens
         
     | 
| 
      
 187 
     | 
    
         
            +
                    def tokenize_json(line)
         
     | 
| 
      
 188 
     | 
    
         
            +
                      tokens = []
         
     | 
| 
      
 189 
     | 
    
         
            +
                      scanner = StringScanner.new(line)
         
     | 
| 
      
 190 
     | 
    
         
            +
             
     | 
| 
      
 191 
     | 
    
         
            +
                      until scanner.eos?
         
     | 
| 
      
 192 
     | 
    
         
            +
                        tokens << if scanner.scan(/\s+/)
         
     | 
| 
      
 193 
     | 
    
         
            +
                                    # Whitespace
         
     | 
| 
      
 194 
     | 
    
         
            +
                                    scanner.matched
         
     | 
| 
      
 195 
     | 
    
         
            +
                                  elsif scanner.scan(/"(?:[^"\\]|\\.)*"/)
         
     | 
| 
      
 196 
     | 
    
         
            +
                                    # String values (with quotes)
         
     | 
| 
      
 197 
     | 
    
         
            +
                                    scanner.matched
         
     | 
| 
      
 198 
     | 
    
         
            +
                                  elsif scanner.scan(/-?\d+\.?\d*(?:[eE][+-]?\d+)?/)
         
     | 
| 
      
 199 
     | 
    
         
            +
                                    # Numbers
         
     | 
| 
      
 200 
     | 
    
         
            +
                                    scanner.matched
         
     | 
| 
      
 201 
     | 
    
         
            +
                                  elsif scanner.scan(/\b(?:true|false|null)\b/)
         
     | 
| 
      
 202 
     | 
    
         
            +
                                    # Booleans and null
         
     | 
| 
      
 203 
     | 
    
         
            +
                                    scanner.matched
         
     | 
| 
      
 204 
     | 
    
         
            +
                                  elsif scanner.scan(/[{}\[\]:,]/)
         
     | 
| 
      
 205 
     | 
    
         
            +
                                    # Structural characters
         
     | 
| 
      
 206 
     | 
    
         
            +
                                    scanner.matched
         
     | 
| 
      
 207 
     | 
    
         
            +
                                  else
         
     | 
| 
      
 208 
     | 
    
         
            +
                                    # Any other character
         
     | 
| 
      
 209 
     | 
    
         
            +
                                    scanner.getch
         
     | 
| 
      
 210 
     | 
    
         
            +
                                  end
         
     | 
| 
      
 211 
     | 
    
         
            +
                      end
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
      
 213 
     | 
    
         
            +
                      tokens
         
     | 
| 
      
 214 
     | 
    
         
            +
                    end
         
     | 
| 
      
 215 
     | 
    
         
            +
             
     | 
| 
      
 216 
     | 
    
         
            +
                    # Build highlighted text from token diff
         
     | 
| 
      
 217 
     | 
    
         
            +
                    #
         
     | 
| 
      
 218 
     | 
    
         
            +
                    # @param token_diffs [Array] Token-level diff
         
     | 
| 
      
 219 
     | 
    
         
            +
                    # @param side [Symbol] Which side (:old or :new)
         
     | 
| 
      
 220 
     | 
    
         
            +
                    # @return [String] Highlighted text
         
     | 
| 
      
 221 
     | 
    
         
            +
                    def build_token_highlighted_text(token_diffs, side)
         
     | 
| 
      
 222 
     | 
    
         
            +
                      parts = []
         
     | 
| 
      
 223 
     | 
    
         
            +
             
     | 
| 
      
 224 
     | 
    
         
            +
                      token_diffs.each do |change|
         
     | 
| 
      
 225 
     | 
    
         
            +
                        case change.action
         
     | 
| 
      
 226 
     | 
    
         
            +
                        when "="
         
     | 
| 
      
 227 
     | 
    
         
            +
                          # Unchanged token - apply visualization with explicit reset
         
     | 
| 
      
 228 
     | 
    
         
            +
                          visual = change.old_element.chars.map do |char|
         
     | 
| 
      
 229 
     | 
    
         
            +
                            @visualization_map.fetch(char, char)
         
     | 
| 
      
 230 
     | 
    
         
            +
                          end.join
         
     | 
| 
      
 231 
     | 
    
         
            +
             
     | 
| 
      
 232 
     | 
    
         
            +
                          parts << if @use_color
         
     | 
| 
      
 233 
     | 
    
         
            +
                                     colorize(visual, :default)
         
     | 
| 
      
 234 
     | 
    
         
            +
                                   else
         
     | 
| 
      
 235 
     | 
    
         
            +
                                     visual
         
     | 
| 
      
 236 
     | 
    
         
            +
                                   end
         
     | 
| 
      
 237 
     | 
    
         
            +
                        when "-"
         
     | 
| 
      
 238 
     | 
    
         
            +
                          # Deleted token (only show on old side)
         
     | 
| 
      
 239 
     | 
    
         
            +
                          if side == :old
         
     | 
| 
      
 240 
     | 
    
         
            +
                            token = change.old_element
         
     | 
| 
      
 241 
     | 
    
         
            +
                            parts << apply_visualization(token, :red)
         
     | 
| 
      
 242 
     | 
    
         
            +
                          end
         
     | 
| 
      
 243 
     | 
    
         
            +
                        when "+"
         
     | 
| 
      
 244 
     | 
    
         
            +
                          # Added token (only show on new side)
         
     | 
| 
      
 245 
     | 
    
         
            +
                          if side == :new
         
     | 
| 
      
 246 
     | 
    
         
            +
                            token = change.new_element
         
     | 
| 
      
 247 
     | 
    
         
            +
                            parts << apply_visualization(token, :green)
         
     | 
| 
      
 248 
     | 
    
         
            +
                          end
         
     | 
| 
      
 249 
     | 
    
         
            +
                        when "!"
         
     | 
| 
      
 250 
     | 
    
         
            +
                          # Changed token
         
     | 
| 
      
 251 
     | 
    
         
            +
                          if side == :old
         
     | 
| 
      
 252 
     | 
    
         
            +
                            token = change.old_element
         
     | 
| 
      
 253 
     | 
    
         
            +
                            parts << apply_visualization(token, :red)
         
     | 
| 
      
 254 
     | 
    
         
            +
                          else
         
     | 
| 
      
 255 
     | 
    
         
            +
                            token = change.new_element
         
     | 
| 
      
 256 
     | 
    
         
            +
                            parts << apply_visualization(token, :green)
         
     | 
| 
      
 257 
     | 
    
         
            +
                          end
         
     | 
| 
      
 258 
     | 
    
         
            +
                        end
         
     | 
| 
      
 259 
     | 
    
         
            +
                      end
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                      parts.join
         
     | 
| 
      
 262 
     | 
    
         
            +
                    end
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
                    # Apply character visualization
         
     | 
| 
      
 265 
     | 
    
         
            +
                    #
         
     | 
| 
      
 266 
     | 
    
         
            +
                    # @param token [String] Token to visualize
         
     | 
| 
      
 267 
     | 
    
         
            +
                    # @param color [Symbol, nil] Optional color
         
     | 
| 
      
 268 
     | 
    
         
            +
                    # @return [String] Visualized token
         
     | 
| 
      
 269 
     | 
    
         
            +
                    def apply_visualization(token, color = nil)
         
     | 
| 
      
 270 
     | 
    
         
            +
                      visual = token.chars.map do |char|
         
     | 
| 
      
 271 
     | 
    
         
            +
                        @visualization_map.fetch(char, char)
         
     | 
| 
      
 272 
     | 
    
         
            +
                      end.join
         
     | 
| 
      
 273 
     | 
    
         
            +
             
     | 
| 
      
 274 
     | 
    
         
            +
                      if color && @use_color
         
     | 
| 
      
 275 
     | 
    
         
            +
                        require "paint"
         
     | 
| 
      
 276 
     | 
    
         
            +
                        Paint[visual, color, :bold]
         
     | 
| 
      
 277 
     | 
    
         
            +
                      else
         
     | 
| 
      
 278 
     | 
    
         
            +
                        visual
         
     | 
| 
      
 279 
     | 
    
         
            +
                      end
         
     | 
| 
      
 280 
     | 
    
         
            +
                    end
         
     | 
| 
      
 281 
     | 
    
         
            +
                  end
         
     | 
| 
      
 282 
     | 
    
         
            +
                end
         
     | 
| 
      
 283 
     | 
    
         
            +
              end
         
     | 
| 
      
 284 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,190 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "base_formatter"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "../legend"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Canon
         
     | 
| 
      
 7 
     | 
    
         
            +
              class DiffFormatter
         
     | 
| 
      
 8 
     | 
    
         
            +
                module ByLine
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # Simple line-based formatter (fallback)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # Uses basic LCS diff without format-specific intelligence
         
     | 
| 
      
 11 
     | 
    
         
            +
                  class SimpleFormatter < BaseFormatter
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # Format simple line-by-line diff
         
     | 
| 
      
 13 
     | 
    
         
            +
                    #
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # @param doc1 [String] First document
         
     | 
| 
      
 15 
     | 
    
         
            +
                    # @param doc2 [String] Second document
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # @return [String] Formatted diff
         
     | 
| 
      
 17 
     | 
    
         
            +
                    def format(doc1, doc2)
         
     | 
| 
      
 18 
     | 
    
         
            +
                      output = []
         
     | 
| 
      
 19 
     | 
    
         
            +
                      # Use split with -1 to preserve trailing empty strings (from trailing \n)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      lines1 = doc1.split("\n", -1)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      lines2 = doc2.split("\n", -1)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                      # Detect non-ASCII characters in the diff
         
     | 
| 
      
 24 
     | 
    
         
            +
                      all_text = (lines1 + lines2).join
         
     | 
| 
      
 25 
     | 
    
         
            +
                      non_ascii = Legend.detect_non_ascii(all_text, @visualization_map)
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                      # Add Unicode legend if any non-ASCII characters detected
         
     | 
| 
      
 28 
     | 
    
         
            +
                      unless non_ascii.empty?
         
     | 
| 
      
 29 
     | 
    
         
            +
                        output << Legend.build_legend(non_ascii, use_color: @use_color)
         
     | 
| 
      
 30 
     | 
    
         
            +
                        output << ""
         
     | 
| 
      
 31 
     | 
    
         
            +
                      end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                      # Get LCS diff
         
     | 
| 
      
 34 
     | 
    
         
            +
                      diffs = ::Diff::LCS.sdiff(lines1, lines2)
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                      # Group into hunks with context
         
     | 
| 
      
 37 
     | 
    
         
            +
                      hunks = build_hunks(diffs, lines1, lines2,
         
     | 
| 
      
 38 
     | 
    
         
            +
                                          context_lines: @context_lines)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                      # Format each hunk
         
     | 
| 
      
 41 
     | 
    
         
            +
                      hunks.each do |hunk|
         
     | 
| 
      
 42 
     | 
    
         
            +
                        output << format_hunk(hunk)
         
     | 
| 
      
 43 
     | 
    
         
            +
                      end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                      output.join("\n")
         
     | 
| 
      
 46 
     | 
    
         
            +
                    end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    private
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    # Format a hunk of changes
         
     | 
| 
      
 51 
     | 
    
         
            +
                    #
         
     | 
| 
      
 52 
     | 
    
         
            +
                    # @param hunk [Array] Hunk of diff changes
         
     | 
| 
      
 53 
     | 
    
         
            +
                    # @return [String] Formatted hunk
         
     | 
| 
      
 54 
     | 
    
         
            +
                    def format_hunk(hunk)
         
     | 
| 
      
 55 
     | 
    
         
            +
                      output = []
         
     | 
| 
      
 56 
     | 
    
         
            +
                      old_line = hunk.first.old_position + 1
         
     | 
| 
      
 57 
     | 
    
         
            +
                      new_line = hunk.first.new_position + 1
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                      hunk.each do |change|
         
     | 
| 
      
 60 
     | 
    
         
            +
                        case change.action
         
     | 
| 
      
 61 
     | 
    
         
            +
                        when "="
         
     | 
| 
      
 62 
     | 
    
         
            +
                          # Unchanged line (context)
         
     | 
| 
      
 63 
     | 
    
         
            +
                          output << format_unified_line(old_line, new_line, " ",
         
     | 
| 
      
 64 
     | 
    
         
            +
                                                        change.old_element)
         
     | 
| 
      
 65 
     | 
    
         
            +
                          old_line += 1
         
     | 
| 
      
 66 
     | 
    
         
            +
                          new_line += 1
         
     | 
| 
      
 67 
     | 
    
         
            +
                        when "-"
         
     | 
| 
      
 68 
     | 
    
         
            +
                          # Deletion
         
     | 
| 
      
 69 
     | 
    
         
            +
                          output << format_unified_line(old_line, nil, "-",
         
     | 
| 
      
 70 
     | 
    
         
            +
                                                        change.old_element, :red)
         
     | 
| 
      
 71 
     | 
    
         
            +
                          old_line += 1
         
     | 
| 
      
 72 
     | 
    
         
            +
                        when "+"
         
     | 
| 
      
 73 
     | 
    
         
            +
                          # Addition
         
     | 
| 
      
 74 
     | 
    
         
            +
                          output << format_unified_line(nil, new_line, "+",
         
     | 
| 
      
 75 
     | 
    
         
            +
                                                        change.new_element, :green)
         
     | 
| 
      
 76 
     | 
    
         
            +
                          new_line += 1
         
     | 
| 
      
 77 
     | 
    
         
            +
                        when "!"
         
     | 
| 
      
 78 
     | 
    
         
            +
                          # Change - show both with inline diff highlighting
         
     | 
| 
      
 79 
     | 
    
         
            +
                          old_text = change.old_element
         
     | 
| 
      
 80 
     | 
    
         
            +
                          new_text = change.new_element
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                          # Format with inline highlighting
         
     | 
| 
      
 83 
     | 
    
         
            +
                          output << format_changed_line(old_line, old_text, new_text)
         
     | 
| 
      
 84 
     | 
    
         
            +
                          old_line += 1
         
     | 
| 
      
 85 
     | 
    
         
            +
                          new_line += 1
         
     | 
| 
      
 86 
     | 
    
         
            +
                        end
         
     | 
| 
      
 87 
     | 
    
         
            +
                      end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                      output.join("\n")
         
     | 
| 
      
 90 
     | 
    
         
            +
                    end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                    # Format a unified diff line
         
     | 
| 
      
 93 
     | 
    
         
            +
                    #
         
     | 
| 
      
 94 
     | 
    
         
            +
                    # @param old_num [Integer, nil] Line number in old file
         
     | 
| 
      
 95 
     | 
    
         
            +
                    # @param new_num [Integer, nil] Line number in new file
         
     | 
| 
      
 96 
     | 
    
         
            +
                    # @param marker [String] Diff marker (' ', '-', '+')
         
     | 
| 
      
 97 
     | 
    
         
            +
                    # @param content [String] Line content
         
     | 
| 
      
 98 
     | 
    
         
            +
                    # @param color [Symbol, nil] Color for diff lines
         
     | 
| 
      
 99 
     | 
    
         
            +
                    # @return [String] Formatted line
         
     | 
| 
      
 100 
     | 
    
         
            +
                    def format_unified_line(old_num, new_num, marker, content, color = nil)
         
     | 
| 
      
 101 
     | 
    
         
            +
                      old_str = old_num ? "%4d" % old_num : "    "
         
     | 
| 
      
 102 
     | 
    
         
            +
                      new_str = new_num ? "%4d" % new_num : "    "
         
     | 
| 
      
 103 
     | 
    
         
            +
                      marker_part = "#{marker} "
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                      # Only apply visualization to diff lines (when color is provided),
         
     | 
| 
      
 106 
     | 
    
         
            +
                      # not context lines
         
     | 
| 
      
 107 
     | 
    
         
            +
                      visualized_content = if color
         
     | 
| 
      
 108 
     | 
    
         
            +
                                             apply_visualization(content, color)
         
     | 
| 
      
 109 
     | 
    
         
            +
                                           else
         
     | 
| 
      
 110 
     | 
    
         
            +
                                             content
         
     | 
| 
      
 111 
     | 
    
         
            +
                                           end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                      if @use_color
         
     | 
| 
      
 114 
     | 
    
         
            +
                        # Yellow for line numbers and pipes
         
     | 
| 
      
 115 
     | 
    
         
            +
                        yellow_old = colorize(old_str, :yellow)
         
     | 
| 
      
 116 
     | 
    
         
            +
                        yellow_pipe1 = colorize("|", :yellow)
         
     | 
| 
      
 117 
     | 
    
         
            +
                        yellow_new = colorize(new_str, :yellow)
         
     | 
| 
      
 118 
     | 
    
         
            +
                        yellow_pipe2 = colorize("|", :yellow)
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                        if color
         
     | 
| 
      
 121 
     | 
    
         
            +
                          # Colored marker for additions/deletions
         
     | 
| 
      
 122 
     | 
    
         
            +
                          colored_marker = colorize(marker, color)
         
     | 
| 
      
 123 
     | 
    
         
            +
                          "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{colored_marker} #{yellow_pipe2} #{visualized_content}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                        else
         
     | 
| 
      
 125 
     | 
    
         
            +
                          # Context line - apply visualization but no color
         
     | 
| 
      
 126 
     | 
    
         
            +
                          "#{yellow_old}#{yellow_pipe1}#{yellow_new}#{marker} #{yellow_pipe2} #{visualized_content}"
         
     | 
| 
      
 127 
     | 
    
         
            +
                        end
         
     | 
| 
      
 128 
     | 
    
         
            +
                      else
         
     | 
| 
      
 129 
     | 
    
         
            +
                        # No color mode
         
     | 
| 
      
 130 
     | 
    
         
            +
                        "#{old_str}|#{new_str}#{marker_part}| #{visualized_content}"
         
     | 
| 
      
 131 
     | 
    
         
            +
                      end
         
     | 
| 
      
 132 
     | 
    
         
            +
                    end
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                    # Format changed lines with basic character-level diff
         
     | 
| 
      
 135 
     | 
    
         
            +
                    #
         
     | 
| 
      
 136 
     | 
    
         
            +
                    # @param line_num [Integer] Line number
         
     | 
| 
      
 137 
     | 
    
         
            +
                    # @param old_text [String] Old line text
         
     | 
| 
      
 138 
     | 
    
         
            +
                    # @param new_text [String] New line text
         
     | 
| 
      
 139 
     | 
    
         
            +
                    # @return [String] Formatted change
         
     | 
| 
      
 140 
     | 
    
         
            +
                    def format_changed_line(line_num, old_text, new_text)
         
     | 
| 
      
 141 
     | 
    
         
            +
                      output = []
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                      # Apply visualization
         
     | 
| 
      
 144 
     | 
    
         
            +
                      old_visualized = apply_visualization(old_text, :red)
         
     | 
| 
      
 145 
     | 
    
         
            +
                      new_visualized = apply_visualization(new_text, :green)
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                      # Format both lines with yellow line numbers and pipes
         
     | 
| 
      
 148 
     | 
    
         
            +
                      if @use_color
         
     | 
| 
      
 149 
     | 
    
         
            +
                        yellow_old = colorize("%4d" % line_num, :yellow)
         
     | 
| 
      
 150 
     | 
    
         
            +
                        yellow_pipe1 = colorize("|", :yellow)
         
     | 
| 
      
 151 
     | 
    
         
            +
                        yellow_new = colorize("%4d" % line_num, :yellow)
         
     | 
| 
      
 152 
     | 
    
         
            +
                        yellow_pipe2 = colorize("|", :yellow)
         
     | 
| 
      
 153 
     | 
    
         
            +
                        red_marker = colorize("-", :red)
         
     | 
| 
      
 154 
     | 
    
         
            +
                        green_marker = colorize("+", :green)
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                        output << "#{yellow_old}#{yellow_pipe1}    #{red_marker} #{yellow_pipe2} #{old_visualized}"
         
     | 
| 
      
 157 
     | 
    
         
            +
                        output << "    #{yellow_pipe1}#{yellow_new}#{green_marker} #{yellow_pipe2} #{new_visualized}"
         
     | 
| 
      
 158 
     | 
    
         
            +
                      else
         
     | 
| 
      
 159 
     | 
    
         
            +
                        old_str = "%4d" % line_num
         
     | 
| 
      
 160 
     | 
    
         
            +
                        new_str = "%4d" % line_num
         
     | 
| 
      
 161 
     | 
    
         
            +
                        output << "#{old_str}|    - | #{old_visualized}"
         
     | 
| 
      
 162 
     | 
    
         
            +
                        output << "    |#{new_str}+ | #{new_visualized}"
         
     | 
| 
      
 163 
     | 
    
         
            +
                      end
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
                      output.join("\n")
         
     | 
| 
      
 166 
     | 
    
         
            +
                    end
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
                    # Apply character visualization using configurable visualization map
         
     | 
| 
      
 169 
     | 
    
         
            +
                    #
         
     | 
| 
      
 170 
     | 
    
         
            +
                    # @param token [String] The token to apply visualization to
         
     | 
| 
      
 171 
     | 
    
         
            +
                    # @param color [Symbol, nil] Optional color to apply
         
     | 
| 
      
 172 
     | 
    
         
            +
                    # @return [String] Visualized and optionally colored token
         
     | 
| 
      
 173 
     | 
    
         
            +
                    def apply_visualization(token, color = nil)
         
     | 
| 
      
 174 
     | 
    
         
            +
                      # Replace each character with its visualization from the map
         
     | 
| 
      
 175 
     | 
    
         
            +
                      visual = token.chars.map do |char|
         
     | 
| 
      
 176 
     | 
    
         
            +
                        @visualization_map.fetch(char, char)
         
     | 
| 
      
 177 
     | 
    
         
            +
                      end.join
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
                      # Apply color if provided and color is enabled
         
     | 
| 
      
 180 
     | 
    
         
            +
                      if color && @use_color
         
     | 
| 
      
 181 
     | 
    
         
            +
                        require "paint"
         
     | 
| 
      
 182 
     | 
    
         
            +
                        Paint[visual, color, :bold]
         
     | 
| 
      
 183 
     | 
    
         
            +
                      else
         
     | 
| 
      
 184 
     | 
    
         
            +
                        visual
         
     | 
| 
      
 185 
     | 
    
         
            +
                      end
         
     | 
| 
      
 186 
     | 
    
         
            +
                    end
         
     | 
| 
      
 187 
     | 
    
         
            +
                  end
         
     | 
| 
      
 188 
     | 
    
         
            +
                end
         
     | 
| 
      
 189 
     | 
    
         
            +
              end
         
     | 
| 
      
 190 
     | 
    
         
            +
            end
         
     |