char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
d540258
main
1export interface DiffLine { 2sign :" " | "+" | "-" ; 3text :string ; 4} 5 6export interface Hunk { 7aStart :number ; 8aLines :number ; 9bStart :number ; 10bLines :number ; 11lines :DiffLine []; 12} 13 14/** myers O(ND) shortest edit script over lines, grouped into context hunks */ 15export function diffLines ( aText :string , bText :string , context = 3 ) :Hunk [] { 16const a = splitLines ( aText ); 17const b = splitLines ( bText ); 18const trace = myers ( a , b ); 19return groupHunks ( a , b , trace , context ); 20} 21 22function splitLines ( text :string ) :string [] { 23if ( text === "" ) return []; 24const lines = text . split ( "\n" ); 25if ( lines . at ( - 1 ) === "" ) lines . pop (); 26return lines ; 27} 28 29type Edit = { sign :" " | "+" | "-" ; aLine :number ; bLine :number }; 30 31function myers ( a :string [], b :string []) :Edit [] { 32const max = a . length + b . length ; 33if ( max === 0 ) return []; 34const offset = max ; 35let v = new Array < number >( 2 * max + 1 ). fill ( 0 ); 36const traces :number [][] = []; 37 38 outer:for ( let d = 0 ; d <= max ; d ++ ) { 39traces . push ( v . slice ()); 40for ( let k = - d ; k <= d ; k += 2 ) { 41let x = k === - d || ( k !== d && v [ offset + k - 1 ] < v [ offset + k + 1 ]) 42 ?v [ offset + k + 1 ] 43 :v [ offset + k - 1 ] + 1 ; 44let y = x - k ; 45while ( x < a . length && y < b . length && a [ x ] === b [ y ]) { 46x ++ ; 47y ++ ; 48} 49v [ offset + k ] = x ; 50if ( x >= a . length && y >= b . length ) break outer; 51} 52v = v . slice (); 53} 54 55// backtrack the recorded frontiers into an edit script 56const edits :Edit [] = []; 57let x = a . length ; 58let y = b . length ; 59for ( let d = traces . length - 1 ; x > 0 || y > 0 ; d -- ) { 60const vd = traces [ d ]; 61const k = x - y ; 62const prevK = k === - d || ( k !== d && vd [ offset + k - 1 ] < vd [ offset + k + 1 ]) ?k + 1 :k - 1 ; 63const prevX = vd [ offset + prevK ]; 64const prevY = prevX - prevK ; 65while ( x > prevX && y > prevY ) { 66edits . push ({ sign :" " , aLine :-- x , bLine :-- y }); 67} 68if ( d === 0 ) break ; 69if ( x === prevX ) edits . push ({ sign :"+" , aLine :x , bLine :-- y }); 70else edits . push ({ sign :"-" , aLine :-- x , bLine :y }); 71} 72return edits . reverse (); 73} 74 75function groupHunks ( a :string [], b :string [], edits :Edit [], context :number ) :Hunk [] { 76const hunks :Hunk [] = []; 77let current :Hunk | null = null ; 78let trailingContext = 0 ; 79 80for ( let i = 0 ; i < edits . length ; i ++ ) { 81const edit = edits [ i ]; 82if ( edit . sign === " " ) { 83if ( current ) { 84if ( trailingContext < context ) { 85current . lines . push ({ sign :" " , text :a [ edit . aLine ] }); 86current . aLines ++ ; 87current . bLines ++ ; 88trailingContext ++ ; 89} else { 90// check whether another change follows within 2*context 91const nextChange = edits . slice ( i , i + context + 1 ). findIndex ( e => e . sign !== " " ); 92if ( nextChange === - 1 ) { 93current = null ; 94} else { 95current . lines . push ({ sign :" " , text :a [ edit . aLine ] }); 96current . aLines ++ ; 97current . bLines ++ ; 98} 99} 100} 101continue ; 102} 103 104if ( ! current ) { 105const lead :DiffLine [] = []; 106let aStart = edit . aLine ; 107let bStart = edit . bLine ; 108for ( let c = 1 ; c <= context ; c ++ ) { 109const prev = edits [ i - c ]; 110if ( ! prev || prev . sign !== " " ) break ; 111lead . unshift ({ sign :" " , text :a [ prev . aLine ] }); 112aStart = prev . aLine ; 113bStart = prev . bLine ; 114} 115current = { 116aStart :aStart + 1 , 117bStart :bStart + 1 , 118aLines :lead . length , 119bLines :lead . length , 120lines :lead , 121}; 122hunks . push ( current ); 123} 124trailingContext = 0 ; 125if ( edit . sign === "-" ) { 126current . lines . push ({ sign :"-" , text :a [ edit . aLine ] }); 127current . aLines ++ ; 128} else { 129current . lines . push ({ sign :"+" , text :b [ edit . bLine ] }); 130current . bLines ++ ; 131} 132} 133 134return hunks ; 135}