Commit 2b68d4d3 authored by Vasilii Iakliushin's avatar Vasilii Iakliushin

Introduce MarkerRange class

Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/16950

MarkerRange behaves like a Range but can additionally keep information
about the type of the change. It helps to identify if changed
characters were removed or added.
parent 2aab7082
...@@ -4,8 +4,8 @@ module DiffHelper ...@@ -4,8 +4,8 @@ module DiffHelper
def mark_inline_diffs(old_line, new_line) def mark_inline_diffs(old_line, new_line)
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion) marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs)
marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition) marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs)
[marked_old_line, marked_new_line] [marked_old_line, marked_new_line]
end end
......
...@@ -125,8 +125,8 @@ module SystemNotes ...@@ -125,8 +125,8 @@ module SystemNotes
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs, mode: :deletion) marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs)
marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs, mode: :addition) marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs)
body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**" body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
......
...@@ -120,7 +120,7 @@ module Banzai ...@@ -120,7 +120,7 @@ module Banzai
end end
def autolink_filter(text) def autolink_filter(text)
Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:| Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:, mode:|
autolink_match(link).html_safe autolink_match(link).html_safe
end end
end end
......
...@@ -76,7 +76,7 @@ module Banzai ...@@ -76,7 +76,7 @@ module Banzai
end end
def spaced_link_filter(text) def spaced_link_filter(text)
Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_OR_IMAGE_PATTERN) do |link, left:, right:| Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_OR_IMAGE_PATTERN) do |link, left:, right:, mode:|
spaced_link_match(link).html_safe spaced_link_match(link).html_safe
end end
end end
......
...@@ -80,7 +80,7 @@ module Gitlab ...@@ -80,7 +80,7 @@ module Gitlab
highlighted_lines.map!.with_index do |rich_line, i| highlighted_lines.map!.with_index do |rich_line, i|
marker = StringRegexMarker.new((plain_lines[i].chomp! || plain_lines[i]), rich_line.html_safe) marker = StringRegexMarker.new((plain_lines[i].chomp! || plain_lines[i]), rich_line.html_safe)
marker.mark(regex, group: :name) do |text, left:, right:| marker.mark(regex, group: :name) do |text, left:, right:, mode:|
url = yield(text) url = yield(text)
url ? link_tag(text, url) : text url ? link_tag(text, url) : text
end end
......
...@@ -22,7 +22,7 @@ module Gitlab ...@@ -22,7 +22,7 @@ module Gitlab
i, j = match.offset(:name) i, j = match.offset(:name)
marker = StringRangeMarker.new(plain_line, rich_line.html_safe) marker = StringRangeMarker.new(plain_line, rich_line.html_safe)
marker.mark([i..(j - 1)]) do |text, left:, right:| marker.mark([i..(j - 1)]) do |text, left:, right:, mode:|
url = package_url(text, match[:version]) url = package_url(text, match[:version])
url ? link_tag(text, url) : text url ? link_tag(text, url) : text
end end
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
i2, j2 = match.offset(:checksum) i2, j2 = match.offset(:checksum)
marker = StringRangeMarker.new(plain_line, rich_line.html_safe) marker = StringRangeMarker.new(plain_line, rich_line.html_safe)
marker.mark([i0..(j0 - 1), i2..(j2 - 1)]) do |text, left:, right:| marker.mark([i0..(j0 - 1), i2..(j2 - 1)]) do |text, left:, right:, mode:|
if left if left
url = package_url(text, match[:version]) url = package_url(text, match[:version])
url ? link_tag(text, url) : text url ? link_tag(text, url) : text
......
...@@ -32,12 +32,12 @@ module Gitlab ...@@ -32,12 +32,12 @@ module Gitlab
end end
if action == :delete if action == :delete
old_diffs << (old_pointer..(old_pointer + content_size - 1)) old_diffs << MarkerRange.new(old_pointer, old_pointer + content_size - 1, mode: MarkerRange::DELETION)
old_pointer += content_size old_pointer += content_size
end end
if action == :insert if action == :insert
new_diffs << (new_pointer..(new_pointer + content_size - 1)) new_diffs << MarkerRange.new(new_pointer, new_pointer + content_size - 1, mode: MarkerRange::ADDITION)
new_pointer += content_size new_pointer += content_size
end end
end end
......
...@@ -8,8 +8,8 @@ module Gitlab ...@@ -8,8 +8,8 @@ module Gitlab
deletion: "-" deletion: "-"
}.freeze }.freeze
def mark(line_inline_diffs, mode: nil) def mark(line_inline_diffs)
super(line_inline_diffs) do |text, left:, right:| super(line_inline_diffs) do |text, left:, right:, mode:|
symbol = MARKDOWN_SYMBOLS[mode] symbol = MARKDOWN_SYMBOLS[mode]
"{#{symbol}#{text}#{symbol}}" "{#{symbol}#{text}#{symbol}}"
end end
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
super(line, rich_line || line) super(line, rich_line || line)
end end
def mark(line_inline_diffs, mode: nil) def mark(line_inline_diffs)
super(line_inline_diffs) do |text, left:, right:| super(line_inline_diffs) do |text, left:, right:, mode:|
%{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}.html_safe %{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}.html_safe
end end
end end
......
# frozen_string_literal: true
# It is a Range object extended with `mode` attribute
# MarkerRange not only keeps information about changed characters, but also
# the type of changes
module Gitlab
class MarkerRange < Range
DELETION = :deletion
ADDITION = :addition
# Converts Range object to MarkerRange class
def self.from_range(range)
return range if range.is_a?(self)
new(range.begin, range.end, exclude_end: range.exclude_end?)
end
def initialize(first, last, exclude_end: false, mode: nil)
super(first, last, exclude_end)
@mode = mode
end
attr_reader :mode
end
end
...@@ -15,8 +15,10 @@ module Gitlab ...@@ -15,8 +15,10 @@ module Gitlab
end end
end end
def mark(marker_ranges) def mark(ranges)
return rich_line unless marker_ranges&.any? return rich_line unless ranges&.any?
marker_ranges = ranges.map { |range| Gitlab::MarkerRange.from_range(range) }
if html_escaped if html_escaped
rich_marker_ranges = [] rich_marker_ranges = []
...@@ -24,7 +26,7 @@ module Gitlab ...@@ -24,7 +26,7 @@ module Gitlab
# Map the inline-diff range based on the raw line to character positions in the rich line # Map the inline-diff range based on the raw line to character positions in the rich line
rich_positions = position_mapping[range].flatten rich_positions = position_mapping[range].flatten
# Turn the array of character positions into ranges # Turn the array of character positions into ranges
rich_marker_ranges.concat(collapse_ranges(rich_positions)) rich_marker_ranges.concat(collapse_ranges(rich_positions, range.mode))
end end
else else
rich_marker_ranges = marker_ranges rich_marker_ranges = marker_ranges
...@@ -36,7 +38,7 @@ module Gitlab ...@@ -36,7 +38,7 @@ module Gitlab
offset_range = (range.begin + offset)..(range.end + offset) offset_range = (range.begin + offset)..(range.end + offset)
original_text = rich_line[offset_range] original_text = rich_line[offset_range]
text = yield(original_text, left: i == 0, right: i == rich_marker_ranges.length - 1) text = yield(original_text, left: i == 0, right: i == rich_marker_ranges.length - 1, mode: range.mode)
rich_line[offset_range] = text rich_line[offset_range] = text
...@@ -90,21 +92,21 @@ module Gitlab ...@@ -90,21 +92,21 @@ module Gitlab
end end
# Takes an array of integers, and returns an array of ranges covering the same integers # Takes an array of integers, and returns an array of ranges covering the same integers
def collapse_ranges(positions) def collapse_ranges(positions, mode)
return [] if positions.empty? return [] if positions.empty?
ranges = [] ranges = []
start = prev = positions[0] start = prev = positions[0]
range = start..prev range = MarkerRange.new(start, prev, mode: mode)
positions[1..-1].each do |pos| positions[1..-1].each do |pos|
if pos == prev + 1 if pos == prev + 1
range = start..pos range = MarkerRange.new(start, pos, mode: mode)
prev = pos prev = pos
else else
ranges << range ranges << range
start = prev = pos start = prev = pos
range = start..prev range = MarkerRange.new(start, prev, mode: mode)
end end
end end
ranges << range ranges << range
......
...@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Diff::Highlight do ...@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Diff::Highlight do
end end
it 'highlights and marks added lines' do it 'highlights and marks added lines' do
code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].rich_text).to eq(code) expect(subject[5].rich_text).to eq(code)
end end
...@@ -93,7 +93,7 @@ RSpec.describe Gitlab::Diff::Highlight do ...@@ -93,7 +93,7 @@ RSpec.describe Gitlab::Diff::Highlight do
end end
it 'marks added lines' do it 'marks added lines' do
code = %q{+ raise <span class="idiff left right">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;} code = %q{+ raise <span class="idiff left right addition">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
expect(subject[5].rich_text).to eq(code) expect(subject[5].rich_text).to eq(code)
expect(subject[5].rich_text).to be_html_safe expect(subject[5].rich_text).to be_html_safe
......
...@@ -5,8 +5,8 @@ require 'spec_helper' ...@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::InlineDiffMarkdownMarker do RSpec.describe Gitlab::Diff::InlineDiffMarkdownMarker do
describe '#mark' do describe '#mark' do
let(:raw) { "abc 'def'" } let(:raw) { "abc 'def'" }
let(:inline_diffs) { [2..5] } let(:inline_diffs) { [Gitlab::MarkerRange.new(2, 5, mode: Gitlab::MarkerRange::DELETION)] }
let(:subject) { described_class.new(raw).mark(inline_diffs, mode: :deletion) } let(:subject) { described_class.new(raw).mark(inline_diffs) }
it 'does not escape html etities and marks the range' do it 'does not escape html etities and marks the range' do
expect(subject).to eq("ab{-c 'd-}ef'") expect(subject).to eq("ab{-c 'd-}ef'")
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::MarkerRange do
subject(:marker_range) { described_class.new(first, last, mode: mode) }
let(:first) { 1 }
let(:last) { 10 }
let(:mode) { nil }
it { is_expected.to eq(first..last) }
it 'behaves like a Range' do
is_expected.to be_kind_of(Range)
end
describe '#mode' do
subject { marker_range.mode }
it { is_expected.to be_nil }
context 'when mode is provided' do
let(:mode) { :deletion }
it { is_expected.to eq(mode) }
end
end
describe '.from_range' do
subject { described_class.from_range(range) }
let(:range) { 1..3 }
it 'converts Range to MarkerRange object' do
is_expected.to be_a(described_class)
end
it 'keeps correct range' do
is_expected.to eq(range)
end
context 'when range excludes end' do
let(:range) { 1...3 }
it 'keeps correct range' do
is_expected.to eq(range)
end
end
context 'when range is already a MarkerRange' do
let(:range) { marker_range }
it { is_expected.to be(marker_range) }
end
end
end
...@@ -8,7 +8,7 @@ RSpec.describe Gitlab::StringRangeMarker do ...@@ -8,7 +8,7 @@ RSpec.describe Gitlab::StringRangeMarker do
raw = 'abc <def>' raw = 'abc <def>'
inline_diffs = [2..5] inline_diffs = [2..5]
described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:| described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:, mode:|
"LEFT#{text}RIGHT".html_safe "LEFT#{text}RIGHT".html_safe
end end
end end
......
...@@ -9,7 +9,7 @@ RSpec.describe Gitlab::StringRegexMarker do ...@@ -9,7 +9,7 @@ RSpec.describe Gitlab::StringRegexMarker do
let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
subject do subject do
described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:, mode:|
%{<a href="#">#{text}</a>}.html_safe %{<a href="#">#{text}</a>}.html_safe
end end
end end
...@@ -25,7 +25,7 @@ RSpec.describe Gitlab::StringRegexMarker do ...@@ -25,7 +25,7 @@ RSpec.describe Gitlab::StringRegexMarker do
let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe } let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe }
subject do subject do
described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:| described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:, mode:|
%{<strong>#{text}</strong>}.html_safe %{<strong>#{text}</strong>}.html_safe
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment