string_range_marker.rb 3.06 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
module Gitlab
  class StringRangeMarker
5
    attr_accessor :raw_line, :rich_line, :html_escaped
6

7 8 9 10 11 12 13 14 15
    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
16 17 18
    end

    def mark(marker_ranges)
19
      return rich_line unless marker_ranges&.any?
20

21 22 23 24 25 26 27 28 29 30
      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
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
      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

46
      @html_escaped ? rich_line.html_safe : rich_line
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
    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?
95

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
      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