161 lines
7.4 KiB
Lua
161 lines
7.4 KiB
Lua
-- LuaDiffer: A simple Lua diff library
|
||
--
|
||
-- This file returns a function that can be called on two strings to return a table
|
||
-- containing the details of the difference between them. Usage:
|
||
-- >> local diff = require("diff")
|
||
-- >> diff("hello\nworld", "hello\nWORLD\n!!!")
|
||
-- {{old="hello\n", old_line=1, old_line_end=1, new="hello\n", new_line=1, new_line_end=1},
|
||
-- {old="world", old_line=2, old_line_end=2, new="WORLD\n!!!", new_line=2, new_line_end=3}}
|
||
-- The returned diff table also has a diff:print() method on it that can be used to print
|
||
-- the diff in a human-readable form. See below for the print options.
|
||
--
|
||
-- This implementation uses the Hunt–McIlroy algorithm, and is inspired by Jason Orendorff's
|
||
-- lovely python implementation at: http://pynash.org/2013/02/26/diff-in-50-lines/
|
||
--
|
||
-- Copyright 2017 Bruce Hill
|
||
--
|
||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||
-- this software and associated documentation files (the "Software"), to deal in
|
||
-- the Software without restriction, including without limitation the rights to
|
||
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||
-- so, subject to the following conditions:
|
||
--
|
||
-- The above copyright notice and this permission notice shall be included in all
|
||
-- copies or substantial portions of the Software.
|
||
--
|
||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
-- SOFTWARE.
|
||
|
||
-- ANSI color codes
|
||
local function color(n) return string.char(27)..("[%dm"):format(n) end
|
||
local COLORS = {green=color(32), red=color(31), bright=color(1), underscore=color(4), reset=color(0)}
|
||
|
||
-- With a diff, you can call diff:print{...} to print it out. The options available are:
|
||
-- color = (true/false), true by default, whether or not to print with colors.
|
||
-- context = <number>, how many lines of context to print around the diff, defaults to infinite
|
||
-- line_numbers = (true/false), false by default, whether or not to print line numbers
|
||
local function print_diff(d, options)
|
||
options = options or {}
|
||
local colors = (options.color ~= false) and COLORS or setmetatable({}, {__index=function() return "" end})
|
||
local line_numbers = options.line_numbers or false
|
||
local context_pattern = nil
|
||
if options.context and options.context ~= 0 and options.context ~= math.huge then
|
||
local lines = ("[^\n]*\n"):rep(options.context)
|
||
context_pattern = ("^("..lines..").*\n("..lines..")$")
|
||
end
|
||
for i,chunk in ipairs(d) do
|
||
if chunk.old == chunk.new then
|
||
-- Unchanged
|
||
local old = chunk.old
|
||
if context_pattern then
|
||
local before, after = old:match(context_pattern)
|
||
if before and after then
|
||
-- "\b" is used as a hack to make sure the ellipsis is unindented
|
||
if i == 1 then old = "\b\b…\n"..after
|
||
elseif i == #d then old = before.."\n\b\b…"
|
||
else old = before.."\b\b…\n"..after end
|
||
end
|
||
end
|
||
if not (options and options.context == 0) then
|
||
io.write((old:gsub("[^\n]*\n?", " %1")))
|
||
end
|
||
else
|
||
-- Changed
|
||
if #chunk.old > 0 then
|
||
if line_numbers then
|
||
print(colors.underscore..colors.bright..colors.red..
|
||
("Old line %d-%d:"):format(chunk.old_line, chunk.old_line_end)..colors.reset)
|
||
end
|
||
io.write(colors.red..(chunk.old:gsub("[^\n]*\n?", "- %1"))..colors.reset)
|
||
end
|
||
if #chunk.new > 0 then
|
||
if line_numbers then
|
||
print(colors.underscore..colors.bright..colors.green..
|
||
("New line %d-%d:"):format(chunk.new_line, chunk.new_line_end)..colors.reset)
|
||
end
|
||
io.write(colors.green..(chunk.new:gsub("[^\n]*\n?", "+ %1"))..colors.reset)
|
||
end
|
||
end
|
||
end
|
||
io.write('\n')
|
||
end
|
||
|
||
local diff_mt = {__index={print=print_diff}}
|
||
|
||
-- Take two strings, and return a table representing a line-by-line diff of the two.
|
||
-- The return value is a list of changes, that have have a .change of "+", "-", or nil
|
||
-- and a .lines with the relevant lines.
|
||
local function diff(old, new)
|
||
local insert, concat = table.insert, table.concat
|
||
|
||
-- Split into lines
|
||
local A, B = {}, {}
|
||
for c in old:gmatch("[^\n]*\n?") do insert(A, c) end
|
||
for c in new:gmatch("[^\n]*\n?") do insert(B, c) end
|
||
|
||
-- Find the longest common subsequence between A[a_min..a_max] and B[b_min..b_max] (inclusive),
|
||
-- and return (the starting position in a), (the starting position in b), (the length)
|
||
local longest_common_subsequence = function(a_min,a_max, b_min,b_max)
|
||
local longest = {a=a_min, b=b_min, length=0}
|
||
local runs = {}
|
||
for a = a_min, a_max do
|
||
local new_runs = {}
|
||
for b = b_min, b_max do
|
||
if A[a] == B[b] then
|
||
local new_run_len = 1 + (runs[b-1] or 0)
|
||
new_runs[b] = new_run_len
|
||
if new_run_len > longest.length then
|
||
longest.a = a - new_run_len + 1
|
||
longest.b = b - new_run_len + 1
|
||
longest.length = new_run_len
|
||
end
|
||
end
|
||
end
|
||
runs = new_runs
|
||
end
|
||
return longest
|
||
end
|
||
|
||
-- Find *all* the common subsequences between A[a_min..a_max] and B[b_min..b_max] (inclusive)
|
||
-- and put them into the common_subsequences table.
|
||
local common_subsequences = {}
|
||
local find_common_subsequences
|
||
find_common_subsequences = function(a_min,a_max, b_min,b_max)
|
||
-- Take a greedy approach and pull out the longest subsequences first
|
||
local lcs = longest_common_subsequence(a_min,a_max, b_min,b_max)
|
||
if lcs.length == 0 then return end
|
||
find_common_subsequences(a_min, lcs.a - 1, b_min, lcs.b - 1)
|
||
insert(common_subsequences, lcs)
|
||
find_common_subsequences(lcs.a + lcs.length, a_max, lcs.b + lcs.length, b_max)
|
||
end
|
||
|
||
find_common_subsequences(1,#A, 1,#B)
|
||
-- For convenience in iteration:
|
||
insert(common_subsequences, {a=#A+1, b=#B+1, length=0})
|
||
local chunks = setmetatable({}, diff_mt)
|
||
local a, b = 1, 1
|
||
for _,subseq in ipairs(common_subsequences) do
|
||
if subseq.a > a or subseq.b > b then
|
||
insert(chunks, {
|
||
old=concat(A, "", a, subseq.a-1), old_line=a, old_line_end=subseq.a-1,
|
||
new=concat(B, "", b, subseq.b-1), new_line=b, new_line_end=subseq.b-1})
|
||
end
|
||
if subseq.length > 0 then
|
||
local lines = concat(A, "", subseq.a, subseq.a+subseq.length-1)
|
||
insert(chunks, {
|
||
old=lines, old_line=subseq.a, old_line_end=subseq.a+subseq.length-1,
|
||
new=lines, new_line=subseq.b, new_line_end=subseq.b+subseq.length-1})
|
||
end
|
||
a = subseq.a + subseq.length
|
||
b = subseq.b + subseq.length
|
||
end
|
||
return chunks
|
||
end
|
||
return diff
|