blob: 3bd2d80df5491e78616b3a7dcb744a020e36857a [file] [log] [blame]
require 'cgi'
require 'pp'
require 'set'
module PrettyPatch
public
def self.prettify(string)
fileDiffs = FileDiff.parse(string)
str = HEADER + "\n"
str += fileDiffs.collect{ |diff| diff.to_html }.join
end
def self.filename_from_diff_header(line)
DIFF_HEADER_FORMATS.each do |format|
match = format.match(line)
return match[1] unless match.nil?
end
nil
end
def self.diff_header?(line)
RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format }
end
private
DIFF_HEADER_FORMATS = [
/^Index: (.*)\r?$/,
/^diff --git a\/.+ b\/(.+)\r?$/
]
RELAXED_DIFF_HEADER_FORMATS = [
/^Index:/,
/^diff/
]
START_OF_SECTION_FORMAT = /^@@ -(\d+),\d+ \+(\d+)(?:,\d+)? @@\s*(.*)/
START_OF_EXTENT_STRING = "%c" % 0
END_OF_EXTENT_STRING = "%c" % 1
OPENSOURCE_TRAC_URL = "http://trac.webkit.org/projects/webkit/"
OPENSOURCE_DIRS = Set.new [
"Bakefiles",
"JavaScriptCore",
"JavaScriptGlue",
"LayoutTestResults",
"LayoutTests",
"PageLoadTests",
"WebCore",
"WebKit",
"WebKitLibraries",
"WebKitQt",
"WebKitSite",
"WebKitTools",
]
def self.linkifyFilename(filename)
directory = /^([^\/]+)/.match(filename)[1]
url, pathBeneathTrunk = if filename =~ /\bOpenSource\// then
[OPENSOURCE_TRAC_URL, filename.gsub(/^.*\bOpenSource\//, '')]
elsif OPENSOURCE_DIRS.include?(directory) then
[OPENSOURCE_TRAC_URL, filename]
else
[nil, nil]
end
url.nil? ? filename : "<a href='#{url}browser/trunk/#{pathBeneathTrunk}'>#{filename}</a>"
end
HEADER =<<EOF
<style>
:link, :visited {
text-decoration: none;
border-bottom: 1px dotted;
}
.FileDiff {
background-color: #f8f8f8;
border: 1px solid #ddd;
font-family: monospace;
margin: 2em 0px;
}
h1 {
color: #333;
font-family: sans-serif;
font-size: 1em;
margin-left: 0.5em;
}
h1 :link, h1 :visited {
color: inherit;
}
h1 :hover {
color: #555;
background-color: #eee;
}
.DiffSection {
background-color: white;
border: solid #ddd;
border-width: 1px 0px;
}
.lineNumber {
background-color: #eed;
border-bottom: 1px solid #998;
border-right: 1px solid #ddd;
color: #444;
display: inline-block;
padding: 1px 5px 0px 0px;
text-align: right;
vertical-align: bottom;
width: 3em;
}
.text {
padding-left: 5px;
white-space: pre;
white-space: pre-wrap;
}
.context, .context .lineNumber {
color: #849;
background-color: #fef;
}
.add {
background-color: #dfd;
}
.add ins {
background-color: #9e9;
text-decoration: none;
}
.remove {
background-color: #fdd;
}
.remove del {
background-color: #e99;
text-decoration: none;
}
</style>
EOF
def self.revisionOrDescription(string)
case string
when /\(revision \d+\)/
/\(revision (\d+)\)/.match(string)[1]
when /\(.*\)/
/\((.*)\)/.match(string)[1]
end
end
class FileDiff
def initialize(lines)
@filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
startOfSections = 1
for i in 1...lines.length
case lines[i]
when /^--- /
@from = PrettyPatch.revisionOrDescription(lines[i])
when /^\+\+\+ /
@to = PrettyPatch.revisionOrDescription(lines[i])
startOfSections = i + 1
break
end
end
@sections = DiffSection.parse(lines[startOfSections...lines.length])
nil
end
def to_html
str = "<div class='FileDiff'>\n"
str += "<h1>#{PrettyPatch.linkifyFilename(@filename)}</h1>\n"
#str += "<span class='from'>#{@from}</span>\n"
str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
str += "</div>\n"
end
def self.parse(string)
linesForDiffs = string.inject([]) do |diffChunks, line|
diffChunks << [] if PrettyPatch.diff_header?(line)
diffChunks.last << line unless diffChunks.last.nil?
diffChunks
end
linesForDiffs.collect { |lines| FileDiff.new(lines) }
end
end
class DiffSection
def initialize(lines)
lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
matches = START_OF_SECTION_FORMAT.match(lines[0])
from, to = [matches[1].to_i, matches[2].to_i] unless matches.nil?
@lines = lines[1...lines.length].collect do |line|
startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
text = line[startOfLine...line.length].chomp
case line[0]
when ?-
result = CodeLine.new(from, nil, text)
from += 1 unless from.nil?
result
when ?+
result = CodeLine.new(nil, to, text)
to += 1 unless to.nil?
result
else
result = CodeLine.new(from, to, text)
from += 1 unless from.nil?
to += 1 unless to.nil?
result
end
end
@lines.unshift(ContextLine.new(matches[3])) unless matches.nil? || matches[3].empty?
changes = [ [ [], [] ] ]
for line in @lines
if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
changes << [ [], [] ]
next
end
changes.last.first << line if line.toLineNumber.nil?
changes.last.last << line if line.fromLineNumber.nil?
end
for change in changes
next unless change.first.length == change.last.length
for i in (0...change.first.length)
change.first[i].setChangeExtentFromLine(change.last[i])
change.last[i].changeExtent = change.first[i].changeExtent
end
end
end
def to_html
str = "<div class='DiffSection'>\n"
str += @lines.collect{ |line| line.to_html }.join
str += "</div>\n"
end
def self.parse(lines)
linesForSections = lines.inject([[]]) do |sections, line|
sections << [] if line =~ /^@@/
sections.last << line
sections
end
linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
linesForSections.collect { |lines| DiffSection.new(lines) }
end
end
class Line
attr_reader :fromLineNumber
attr_reader :toLineNumber
attr_reader :text
def initialize(from, to, text)
@fromLineNumber = from
@toLineNumber = to
@text = text
end
def text_as_html
CGI.escapeHTML(text)
end
def classes
lineClasses = ["Line"]
lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
lineClasses
end
def to_html
markedUpText = self.text_as_html
str = "<div class='%s'>\n" % self.classes.join(' ')
str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>\n" %
[@fromLineNumber.nil? ? '&nbsp;' : @fromLineNumber,
@toLineNumber.nil? ? '&nbsp;' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
str += "<span class='text'>%s</span>\n" % markedUpText
str += "</div>\n"
end
end
class CodeLine < Line
attr :changeExtent, true
def setChangeExtentFromLine(line)
limit = [@text.length, line.text.length].min
start = 0
while (start < limit && @text[start] == line.text[start])
start += 1
end
limit -= start
eend = -1
while (-eend <= limit && @text[eend] == line.text[eend])
eend -= 1
end
@changeExtent = start..eend
end
def text_as_html
html = @text
html = html.insert(@changeExtent.begin, START_OF_EXTENT_STRING).insert(@changeExtent.end, END_OF_EXTENT_STRING) unless @changeExtent.nil?
html = CGI.escapeHTML(html)
html.gsub(START_OF_EXTENT_STRING, @fromLineNumber.nil? ? "<ins>" : "<del>").gsub(END_OF_EXTENT_STRING, @fromLineNumber.nil? ? "</ins>" : "</del>")
end
end
class ContextLine < Line
def initialize(context)
super("@", "@", context)
end
def classes
super << "context"
end
end
end