summaryrefslogtreecommitdiff
path: root/lib/gitlab/string_range_marker.rb
blob: 11aeec1ebfa8db640d76f13a55d5bc55141c93a7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
module Gitlab
  class StringRangeMarker
    attr_accessor :raw_line, :rich_line, :html_escaped

    def initialize(raw_line, rich_line = nil)
      @raw_line = raw_line.dup
      if rich_line.nil?
        @rich_line = raw_line.dup
        @html_escaped = false
      else
        @rich_line = ERB::Util.html_escape(rich_line)
        @html_escaped = true
      end
    end

    def mark(marker_ranges)
      return rich_line unless marker_ranges

      if html_escaped
        rich_marker_ranges = []
        marker_ranges.each do |range|
          # Map the inline-diff range based on the raw line to character positions in the rich line
          rich_positions = position_mapping[range].flatten
          # Turn the array of character positions into ranges
          rich_marker_ranges.concat(collapse_ranges(rich_positions))
        end
      else
        rich_marker_ranges = marker_ranges
      end

      offset = 0
      # Mark each range
      rich_marker_ranges.each_with_index do |range, i|
        offset_range = (range.begin + offset)..(range.end + offset)
        original_text = rich_line[offset_range]

        text = yield(original_text, left: i == 0, right: i == rich_marker_ranges.length - 1)

        rich_line[offset_range] = text

        offset += text.length - original_text.length
      end

      @html_escaped ? rich_line.html_safe : rich_line
    end

    private

    # Mapping of character positions in the raw line, to the rich (highlighted) line
    def position_mapping
      @position_mapping ||= begin
        mapping = []
        rich_pos = 0
        (0..raw_line.length).each do |raw_pos|
          rich_char = rich_line[rich_pos]

          # The raw and rich lines are the same except for HTML tags,
          # so skip over any `<...>` segment
          while rich_char == '<'
            until rich_char == '>'
              rich_pos += 1
              rich_char = rich_line[rich_pos]
            end

            rich_pos += 1
            rich_char = rich_line[rich_pos]
          end

          # multi-char HTML entities in the rich line correspond to a single character in the raw line
          if rich_char == '&'
            multichar_mapping = [rich_pos]
            until rich_char == ';'
              rich_pos += 1
              multichar_mapping << rich_pos
              rich_char = rich_line[rich_pos]
            end

            mapping[raw_pos] = multichar_mapping
          else
            mapping[raw_pos] = rich_pos
          end

          rich_pos += 1
        end

        mapping
      end
    end

    # Takes an array of integers, and returns an array of ranges covering the same integers
    def collapse_ranges(positions)
      return [] if positions.empty?
      ranges = []

      start = prev = positions[0]
      range = start..prev
      positions[1..-1].each do |pos|
        if pos == prev + 1
          range = start..pos
          prev = pos
        else
          ranges << range
          start = prev = pos
          range = start..prev
        end
      end
      ranges << range

      ranges
    end
  end
end