Created
December 18, 2025 06:40
-
-
Save eliascotto/30589e023bf65fe42cfa889e3dcacf67 to your computer and use it in GitHub Desktop.
2/3 files diff viewver
This file contains hidden or 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
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Diff Viewer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jsdiff/5.1.0/diff.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| added: { light: '#d4edda', dark: '#1e3a1e' }, | |
| removed: { light: '#f8d7da', dark: '#3a1e1e' }, | |
| modified: { light: '#fff3cd', dark: '#3a3a1e' }, | |
| padding: { light: '#e9ecef', dark: '#2d2d2d' } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { margin: 0; padding: 0; } | |
| .resizer { | |
| width: 4px; | |
| background: #ccc; | |
| cursor: col-resize; | |
| flex-shrink: 0; | |
| } | |
| .dark .resizer { background: #444; } | |
| .resizer:hover { background: #999; } | |
| .dark .resizer:hover { background: #666; } | |
| .line-added { background-color: #d4edda; } | |
| .dark .line-added { background-color: #1e3a1e; color: #fff; } | |
| .line-removed { background-color: #f8d7da; } | |
| .dark .line-removed { background-color: #3a1e1e; color: #fff; } | |
| .line-modified { background-color: #fff3cd; } | |
| .dark .line-modified { background-color: #3a3a1e; color: #fff; } | |
| .line-padding { background-color: #e9ecef; } | |
| .dark .line-padding { background-color: #2d2d2d; color: #fff; } | |
| .dark .diff-line { color: #fff; } | |
| .char-added { background-color: #28a745; color: #fff; } | |
| .dark .char-added { background-color: #2ea043; } | |
| .char-removed { background-color: #dc3545; color: #fff; } | |
| .dark .char-removed { background-color: #d73a49; } | |
| .diff-content { | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| .line-number { | |
| min-width: 40px; | |
| text-align: right; | |
| padding-right: 8px; | |
| color: #999; | |
| user-select: none; | |
| flex-shrink: 0; | |
| } | |
| .line-content { | |
| flex: 1; | |
| padding-left: 8px; | |
| } | |
| .diff-line { | |
| display: flex; | |
| min-height: 1.4em; | |
| } | |
| .diff-line.highlight { | |
| outline: 2px solid #007bff; | |
| outline-offset: -2px; | |
| } | |
| .dark .diff-line.highlight { | |
| outline-color: #4da3ff; | |
| } | |
| textarea { | |
| resize: none; | |
| border: none; | |
| outline: none; | |
| } | |
| .pane-container { | |
| display: flex; | |
| flex-direction: column; | |
| min-width: 100px; | |
| overflow: hidden; | |
| } | |
| .scroll-container { | |
| overflow: auto; | |
| flex: 1; | |
| } | |
| </style> | |
| </head> | |
| <body class="h-screen flex flex-col"> | |
| <header class="flex items-center justify-between px-3 py-2 border-b border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 flex-shrink-0"> | |
| <div class="flex items-center gap-3"> | |
| <h1 class="text-sm font-medium text-gray-800 dark:text-gray-200">Diff Viewer</h1> | |
| <div class="flex items-center gap-1"> | |
| <button id="togglePane2" class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"> | |
| Center | |
| </button> | |
| <button id="togglePane1" class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"> | |
| Right | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <div class="flex items-center gap-1"> | |
| <button id="prevDiff" class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"> | |
| Prev | |
| </button> | |
| <span id="diffCounter" class="text-xs text-gray-600 dark:text-gray-400 min-w-[60px] text-center">0/0</span> | |
| <button id="nextDiff" class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"> | |
| Next | |
| </button> | |
| </div> | |
| <label class="flex items-center gap-1 text-xs text-gray-700 dark:text-gray-300"> | |
| <input type="checkbox" id="syncScroll" checked class="w-3 h-3"> | |
| Sync scroll | |
| </label> | |
| <button id="compareBtn" class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 bg-blue-500 text-white hover:bg-blue-600"> | |
| Compare | |
| </button> | |
| <button id="editBtn" class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hidden"> | |
| Edit | |
| </button> | |
| </div> | |
| </header> | |
| <main id="panesContainer" class="flex flex-1 overflow-hidden bg-gray-50 dark:bg-gray-800"> | |
| <div id="pane0" class="pane-container flex-1"> | |
| <div class="px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 border-b border-gray-300 dark:border-gray-600"> | |
| Base | |
| </div> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <textarea id="input0" class="flex-1 p-2 text-sm font-mono bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200" placeholder="Paste base content here..."></textarea> | |
| <div id="diff0" class="scroll-container hidden bg-white dark:bg-gray-900"></div> | |
| </div> | |
| </div> | |
| <div class="resizer" data-resizer="0"></div> | |
| <div id="pane1" class="pane-container flex-1"> | |
| <div class="px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 border-b border-gray-300 dark:border-gray-600"> | |
| Center | |
| </div> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <textarea id="input1" class="flex-1 p-2 text-sm font-mono bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200" placeholder="Paste content to compare..."></textarea> | |
| <div id="diff1" class="scroll-container hidden bg-white dark:bg-gray-900"></div> | |
| </div> | |
| </div> | |
| <div class="resizer" data-resizer="1"></div> | |
| <div id="pane2" class="pane-container flex-1"> | |
| <div class="px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 border-b border-gray-300 dark:border-gray-600"> | |
| Right | |
| </div> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <textarea id="input2" class="flex-1 p-2 text-sm font-mono bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200" placeholder="Paste content to compare..."></textarea> | |
| <div id="diff2" class="scroll-container hidden bg-white dark:bg-gray-900"></div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| const state = { | |
| panes: [true, true, true], | |
| syncScroll: true, | |
| currentDiffIndex: -1, | |
| diffPositions: [], | |
| contents: ['', '', ''] | |
| }; | |
| const inputs = [ | |
| document.getElementById('input0'), | |
| document.getElementById('input1'), | |
| document.getElementById('input2') | |
| ]; | |
| const diffs = [ | |
| document.getElementById('diff0'), | |
| document.getElementById('diff1'), | |
| document.getElementById('diff2') | |
| ]; | |
| const panes = [ | |
| document.getElementById('pane0'), | |
| document.getElementById('pane1'), | |
| document.getElementById('pane2') | |
| ]; | |
| // Dark mode detection | |
| function updateDarkMode() { | |
| if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| document.documentElement.classList.add('dark'); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| } | |
| } | |
| updateDarkMode(); | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateDarkMode); | |
| // Resizer functionality | |
| const resizers = document.querySelectorAll('.resizer'); | |
| resizers.forEach(resizer => { | |
| let startX, startWidths, paneIndices; | |
| resizer.addEventListener('mousedown', (e) => { | |
| e.preventDefault(); | |
| startX = e.clientX; | |
| const resizerIndex = parseInt(resizer.dataset.resizer); | |
| const visiblePanes = panes.filter((_, i) => state.panes[i]); | |
| const visibleIndices = state.panes.map((v, i) => v ? i : -1).filter(i => i >= 0); | |
| let leftPaneIdx = -1, rightPaneIdx = -1; | |
| if (resizerIndex === 0) { | |
| if (state.panes[0] && state.panes[1]) { | |
| leftPaneIdx = 0; rightPaneIdx = 1; | |
| } else if (state.panes[0] && state.panes[2]) { | |
| leftPaneIdx = 0; rightPaneIdx = 2; | |
| } | |
| } else { | |
| if (state.panes[1] && state.panes[2]) { | |
| leftPaneIdx = 1; rightPaneIdx = 2; | |
| } else if (state.panes[0] && state.panes[2] && !state.panes[1]) { | |
| leftPaneIdx = 0; rightPaneIdx = 2; | |
| } | |
| } | |
| if (leftPaneIdx === -1 || rightPaneIdx === -1) return; | |
| paneIndices = [leftPaneIdx, rightPaneIdx]; | |
| startWidths = [panes[leftPaneIdx].offsetWidth, panes[rightPaneIdx].offsetWidth]; | |
| document.addEventListener('mousemove', onMouseMove); | |
| document.addEventListener('mouseup', onMouseUp); | |
| }); | |
| function onMouseMove(e) { | |
| const dx = e.clientX - startX; | |
| const newLeftWidth = startWidths[0] + dx; | |
| const newRightWidth = startWidths[1] - dx; | |
| if (newLeftWidth > 100 && newRightWidth > 100) { | |
| panes[paneIndices[0]].style.flex = 'none'; | |
| panes[paneIndices[1]].style.flex = 'none'; | |
| panes[paneIndices[0]].style.width = newLeftWidth + 'px'; | |
| panes[paneIndices[1]].style.width = newRightWidth + 'px'; | |
| } | |
| } | |
| function onMouseUp() { | |
| document.removeEventListener('mousemove', onMouseMove); | |
| document.removeEventListener('mouseup', onMouseUp); | |
| } | |
| }); | |
| // Toggle panes | |
| document.getElementById('togglePane1').addEventListener('click', () => togglePane(2)); | |
| document.getElementById('togglePane2').addEventListener('click', () => togglePane(1)); | |
| function togglePane(index) { | |
| const visibleCount = state.panes.filter(Boolean).length; | |
| if (state.panes[index] && visibleCount <= 2) return; | |
| state.panes[index] = !state.panes[index]; | |
| updatePaneVisibility(); | |
| computeDiffs(); | |
| } | |
| function updatePaneVisibility() { | |
| panes.forEach((pane, i) => { | |
| pane.style.display = state.panes[i] ? 'flex' : 'none'; | |
| }); | |
| resizers[0].style.display = (state.panes[0] && (state.panes[1] || state.panes[2])) ? 'block' : 'none'; | |
| resizers[1].style.display = (state.panes[1] && state.panes[2]) ? 'block' : 'none'; | |
| document.getElementById('togglePane2').textContent = state.panes[1] ? 'Hide Center' : 'Show Center'; | |
| document.getElementById('togglePane1').textContent = state.panes[2] ? 'Hide Right' : 'Show Right'; | |
| panes.forEach((pane, i) => { | |
| if (state.panes[i]) { | |
| pane.style.flex = '1'; | |
| pane.style.width = 'auto'; | |
| } | |
| }); | |
| } | |
| // Sync scroll | |
| document.getElementById('syncScroll').addEventListener('change', (e) => { | |
| state.syncScroll = e.target.checked; | |
| }); | |
| function setupSyncScroll() { | |
| diffs.forEach((diff, i) => { | |
| diff.addEventListener('scroll', () => { | |
| if (!state.syncScroll) return; | |
| diffs.forEach((otherDiff, j) => { | |
| if (i !== j && state.panes[j]) { | |
| otherDiff.scrollTop = diff.scrollTop; | |
| otherDiff.scrollLeft = diff.scrollLeft; | |
| } | |
| }); | |
| }); | |
| }); | |
| } | |
| setupSyncScroll(); | |
| // Diff navigation | |
| document.getElementById('prevDiff').addEventListener('click', () => navigateDiff(-1)); | |
| document.getElementById('nextDiff').addEventListener('click', () => navigateDiff(1)); | |
| function navigateDiff(direction) { | |
| if (state.diffPositions.length === 0) return; | |
| state.currentDiffIndex += direction; | |
| if (state.currentDiffIndex < 0) state.currentDiffIndex = state.diffPositions.length - 1; | |
| if (state.currentDiffIndex >= state.diffPositions.length) state.currentDiffIndex = 0; | |
| highlightCurrentDiff(); | |
| updateDiffCounter(); | |
| } | |
| function highlightCurrentDiff() { | |
| document.querySelectorAll('.diff-line.highlight').forEach(el => el.classList.remove('highlight')); | |
| if (state.currentDiffIndex >= 0 && state.currentDiffIndex < state.diffPositions.length) { | |
| const lineIndex = state.diffPositions[state.currentDiffIndex]; | |
| diffs.forEach((diff, i) => { | |
| if (state.panes[i]) { | |
| const line = diff.querySelector(`[data-line="${lineIndex}"]`); | |
| if (line) { | |
| line.classList.add('highlight'); | |
| line.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| function updateDiffCounter() { | |
| const counter = document.getElementById('diffCounter'); | |
| if (state.diffPositions.length === 0) { | |
| counter.textContent = '0/0'; | |
| } else { | |
| counter.textContent = `${state.currentDiffIndex + 1}/${state.diffPositions.length}`; | |
| } | |
| } | |
| // Compare button | |
| document.getElementById('compareBtn').addEventListener('click', () => { | |
| inputs.forEach((input, i) => { | |
| state.contents[i] = input.value; | |
| }); | |
| const filledPanes = state.panes.filter((visible, i) => visible && state.contents[i].length > 0); | |
| if (filledPanes.length < 2) { | |
| alert('At least 2 panes must have content to compare.'); | |
| return; | |
| } | |
| computeDiffs(); | |
| document.getElementById('compareBtn').classList.add('hidden'); | |
| document.getElementById('editBtn').classList.remove('hidden'); | |
| }); | |
| // Edit button | |
| document.getElementById('editBtn').addEventListener('click', () => { | |
| exitCompareMode(); | |
| document.getElementById('editBtn').classList.add('hidden'); | |
| document.getElementById('compareBtn').classList.remove('hidden'); | |
| }); | |
| function exitCompareMode() { | |
| state.panes.forEach((visible, i) => { | |
| if (visible) { | |
| inputs[i].classList.remove('hidden'); | |
| inputs[i].value = state.contents[i]; | |
| diffs[i].classList.add('hidden'); | |
| } | |
| }); | |
| state.diffPositions = []; | |
| state.currentDiffIndex = -1; | |
| updateDiffCounter(); | |
| } | |
| function computeDiffs() { | |
| const base = state.contents[0]; | |
| const hasContent = state.contents.some(c => c.length > 0); | |
| if (!hasContent) { | |
| inputs.forEach((input, i) => { | |
| input.classList.remove('hidden'); | |
| diffs[i].classList.add('hidden'); | |
| }); | |
| state.diffPositions = []; | |
| state.currentDiffIndex = -1; | |
| updateDiffCounter(); | |
| return; | |
| } | |
| const baseLines = base.split('\n'); | |
| const alignedResults = computeAlignedDiffs(baseLines, state.contents, state.panes); | |
| state.diffPositions = []; | |
| diffs.forEach((diff, paneIndex) => { | |
| if (!state.panes[paneIndex]) return; | |
| inputs[paneIndex].classList.add('hidden'); | |
| diff.classList.remove('hidden'); | |
| const lines = alignedResults[paneIndex]; | |
| let html = '<div class="diff-content">'; | |
| lines.forEach((line, idx) => { | |
| let className = 'diff-line'; | |
| if (line.type === 'added') className += ' line-added'; | |
| else if (line.type === 'removed') className += ' line-removed'; | |
| else if (line.type === 'modified') className += ' line-modified'; | |
| else if (line.type === 'padding') className += ' line-padding'; | |
| if (line.type !== 'unchanged' && line.type !== 'padding') { | |
| if (!state.diffPositions.includes(idx)) { | |
| state.diffPositions.push(idx); | |
| } | |
| } | |
| const lineNum = line.lineNum !== null ? line.lineNum : ''; | |
| const content = line.html || escapeHtml(line.content) || ' '; | |
| html += `<div class="${className}" data-line="${idx}">`; | |
| html += `<span class="line-number">${lineNum}</span>`; | |
| html += `<span class="line-content">${content}</span>`; | |
| html += '</div>'; | |
| }); | |
| html += '</div>'; | |
| diff.innerHTML = html; | |
| }); | |
| state.diffPositions.sort((a, b) => a - b); | |
| state.currentDiffIndex = state.diffPositions.length > 0 ? 0 : -1; | |
| updateDiffCounter(); | |
| } | |
| function computeAlignedDiffs(baseLines, contents, visiblePanes) { | |
| const aligned = [[], [], []]; | |
| const compareLines = [ | |
| baseLines, | |
| contents[1].split('\n'), | |
| contents[2].split('\n') | |
| ]; | |
| // For each comparison pane, compute line-level diff against base | |
| const lineDiffs = [null, null, null]; | |
| visiblePanes.forEach((visible, paneIdx) => { | |
| if (!visible || paneIdx === 0) return; | |
| lineDiffs[paneIdx] = Diff.diffArrays(baseLines, compareLines[paneIdx]); | |
| }); | |
| // Build aligned view for each pane | |
| visiblePanes.forEach((visible, paneIdx) => { | |
| if (!visible || paneIdx === 0) return; | |
| const diff = lineDiffs[paneIdx]; | |
| if (!diff) return; | |
| let alignIdx = 0; | |
| let baseLineNum = 1; | |
| let compareLineNum = 1; | |
| // Process diff parts, looking ahead for removed+added pairs | |
| for (let partIdx = 0; partIdx < diff.length; partIdx++) { | |
| const part = diff[partIdx]; | |
| const nextPart = diff[partIdx + 1]; | |
| if (part.removed && nextPart && nextPart.added) { | |
| // This is a modification: removed lines followed by added lines | |
| const removedLines = part.value; | |
| const addedLines = nextPart.value; | |
| const maxPairs = Math.max(removedLines.length, addedLines.length); | |
| for (let i = 0; i < maxPairs; i++) { | |
| const baseLine = removedLines[i]; | |
| const compareLine = addedLines[i]; | |
| if (baseLine !== undefined && compareLine !== undefined) { | |
| // Both exist - compute char diff | |
| const charDiff = Diff.diffWords(baseLine, compareLine); | |
| let baseHtml = ''; | |
| let compareHtml = ''; | |
| charDiff.forEach(p => { | |
| const escaped = escapeHtml(p.value); | |
| if (p.added) { | |
| compareHtml += `<span class="char-added">${escaped}</span>`; | |
| } else if (p.removed) { | |
| baseHtml += `<span class="char-removed">${escaped}</span>`; | |
| } else { | |
| baseHtml += escaped; | |
| compareHtml += escaped; | |
| } | |
| }); | |
| aligned[0][alignIdx] = aligned[0][alignIdx] || { | |
| content: baseLine, | |
| lineNum: baseLineNum++, | |
| type: 'modified', | |
| html: baseHtml || ' ' | |
| }; | |
| aligned[paneIdx][alignIdx] = { | |
| content: compareLine, | |
| lineNum: compareLineNum++, | |
| type: 'modified', | |
| html: compareHtml || ' ' | |
| }; | |
| } else if (baseLine !== undefined) { | |
| // Only base line exists - pure removal | |
| aligned[0][alignIdx] = aligned[0][alignIdx] || { | |
| content: baseLine, | |
| lineNum: baseLineNum++, | |
| type: 'removed', | |
| html: `<span class="char-removed">${escapeHtml(baseLine)}</span>` | |
| }; | |
| aligned[paneIdx][alignIdx] = { content: '', lineNum: null, type: 'padding' }; | |
| } else { | |
| // Only compare line exists - pure addition | |
| if (!aligned[0][alignIdx]) { | |
| aligned[0][alignIdx] = { content: '', lineNum: null, type: 'padding' }; | |
| } | |
| aligned[paneIdx][alignIdx] = { | |
| content: compareLine, | |
| lineNum: compareLineNum++, | |
| type: 'added', | |
| html: `<span class="char-added">${escapeHtml(compareLine)}</span>` | |
| }; | |
| } | |
| alignIdx++; | |
| } | |
| partIdx++; // Skip the next part since we processed it | |
| } else if (part.removed) { | |
| // Pure removal (no following addition) | |
| part.value.forEach(line => { | |
| aligned[0][alignIdx] = aligned[0][alignIdx] || { | |
| content: line, | |
| lineNum: baseLineNum++, | |
| type: 'removed', | |
| html: `<span class="char-removed">${escapeHtml(line)}</span>` | |
| }; | |
| aligned[paneIdx][alignIdx] = { content: '', lineNum: null, type: 'padding' }; | |
| alignIdx++; | |
| }); | |
| } else if (part.added) { | |
| // Pure addition (no preceding removal) | |
| part.value.forEach(line => { | |
| if (!aligned[0][alignIdx]) { | |
| aligned[0][alignIdx] = { content: '', lineNum: null, type: 'padding' }; | |
| } | |
| aligned[paneIdx][alignIdx] = { | |
| content: line, | |
| lineNum: compareLineNum++, | |
| type: 'added', | |
| html: `<span class="char-added">${escapeHtml(line)}</span>` | |
| }; | |
| alignIdx++; | |
| }); | |
| } else { | |
| // Unchanged | |
| part.value.forEach(line => { | |
| aligned[0][alignIdx] = aligned[0][alignIdx] || { | |
| content: line, | |
| lineNum: baseLineNum++, | |
| type: 'unchanged' | |
| }; | |
| aligned[paneIdx][alignIdx] = { | |
| content: line, | |
| lineNum: compareLineNum++, | |
| type: 'unchanged' | |
| }; | |
| alignIdx++; | |
| }); | |
| } | |
| } | |
| }); | |
| // Fill gaps | |
| const totalLines = Math.max(aligned[0].length, aligned[1].length, aligned[2].length); | |
| for (let i = 0; i < totalLines; i++) { | |
| if (!aligned[0][i]) aligned[0][i] = { content: '', lineNum: null, type: 'padding' }; | |
| if (visiblePanes[1] && !aligned[1][i]) aligned[1][i] = { content: '', lineNum: null, type: 'padding' }; | |
| if (visiblePanes[2] && !aligned[2][i]) aligned[2][i] = { content: '', lineNum: null, type: 'padding' }; | |
| } | |
| return aligned; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| updatePaneVisibility(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment