# -*- coding: utf-8 -*- # # Copyright 2013 whiteleaf. All rights reserved. # require "diff/lcs" require "termcolor" # # 指定されたソースの差分を作成する # # 書式はなんちゃってunified contextなので、パッチ作成には使えません # class DiffViewer class InvalidSourceType < StandardError; end def initialize(old, new, source = :file) case source when :file old_strings = File.read(old, :encoding => Encoding::UTF_8) new_strings = File.read(new, :encoding => Encoding::UTF_8) when :string old_strings = old new_strings = new else raise InvalidSourceType, "source supported options are ':file' and ':string'" end old_strings.gsub!("\r", "") new_strings.gsub!("\r", "") @builded_buffer = "" @events = Diff::LCS.sdiff(old_strings.split("\n"), new_strings.split("\n")) build end def to_s @builded_buffer end def view puts @builded_buffer end private def build @buffer = {} pos = 0 events_size = @events.size while pos < events_size pos = output_differing_line_and_before_and_behind(pos) end before_index = -1000 @builded_buffer = @buffer.sort_by { |index| # buffer の格納順はバラバラなのでソートしておく index }.map { |(index, (event, str))| # index が途切れたら、ポジション情報を付与する result = "" if index - before_index >= 2 result += "@@ -#{event.old_position+1}, " \ "+#{event.new_position+1} @@\n".termcolor end result += str before_index = index result }.join("\n") end # # 指定ラインが編集・追加・削除されていた行だったら前後3行をバッファに出力 # def output_differing_line_and_before_and_behind(index, force = false) last = index + 1 return last if @buffer[index] || index < 0 event = @events[index] or return last if %w(! + -).include?(event.action) @buffer[index] = [event, decorate_event(event)] ((index - 3)..(index + 3)).each do |i| output_differing_line_and_before_and_behind(i, true) end last = index + 3 + 1 elsif force @buffer[index] = [event, " #{event.old_element}"] end last end # # レーベンシュタイン距離を計算する # # differ には Diff::LCS.sdiff で処理したものをそのまま渡す # def calc_levenshtein_distance(differ) differ.reject { |e| e.unchanged? }.count end # # event.action を見てラインを装飾 # def decorate_event(event) result = "" old_element = event.old_element new_element = event.new_element case event.action when "!" old_str = "" new_str = "" line_events = Diff::LCS.sdiff(old_element, new_element) distance = calc_levenshtein_distance(line_events) # レーベンシュタイン距離を正規化する size = [old_element.length, new_element.length].max normalized_distance = distance / size.to_f if normalized_distance > 0.7 # 双方の文字列があまりにも似ていない場合、編集部分をカラー化すると # 非常に見づらい表示になってしまうので、単純に削除・追加のみ装飾する old_str = TermColor.escape(old_element) new_str = TermColor.escape(new_element) else line_events.each do |e| os = TermColor.escape(e.old_element) rescue "" ns = TermColor.escape(e.new_element) rescue "" case e.action when "=" old_str += os new_str += ns when "!" os = "#{os}" if os == " " || os == " " ns = "#{ns}" if ns == " " || os == " " old_str += "#{os}" new_str += "#{ns}" when "-" old_str += "#{os}" when "+" new_str += "#{ns}" end end end result = "-#{old_str}\n" \ "+#{new_str}" when "-" result = "-#{TermColor.escape(old_element)}" when "+" result = "+#{TermColor.escape(new_element)}" end result.termcolor end end