Class: Canon::Diff::DiffNodeMapper

Inherits:
Object
  • Object
show all
Defined in:
lib/canon/diff/diff_node_mapper.rb

Overview

Maps semantic DiffNodes to textual DiffLines This is Layer 2 of the diff pipeline, bridging semantic differences (from comparators) to textual representation (for formatters)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(diff_nodes, text1, text2, options = {}) ⇒ DiffNodeMapper

Returns a new instance of DiffNodeMapper.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/canon/diff/diff_node_mapper.rb', line 25

def initialize(diff_nodes, text1, text2, options = {})
  @diff_nodes = diff_nodes
  @text1 = text1
  @text2 = text2
  @line_map1 = options[:line_map1]
  @line_map2 = options[:line_map2]

  # Pre-compute comment line ranges for multi-line comment handling
  @comment_lines1 = build_comment_lines(@text1)
  @comment_lines2 = build_comment_lines(@text2)
  @comment_diff_nodes = if @diff_nodes
                          @diff_nodes.select do |n|
                            n.dimension == :comments
                          end
                        else
                          []
                        end
end

Class Method Details

.map(diff_nodes, text1, text2, options = {}) ⇒ Array<DiffLine>

Map diff nodes to diff lines

Parameters:

  • diff_nodes (Array<DiffNode>)

    The semantic differences

  • text1 (String)

    The first text being compared

  • text2 (String)

    The second text being compared

  • options (Hash) (defaults to: {})

    Mapping options

Options Hash (options):

  • :line_map1 (Hash)

    Pre-built line range map for text1

  • :line_map2 (Hash)

    Pre-built line range map for text2

Returns:

  • (Array<DiffLine>)

    Diff lines with semantic linkage



21
22
23
# File 'lib/canon/diff/diff_node_mapper.rb', line 21

def self.map(diff_nodes, text1, text2, options = {})
  new(diff_nodes, text1, text2, options).map
end

Instance Method Details

#mapObject



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/canon/diff/diff_node_mapper.rb', line 44

def map
  lines1 = @text1.split("\n")
  lines2 = @text2.split("\n")

  # Use LCS to get structural diff
  require "diff/lcs"
  lcs_diffs = ::Diff::LCS.sdiff(lines1, lines2)

  # Check if ALL DiffNodes are informative
  all_informative = @diff_nodes && !@diff_nodes.empty? &&
    @diff_nodes.all?(&:informative?)

  # Convert LCS diffs to DiffLines
  # If all DiffNodes are informative, we create a single shared informative DiffNode
  # for all changed lines (this avoids complex linking)
  shared_informative_node = if all_informative
                              @diff_nodes.first # Use any informative node
                            end

  diff_lines = []

  lcs_diffs.each do |change|
    diff_line = case change.action
                when "="
                  DiffLine.new(
                    line_number: change.old_position,
                    new_position: change.new_position,
                    content: change.old_element,
                    type: :unchanged,
                    diff_node: nil,
                  )
                when "-"
                  node = shared_informative_node ||
                    find_diff_node_for_line(
                      change.old_position, lines1, :removed,
                      comment_lines: @comment_lines1
                    )

                  formatting = formatting?(node, change.old_element, "")

                  DiffLine.new(
                    line_number: change.old_position,
                    content: change.old_element,
                    type: :removed,
                    diff_node: node,
                    formatting: formatting,
                  )
                when "+"
                  node = shared_informative_node ||
                    find_diff_node_for_line(
                      change.new_position, lines2, :added,
                      comment_lines: @comment_lines2
                    )

                  formatting = formatting?(node, "", change.new_element)

                  DiffLine.new(
                    line_number: change.new_position,
                    content: change.new_element,
                    type: :added,
                    diff_node: node,
                    formatting: formatting,
                  )
                when "!"
                  node = shared_informative_node ||
                    find_diff_node_for_line(
                      change.new_position, lines2, :changed,
                      comment_lines: @comment_lines2,
                      old_content: change.old_element
                    )

                  formatting = formatting?(node,
                                           change.old_element,
                                           change.new_element)

                  DiffLine.new(
                    line_number: change.old_position,
                    content: change.new_element,
                    type: :changed,
                    diff_node: node,
                    formatting: formatting,
                    new_position: change.new_position,
                  )
                end

    diff_lines << diff_line
  end

  # Post-process: detect multi-line formatting changes that
  # per-line comparison misses (e.g., tag wrapping from 2 lines to 1,
  # element reflow with different line counts).
  apply_block_formatting!(diff_lines, lcs_diffs)

  # Post-process: merge adjacent "-" lines into preceding "!" changes
  # when the removed content already appears in the new line.
  # This handles the case where N old lines map to 1 new line
  # (e.g., closing tag on its own line merged into previous line).
  merge_adjacent_removals!(diff_lines, lines1)

  diff_lines
end