1+ const dmp = new diff_match_patch ( ) ;
2+ let diffResults = [ ] ;
3+ let currentDiffIndex = 0 ;
4+ let versionAContent = '' ;
5+ let versionBContent = '' ;
6+
7+ // Store the full diff list for accurate navigation and rendering
8+ let fullDiffsForNavigation = [ ] ;
9+
10+ // --- Debounce Helper Function ---
11+ function debounce ( func , delay ) {
12+ let timeoutId ;
13+ return function ( ...args ) {
14+ clearTimeout ( timeoutId ) ;
15+ timeoutId = setTimeout ( ( ) => {
16+ func . apply ( this , args ) ;
17+ } , delay ) ;
18+ } ;
19+ }
20+
21+ // --- Line Number and Scroll Helpers ---
22+
23+ function calculateLineAndCol ( text , index ) {
24+ // Splits the text up to the index to find the line breaks
25+ const lines = text . substring ( 0 , index ) . split ( '\n' ) ;
26+ const line = lines . length ; // The line number is the count of lines + 1
27+ const col = lines [ lines . length - 1 ] . length + 1 ; // Column is characters on the last line segment
28+ return { line, col } ;
29+ }
30+
31+ function updateLineNumbers ( lineNumbersDivId , content ) {
32+ const lineNumbersDiv = document . getElementById ( lineNumbersDivId ) ;
33+ const lines = content . split ( '\n' ) ;
34+ let numbers = '' ;
35+
36+ const lineCount = ( lines . length === 1 && lines [ 0 ] . length === 0 ) ? 1 : lines . length ;
37+
38+ for ( let i = 1 ; i <= lineCount ; i ++ ) {
39+ numbers += i + '<br>' ;
40+ }
41+ lineNumbersDiv . innerHTML = numbers ;
42+ }
43+
44+ // Line Numbers for Input Textareas
45+ function updateInputLineNumbers ( ) {
46+ const contentA = document . getElementById ( 'versionAInput' ) . value ;
47+ const contentB = document . getElementById ( 'versionBInput' ) . value ;
48+
49+ updateLineNumbers ( 'inputLineNumbersA' , contentA ) ;
50+ updateLineNumbers ( 'inputLineNumbersB' , contentB ) ;
51+ }
52+
53+ // Sync scrolling for the display editors (Code Editors)
54+ function syncScrollDisplay ( sourceId , targetId ) {
55+ const source = document . getElementById ( sourceId ) ;
56+ const target = document . getElementById ( targetId ) ;
57+
58+ target . scrollTop = source . scrollTop ;
59+
60+ document . getElementById ( 'lineNumbersA' ) . scrollTop = source . scrollTop ;
61+ document . getElementById ( 'lineNumbersB' ) . scrollTop = source . scrollTop ;
62+ }
63+
64+ // Sync scrolling for the input textareas and their line number panels
65+ function syncScrollInput ( sourceInputId , targetNumbersId ) {
66+ const source = document . getElementById ( sourceInputId ) ;
67+ const target = document . getElementById ( targetNumbersId ) ;
68+
69+ target . scrollTop = source . scrollTop ;
70+ }
71+
72+ // --- Rendering and Diff Functions ---
73+
74+ function clearActiveDiffHighlight ( ) {
75+ document . querySelectorAll ( '.active-diff' ) . forEach ( el => {
76+ el . classList . remove ( 'active-diff' ) ;
77+ } ) ;
78+ }
79+
80+ function renderDiff ( editorId , diffs , isVersionA ) {
81+ const editorDiv = document . getElementById ( editorId ) ;
82+ let html = '' ;
83+ let contentForLineNumbers = '' ;
84+
85+ for ( const diff of diffs ) {
86+ const op = diff [ 0 ] ;
87+ const text = diff [ 1 ] ;
88+
89+ let spanClass = '' ;
90+ let showText = true ;
91+
92+ if ( op === 1 ) {
93+ spanClass = 'added' ;
94+ if ( isVersionA ) showText = false ;
95+ } else if ( op === - 1 ) {
96+ spanClass = 'removed' ;
97+ if ( ! isVersionA ) showText = false ;
98+ }
99+
100+ // Replace spaces with non-breaking spaces and escape HTML
101+ const formattedText = text . replace ( / / g, '\u00a0' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
102+
103+ // Wrap text in an inline-block span for correct highlight application
104+ html += `<span class="${ spanClass } ">${ showText ? formattedText : '' } </span>` ;
105+ if ( showText ) {
106+ contentForLineNumbers += text ;
107+ }
108+ }
109+
110+ editorDiv . innerHTML = html ;
111+
112+ return contentForLineNumbers ;
113+ }
114+
115+ let lastDiffCount = 0 ;
116+
117+ function runDiff ( event ) {
118+ versionAContent = document . getElementById ( 'versionAInput' ) . value ;
119+ versionBContent = document . getElementById ( 'versionBInput' ) . value ;
120+
121+ if ( ! versionAContent && ! versionBContent ) {
122+ document . getElementById ( 'percentageDisplay' ) . textContent = `0.00%` ;
123+ document . getElementById ( 'lineDisplay' ) . textContent = '-' ;
124+ document . getElementById ( 'colDisplay' ) . textContent = '-' ;
125+ document . getElementById ( 'codeEditorA' ) . innerHTML = '' ;
126+ document . getElementById ( 'codeEditorB' ) . innerHTML = '' ;
127+ updateLineNumbers ( 'lineNumbersA' , '' ) ;
128+ updateLineNumbers ( 'lineNumbersB' , '' ) ;
129+ lastDiffCount = 0 ;
130+ clearActiveDiffHighlight ( ) ;
131+ return ;
132+ }
133+
134+ const diffs = dmp . diff_main ( versionAContent , versionBContent ) ;
135+ dmp . diff_cleanupSemantic ( diffs ) ;
136+
137+ fullDiffsForNavigation = diffs ;
138+
139+ const newDiffResults = diffs . filter ( d => d [ 0 ] !== 0 ) ;
140+
141+ const diffsChanged = newDiffResults . length !== lastDiffCount ;
142+ lastDiffCount = newDiffResults . length ;
143+
144+ diffResults = newDiffResults ;
145+
146+ const contentA = renderDiff ( 'codeEditorA' , fullDiffsForNavigation , true ) ;
147+ const contentB = renderDiff ( 'codeEditorB' , fullDiffsForNavigation , false ) ;
148+
149+ updateLineNumbers ( 'lineNumbersA' , contentA ) ;
150+ updateLineNumbers ( 'lineNumbersB' , contentB ) ;
151+
152+ updateStats ( fullDiffsForNavigation , versionAContent . length ) ;
153+
154+ if ( diffResults . length > 0 ) {
155+ if ( diffsChanged ) {
156+ currentDiffIndex = 0 ;
157+ }
158+ // Always call navigateDiff to re-apply the highlight and scroll
159+ navigateDiff ( 0 , true ) ;
160+ } else {
161+ clearActiveDiffHighlight ( ) ;
162+ }
163+ }
164+
165+
166+ function updateStats ( diffs , totalLengthA ) {
167+ let equalChars = 0 ;
168+ let firstChangeIndexA = - 1 ; // Character index in Version A where the first change occurs
169+ let currentCharIndexA = 0 ;
170+
171+ for ( const diff of diffs ) {
172+ const op = diff [ 0 ] ;
173+ const text = diff [ 1 ] ;
174+
175+ if ( op === 0 ) {
176+ equalChars += text . length ;
177+ currentCharIndexA += text . length ;
178+ } else if ( op === - 1 ) {
179+ if ( firstChangeIndexA === - 1 ) {
180+ firstChangeIndexA = currentCharIndexA ;
181+ }
182+ currentCharIndexA += text . length ;
183+ } else if ( op === 1 ) {
184+ if ( firstChangeIndexA === - 1 ) {
185+ firstChangeIndexA = currentCharIndexA ;
186+ }
187+ }
188+ }
189+
190+ const totalChars = versionAContent . length + versionBContent . length ;
191+ const totalDiffLength = totalChars - ( 2 * equalChars ) ;
192+ const percentage = totalChars > 0 ? ( totalDiffLength / totalChars ) * 100 : 0 ;
193+
194+ document . getElementById ( 'percentageDisplay' ) . textContent = `${ percentage . toFixed ( 2 ) } %` ;
195+
196+ if ( firstChangeIndexA !== - 1 ) {
197+ // Correctly calculate line and column based on the full content string
198+ const { line, col } = calculateLineAndCol ( versionAContent , firstChangeIndexA ) ;
199+ document . getElementById ( 'lineDisplay' ) . textContent = line ;
200+ document . getElementById ( 'colDisplay' ) . textContent = col ;
201+ } else {
202+ document . getElementById ( 'lineDisplay' ) . textContent = '-' ;
203+ document . getElementById ( 'colDisplay' ) . textContent = '-' ;
204+ }
205+ }
206+
207+
208+ // FUNCTIONAL NAVIGATION: Scrolls the display panels to the change and applies purple highlight.
209+ function navigateDiff ( indexOffset ) {
210+ if ( diffResults . length === 0 ) return ;
211+
212+ // Update index based on offset, with loop-around logic
213+ currentDiffIndex += indexOffset ;
214+ if ( currentDiffIndex < 0 ) {
215+ currentDiffIndex = diffResults . length - 1 ;
216+ } else if ( currentDiffIndex >= diffResults . length ) {
217+ currentDiffIndex = 0 ;
218+ }
219+
220+ const targetChange = diffResults [ currentDiffIndex ] ;
221+
222+ let charIndexA = 0 ;
223+
224+ // Find the character index corresponding to the start of the current change
225+ for ( const diff of fullDiffsForNavigation ) {
226+ if ( diff === targetChange ) {
227+ break ;
228+ }
229+
230+ if ( diff [ 0 ] === 0 || diff [ 0 ] === - 1 ) {
231+ charIndexA += diff [ 1 ] . length ;
232+ }
233+ }
234+
235+ // --- Scrolling ---
236+ const { line } = calculateLineAndCol ( versionAContent , charIndexA ) ;
237+ const editorA = document . getElementById ( 'codeEditorA' ) ;
238+ const editorB = document . getElementById ( 'codeEditorB' ) ;
239+ const lineHeight = 18 ;
240+ const scrollPosition = ( line - 1 ) * lineHeight ;
241+
242+ editorA . scrollTop = scrollPosition ;
243+ editorB . scrollTop = scrollPosition ;
244+ document . getElementById ( 'lineNumbersA' ) . scrollTop = scrollPosition ;
245+ document . getElementById ( 'lineNumbersB' ) . scrollTop = scrollPosition ;
246+
247+ // --- Highlighting ---
248+ clearActiveDiffHighlight ( ) ;
249+
250+ const spansA = editorA . querySelectorAll ( 'span' ) ;
251+ const spansB = editorB . querySelectorAll ( 'span' ) ;
252+
253+ let currentSpanIndex = 0 ;
254+
255+ for ( let i = 0 ; i < fullDiffsForNavigation . length ; i ++ ) {
256+ const diff = fullDiffsForNavigation [ i ] ;
257+
258+ if ( diff === targetChange ) {
259+ // Apply the active highlight (purple) to the span in the respective editor
260+ // Check span in version A (removed diffs)
261+ if ( diff [ 0 ] === - 1 || diff [ 0 ] === 0 ) {
262+ if ( spansA [ currentSpanIndex ] ) spansA [ currentSpanIndex ] . classList . add ( 'active-diff' ) ;
263+ }
264+ // Check span in version B (added diffs)
265+ if ( diff [ 0 ] === 1 || diff [ 0 ] === 0 ) {
266+ if ( spansB [ currentSpanIndex ] ) spansB [ currentSpanIndex ] . classList . add ( 'active-diff' ) ;
267+ }
268+ break ;
269+ }
270+
271+ // The span index always increments for every diff block
272+ currentSpanIndex ++ ;
273+ }
274+
275+ // Update status bar for navigation
276+ document . getElementById ( 'lineDisplay' ) . textContent = line ;
277+ document . getElementById ( 'colDisplay' ) . textContent = calculateLineAndCol ( versionAContent , charIndexA ) . col ;
278+ }
279+
280+
281+ function clearEditor ( version ) {
282+ const inputId = ( version === 'A' ) ? 'versionAInput' : 'versionBInput' ;
283+
284+ document . getElementById ( inputId ) . value = '' ;
285+
286+ runDiff ( null ) ;
287+ }
288+
289+ // Apply the debounce wrapper to runDiff
290+ const debouncedRunDiff = debounce ( runDiff , 300 ) ;
291+
292+ // --- Event Listeners ---
293+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
294+
295+ document . getElementById ( 'nextBtn' ) . addEventListener ( 'click' , ( ) => navigateDiff ( 1 ) ) ;
296+ document . getElementById ( 'prevBtn' ) . addEventListener ( 'click' , ( ) => navigateDiff ( - 1 ) ) ;
297+
298+ // Input listeners: Update line numbers immediately, debounce the heavy diff calculation
299+ document . getElementById ( 'versionAInput' ) . addEventListener ( 'input' , ( ) => {
300+ updateInputLineNumbers ( ) ;
301+ debouncedRunDiff ( ) ;
302+ } ) ;
303+ document . getElementById ( 'versionBInput' ) . addEventListener ( 'input' , ( ) => {
304+ updateInputLineNumbers ( ) ;
305+ debouncedRunDiff ( ) ;
306+ } ) ;
307+
308+ // Listen to scroll events on the resizable textareas to sync line numbers
309+ document . getElementById ( 'versionAInput' ) . addEventListener ( 'scroll' , ( ) => syncScrollInput ( 'versionAInput' , 'inputLineNumbersA' ) ) ;
310+ document . getElementById ( 'versionBInput' ) . addEventListener ( 'scroll' , ( ) => syncScrollInput ( 'versionBInput' , 'inputLineNumbersB' ) ) ;
311+
312+ document . getElementById ( 'clearA' ) . addEventListener ( 'click' , ( ) => clearEditor ( 'A' ) ) ;
313+ document . getElementById ( 'clearB' ) . addEventListener ( 'click' , ( ) => clearEditor ( 'B' ) ) ;
314+
315+ // Initial run on load
316+ runDiff ( null ) ;
317+ } ) ;
0 commit comments