-module(tdiff_debug). -export([svg_diff/3]). %% @doc Example: %% ``` %% svg_diff("A cat ate my hat", "A dog ate my shoe", "tdiff-trace.svg"). %% ''' %% The resulting svg file works well in Chrome and Opera. %% %% The Sx is (usually) the "old" string. %% The Sy is (usually) the "new" string. %% The blue number in the upper left is the D number. %% Taking a step to the right means deleting that character. %% Taking a step down means inserting that character. %% %% The tdiff algorithm can diff any list of objects, such as lists of %% lines, lists of words or whatever. However, this tracer is only %% good at strings (lists of characters). It might produce funny or %% strange svg files in other cases. %% svg_diff(Sx, Sy, DestFileName) -> Tracer = start_svg_tracer(Sx, Sy, DestFileName), Res = tdiff:diff(Sx, Sy, [{algorithm_tracer, fun(Ev) -> Tracer ! Ev end}]), stop_tracer(Tracer), Res. start_svg_tracer(Sx, Sy, DestFileName) -> proc_lib:spawn(fun() -> init_svg_tracer(Sx, Sy, DestFileName) end). stop_tracer(Pid) -> MRef = erlang:monitor(process, Pid), Pid ! {stop, self()}, receive {Pid, Res} -> erlang:demonitor(MRef, [flush]), Res; {'DOWN', MRef, _, _, Reason} -> erlang:error({terminated,Reason}) end. init_svg_tracer(Sx, Sy, DestFileName) -> FdInfo = open_svg_file(Sx, Sy, DestFileName), loop_svg_tracer(FdInfo, _D=unknown, Sx, Sy). loop_svg_tracer(FdInfo, D, Sx, Sy) -> receive {final_edit_script, EditScript} -> print_final_edit_script(FdInfo, EditScript), loop_svg_tracer(FdInfo, D, Sx, Sy); {exhausted_kdiagonals, _D} -> print_svg_d_closed(FdInfo), loop_svg_tracer(FdInfo, D, Sx, Sy); {d, NewD} -> print_svg_d(FdInfo, NewD), loop_svg_tracer(FdInfo, NewD, Sx, Sy); {dpath, DPath} -> print_svg_dpath(FdInfo, DPath), loop_svg_tracer(FdInfo, D, Sx, Sy); {stop, From} -> print_svg_dmax(FdInfo, D), close_svg_file(FdInfo), From ! {self(),ok} end. -record(fdinfo, {fd, xoffs, yoffs, dx, dy, pathlinestyle, endpointstyle, final_pathlinestyle, final_endpointstyle}). open_svg_file(Sx, Sy, DestFileName) -> SxLen = length(Sx), SyLen = length(Sy), DMax = SxLen + SyLen, Dx = 10, Dy = 10, XOffs = 20, YOffs = 10, {ok,Fd} = file:open(DestFileName, [write]), io:format( Fd, "\n" "\n" " \n" " \n" ,[SxLen*Dx+2*XOffs+10, SyLen*Dy+2*YOffs, DMax]), io:format(Fd, " \n", []), io:format(Fd, " \n" , [round(Dx/4),0, Dy, -round(Dx/2), -round(Dy/2)]), io:format(Fd, " \n" , [SxLen*Dx+2*XOffs-Dx,0, Dy, round(Dx/2), -round(Dy/2)]), io:format(Fd, " \n", []), BoxLineStyle = o([{class,border}]), GridLineStyle = o([{class,grid}]), PathLineStyle = o([{class,path}]), EndPtStyle = o([{class,endpt}]), FinalPathLineStyle = o([{class,fpath}]), FinalEndPtStyle = o([{class,fendpt}]), io:format(Fd, " \n", []), %% Grid lines... lists:foreach(fun(X) -> io:format(Fd, " \n" ,[X*Dx+XOffs, YOffs, SyLen*Dy, GridLineStyle]) end, lists:seq(1,SxLen)), lists:foreach(fun(Y) -> io:format(Fd, " \n" ,[XOffs, Y*Dy+YOffs, SxLen*Dx, GridLineStyle]) end, lists:seq(1,SyLen)), %% Box around io:format( Fd, " \n" ,[XOffs, YOffs, SyLen*Dy, SxLen*Dx, SyLen*Dy, BoxLineStyle]), %% Symbols for each X/Y lists:foreach( fun({X,C}) -> io:format( Fd, " ~c\n", [XOffs+Dx*X+round(Dx/2), YOffs-YOffs/4, C]) end, index0seq(Sx)), lists:foreach( fun({Y,C}) -> io:format( Fd, " ~c\n", [XOffs-Dx/2, YOffs+Dy*(Y+1)-YOffs/4, C]) end, index0seq(Sy)), io:format(Fd, " \n", []), #fdinfo{fd = Fd, xoffs = XOffs, yoffs = YOffs, dx = Dx, dy = Dy, pathlinestyle = PathLineStyle, endpointstyle = EndPtStyle, final_pathlinestyle = FinalPathLineStyle, final_endpointstyle = FinalEndPtStyle}. index0seq([]) -> []; index0seq(L) -> lists:zip(lists:seq(0,length(L)-1), L). print_svg_d(#fdinfo{fd=Fd, xoffs=XOffs,yoffs=YOffs, dx=Dx}, D) -> io:format(Fd, " \n" " ~w\n" " \n" ,[D, if D == 0 -> "visible"; true -> "hidden" end, XOffs-Dx*0.25, YOffs-YOffs/4, D]), io:format(Fd, " \n" ,[D, if D == 0 -> "visible"; true -> "hidden" end]). print_svg_d_closed(#fdinfo{fd=Fd}) -> io:format(Fd, " \n", []). print_svg_dpath(FdInfo, {_X, _Y, _Sx, _Sy, RSteps}=_DPath) -> #fdinfo{pathlinestyle = PathLineStyle, endpointstyle = EndPtStyle} = FdInfo, print_svg_dpath_2(FdInfo, RSteps, PathLineStyle, EndPtStyle). print_svg_dpath_2(FdInfo, RSteps, PathLineStyle, EndPtStyle) -> #fdinfo{fd = Fd, xoffs = XOffs, yoffs = YOffs, dx = Dx, dy = Dy} = FdInfo, io:format(Fd, " %% X,Y seems to be reversed below [1] %% but they are not. This is because of %% how x,y are defined (removing a char %% from Sx inserting a char from Sy, %% respectively). {X1,Y1} = if Dir == e -> {1,1}; Dir == x -> {0,1}; %% [1] here... Dir == y -> {1,0} %% ... and here end, io:format(Fd, " l ~w,~w", [X1*Dx, Y1*Dx]), {X0+X1, Y0+Y1} end, {0,0}, lists:reverse(RSteps)), io:format(Fd, "\"\n ~s\n />\n", [PathLineStyle]), io:format(Fd, " \n" ,[XOffs+X*Dx,YOffs+Y*Dy, Dx/4, EndPtStyle]). print_final_edit_script(FdInfo, EditScript) -> #fdinfo{final_pathlinestyle = PathLineStyle, final_endpointstyle = EndPtStyle} = FdInfo, RSteps1 = lists:append( lists:map(fun({ins,Elems}) -> [{x,E} || E <- Elems]; ({del,Elems}) -> [{y,E} || E <- Elems]; ({eq,Elems}) -> [{e,E} || E <- Elems] end, EditScript)), RSteps2 = lists:reverse(RSteps1), print_svg_dpath_2(FdInfo, RSteps2, PathLineStyle, EndPtStyle). print_svg_dmax(#fdinfo{fd=Fd}, D) -> io:format( Fd, " \n" ,[D]). close_svg_file(#fdinfo{fd=Fd}) -> %% Trailer io:format( Fd, " \n" "\n" ,[]), file:close(Fd), ok. o(L) -> case o2(L) of " "++Rest -> Rest; Os -> Os end. o2(L) -> lists:flatten( lists:map( fun({K,V}) when is_atom(V) -> [" ",a2s(K), "=\"", a2s(V), "\""]; ({K,V}) when is_integer(V) -> [" ",a2s(K), "=\"", i2s(V), "\""]; ({K,V}) when is_list(V) -> [" ",a2s(K), "=\"", V, "\""] end, L)). a2s(A) -> atom_to_list(A). i2s(I) -> integer_to_list(I).