-
-
Save williame/2e93260c918e4bc1434e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
versions = [version.strip() for version in ( | |
""" | |
def t(b): | |
L=len;w,n=L(b[0])+3,-1;b=list("@"*w+"@@".join(b)+"@"*w);w+=n;I=b.index | |
for i in range(L(b)): | |
c=b[i];d={"^":-w,"<":n,">":1,"v":w}.get(c) | |
if d: | |
while c!='@': | |
i+=d;c=b[i] | |
if c=='*':b[i]='.' | |
elif c in "123456789":i+=I(c)-i or I(c,i+1)-i | |
elif c in '/\\\\':d={-w:n,w:1,1:w,n:-w}[d]*(-1 if c=='/' else 1) | |
return "*" not in b | |
""", | |
""" | |
def t(b): | |
L=len;w=L(b[0])+3;b=list("@"*w+"@@".join(b)+"@"*w);w-=1;I=b.index | |
for i in range(L(b)): | |
c=b[i];d={"^":-w,"<":-1,">":1,"v":w}.get(c) | |
if d: | |
while c!='@': | |
i+=d;c=b[i] | |
if c=='*':b[i]='.' | |
elif c in "123456789":i+=I(c)-i or I(c,i+1)-i | |
elif c in '/\\\\':d={-w:-1,w:1,1:w,-1:-w}[d]*(-1 if c=='/' else 1) | |
return "*" not in b | |
""", | |
""" | |
def t(b): | |
L=len;w=L(b[0])+3;b=list("@"*w+"@@".join(b)+"@"*w);w-=1;I=b.index | |
for i in range(L(b)): | |
c=b[i];d={"^":-w,"<":-1,">":1,"v":w}.get(c) | |
if d: | |
while c!='@': | |
i+=d;c=b[i] | |
if c=='*':b[i]='.' | |
elif c in '/\\\\':d={-w:-1,w:1,1:w,-1:-w}[d]*(-1 if c=='/' else 1) | |
elif c>'0':i+=I(c)-i or I(c,i+1)-i | |
return "*" not in b | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);C="<>v^";D=[-1,1,w,-w];i=0 | |
while i<l: | |
d=C.find(B[i]) | |
if-1<d: | |
j=i;n=l*4 | |
while('@'!=B[j])*n: | |
c=B[j] | |
if'+'>c:B[j]='' | |
if'/'==c:d=(d+2)%4 | |
if'\\\\'==c:d=3-d | |
if c.isdigit():j=(B*2).index(c,j+1)%l | |
j+=D[d];n-=1 | |
i+=1 | |
return'*'not in B | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);i=0 | |
while i<l: | |
d="<>v^".find(B[i]) | |
if-1<d: | |
j=i;n=l*4 | |
while(B[j]!='@')*n: | |
c=B[j] | |
if'+'>c:B[j]='' | |
if'/'==c:d=(d+2)%4 | |
if'\\\\'==c:d=3-d | |
if c.isdigit():j=(B*2).index(c,j+1)%l | |
j+=[-1,1,w,-w][d];n-=1 | |
i+=1 | |
return'*'not in B | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);i=0 | |
while i<l: | |
d="<>v^".find(B[i]) | |
if-1<d: | |
j=i;c='.' | |
while c not in"@<>v^": | |
if'+'>c:B[j]='' | |
if'/'==c:d^=2 | |
if'\\\\'==c:d^=3 | |
elif'0'<c:j=(B*2).index(c,j+1)%l | |
j+=[-1,1,w,-w][d];c=B[j] | |
i+=1 | |
return'*'not in B | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+2;B=list('@'*w+'@@'.join(b)+'@'*w);l=len(B);i=0 | |
while i<l: | |
d="<>v^".find(B[i]);j=i;c='.' | |
while c not in"@<>v^": | |
if'+'>c:B[j]='' | |
if'/'==c:d^=2 | |
if'\\\\'==c:d^=3 | |
elif'0'<c:j=(B*2).index(c,j+1)%l | |
j+=[-1,1,w,-w,-i][d];c=B[j] | |
i+=1 | |
return'*'not in B | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+1;B=list('@'*w+'@'.join(b)+'@'*w);l=len(B);i=0 | |
while i<l: | |
d="<>v^".find(B[i]);j=i;c='.' | |
while c not in"@<>v^": | |
if'+'>c:B[j]='' | |
if'/'==c:d^=2 | |
if'\\\\'==c:d^=3 | |
elif'0'<c:j=(B*2).index(c,j+1)%l | |
j+=[-1,1,w,-w,-j][d];c=B[j] | |
i+=1 | |
return'*'not in B | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+1;B=list('@'*w+'@'.join(b));i=l=len(B);C="<>^v@" | |
while i: | |
j=l-i;i-=1;d=C.find(B[j]);c='.' | |
while c not in C: | |
if'+'>c:B[j]='.' | |
if'0'<c<C:j=(B*2).index(c,j+1)%l | |
elif'.'<c:d^=2+(c<C) | |
j-=[1,-1,w,-w,j][d];c=B[j%l] | |
return'*'not in B | |
""", | |
""" | |
def t(b): | |
w=len(b[0])+1;B=list('@'*w+'@'.join(b));i=l=len(B);C="<>^v@" | |
while i: | |
i-=1;d,j,c=C.find(B[i]),i,'.' | |
while(c in C)-1: | |
if'+'>c:B[j]='.' | |
if'0'<c<C:j=(B*2).index(c,j+1)%l | |
elif'.'<c:d^=2+(c<C) | |
j-=[1,-1,w,-w,j][d];c=B[j%l] | |
return('*'in B)-1 | |
""")] | |
import sys | |
def ndiff(src,dest): | |
# compute edit distance | |
src_len, dest_len = len(src), len(dest) | |
d = {(-1, -1): -1} | |
for i in xrange(src_len): | |
d[i, -1] = i | |
for j in xrange(dest_len): | |
d[-1, j] = j | |
for i in xrange(src_len): | |
for j in xrange(dest_len): | |
d[i, j] = min( | |
d[i-1, j] + 1, # delete | |
d[i, j-1] + 1, # insert | |
d[i-1, j-1] + (src[i] != dest[j])) # substitute | |
# backtrack to recover cheapest path | |
path = 'S' | |
while i or j: | |
i, j, s = min( | |
(i-1, j, 'D'), | |
(i, j-1, 'I'), | |
(i-1, j-1, 'S'), | |
key = lambda(i, j, s): d.get((i, j), sys.maxint)) | |
path += s | |
# yield in standard ndiff format | |
src_ofs = dest_ofs = 0 | |
for i, s in enumerate(path[::-1]): | |
c, d = src[i + src_ofs], dest[i + dest_ofs] | |
if s == 'S': | |
if c == d: | |
yield ' ', d | |
else: | |
yield '-', c | |
yield '+', d | |
elif s == 'D': | |
dest_ofs -= 1 | |
yield '-', c | |
else: | |
src_ofs -= 1 | |
yield '+', d | |
width = max(len(line) for version in versions for line in version.split("\n")) | |
height = max(len(version.split("\n")) for version in versions) | |
import difflib | |
from PIL import Image, ImageFont, ImageDraw, ImageChops, GifImagePlugin | |
font = ImageFont.truetype("/Library/Fonts/OsakaMono.ttf", 15) | |
em, lineheight = 9, 14 | |
anim_frames = 1 | |
theme = { | |
"identifier": "blue", | |
"keyword": "black", | |
"string": "maroon", | |
"number": "magenta", | |
"punctuation": "gray", | |
"whitespace": None, | |
} | |
def clike_highlighter(src, keywords, theme=theme): | |
import string | |
x = y = i = 0 | |
out = [] | |
def emit(stop, typ): | |
colour = theme.get(typ, "black") | |
# print "(%s \"%s\" %s)" % (typ, src[start:stop].replace("\n","¶"), colour) | |
for c in src[start:stop]: | |
out.append((c, colour)) | |
while i < len(src): | |
start = i | |
ch = src[i] | |
if ch in "\'\"": | |
while True: | |
i += 1 | |
if src[i] == '\\': | |
i += 1 | |
elif src[i] == ch: | |
i += 1 | |
emit(i, "string") | |
break | |
elif ch in string.punctuation: | |
i += 1 | |
emit(i, "punctuation") | |
elif ch in string.whitespace: | |
i += 1 | |
emit(i, "whitespace") | |
elif ch in string.digits: | |
while True: | |
i += 1 | |
if i == len(src) or (src[i] not in string.digits and src[i] not in "e."): | |
emit(i, "number") | |
break | |
else: | |
while True: | |
i += 1 | |
if i == len(src) or src[i] in "\'\"" or src[i] in string.punctuation or src[i] in string.whitespace: | |
emit(i, "keyword" if src[start:i] in keywords else "identifier") | |
break | |
return out | |
from keyword import kwlist as python_keywords | |
versions = [clike_highlighter(version, python_keywords) for version in versions] | |
def render(text, ofs, diff, diff_ch, label): | |
frames = [] | |
for frame in range(anim_frames if ofs is not None else 1): | |
image = Image.new("RGB", (width * em + int(em * 1.5), height * lineheight + int(lineheight/2)), "white") | |
frames.append(image) | |
draw = ImageDraw.Draw(image) | |
x, y = 0, 0 | |
for i, (ch, colour) in enumerate(text): | |
if ch == '\n': | |
x = 0 | |
y += lineheight | |
else: | |
if i == ofs: | |
draw.rectangle((x, y, x + em, y + lineheight), | |
fill='red' if diff=='+' else 'blue') | |
scale = 1.0 / 3 * frame | |
if diff == '-': | |
scale = 1.0 - scale | |
draw.text((x, y), ch, font=font, | |
fill='yellow') | |
draw.rectangle((x + em, y, x + em + em, y + lineheight), | |
fill='white') | |
x += int(round(float(em) * scale)) | |
if colour: | |
draw.text((x, y), ch, font=font, fill=colour) | |
x += em | |
if label: | |
label = str(label) | |
x = width * em - len(label) * em | |
draw.rectangle((x, 0, width * em, lineheight), fill='yellow') | |
draw.text((x, 0), label, font=font, fill='red') | |
del draw | |
return frames | |
def compute_frames(prev, curr): | |
adjust = 0 | |
for ofs, diff in enumerate(ndiff(prev, curr)): | |
diff, ch = diff[0], diff[-1] | |
if diff in "+-": | |
yield prev, ofs-adjust, diff, ch | |
if diff == "+": | |
prev = prev[:ofs-adjust] + [ch] + prev[ofs-adjust:] | |
else: | |
assert prev[ofs-adjust] == ch, (ofs, prev[:ofs+1], ch) | |
prev = prev[:ofs-adjust] + prev[ofs-adjust+1:] | |
adjust += 1 | |
assert prev == curr, ["%d= %s != %s" % (i, prev[i].replace('\n','¶').replace(' ','¢'), | |
curr[i].replace('\n','¶').replace(' ','¢')) for i in range(min(len(prev),len(curr))) if prev[i] != curr[i]] | |
frames = render(versions[0], None, None, None, len(versions[0])) | |
prev = versions[0] | |
for i in range(3, len(versions)): | |
curr = versions[i] | |
print "==== %d (%d %d) ====" % (i, len(curr), len(prev)-len(curr)) | |
for frame in compute_frames(prev, curr): | |
frames += render(*frame, label="%d -> %d" % (len(prev), len(curr))) | |
frames += render(versions[-1], None, None, None, len(versions[-1])) | |
frames = frames[:2000] ### max frames to keep demo small enough to upload to stackoverflow | |
with open("anim.gif", "wb") as f: | |
prev = None | |
for frame in frames: | |
frame = frame.convert('P', palette=Image.WEB) | |
if not prev: | |
for s in GifImagePlugin.getheader(frame)[0]: | |
f.write(s) | |
prev = frame.copy() | |
frame = GifImagePlugin.getdata(frame) | |
else: | |
delta = ImageChops.subtract_modulo(frame, prev) | |
prev = frame.copy() | |
bbox = delta.getbbox() | |
if bbox: | |
frame = GifImagePlugin.getdata(frame.crop(bbox), offset = bbox[:2]) | |
else: | |
continue | |
for s in frame: | |
f.write(s) | |
f.write(";") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment