DevTools TextDiff: Unified Diff Format mit gepaarten Zeilennummern

This commit is contained in:
2026-06-01 11:41:21 +02:00
parent 9995571573
commit 3f63a6dcd7

View File

@@ -945,47 +945,37 @@ function diffLines(a, b) {
const aLines = a.split('\n'); const aLines = a.split('\n');
const bLines = b.split('\n'); const bLines = b.split('\n');
const m = aLines.length, n = bLines.length; const m = aLines.length, n = bLines.length;
// LCS dp
const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0)); const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0));
for (let i = m-1; i >= 0; i--) for (let i = m-1; i >= 0; i--)
for (let j = n-1; j >= 0; j--) for (let j = n-1; j >= 0; j--)
dp[i][j] = aLines[i] === bLines[j] ? dp[i+1][j+1]+1 : Math.max(dp[i+1][j], dp[i][j+1]); dp[i][j] = aLines[i] === bLines[j] ? dp[i+1][j+1]+1 : Math.max(dp[i+1][j], dp[i][j+1]);
// Build flat ops // Build rows: same / del / add — consecutive del+add stay paired (same display line number)
const ops = []; const rows = []; // {type, lineA, lineB, text}
let i = 0, j = 0; let i = 0, j = 0, lnA = 0, lnB = 0;
while (i < m || j < n) { while (i < m || j < n) {
if (i < m && j < n && aLines[i] === bLines[j]) { if (i < m && j < n && aLines[i] === bLines[j]) {
ops.push({type:'same', a:aLines[i], b:bLines[j]}); i++; j++; lnA++; lnB++;
rows.push({type:'same', ln: lnA, text: aLines[i]});
i++; j++;
} else if (j < n && (i >= m || dp[i][j+1] >= dp[i+1][j])) { } else if (j < n && (i >= m || dp[i][j+1] >= dp[i+1][j])) {
ops.push({type:'add', a:null, b:bLines[j]}); j++; lnB++;
rows.push({type:'add', ln: lnB, text: bLines[j]});
j++;
} else { } else {
ops.push({type:'del', a:aLines[i], b:null}); i++; lnA++;
rows.push({type:'del', ln: lnA, text: aLines[i]});
i++;
} }
} }
// Pair up consecutive del+add into 'change' rows return rows;
const paired = [];
let k = 0;
while (k < ops.length) {
if (ops[k].type==='del' && k+1 < ops.length && ops[k+1].type==='add') {
paired.push({type:'change', a:ops[k].a, b:ops[k+1].b}); k+=2;
} else {
paired.push(ops[k]); k++;
}
}
return paired;
} }
// Compute set of changed line numbers (1-based) for a given side // Which line numbers (1-based) in text A/B are changed
function changedLineNums(pairs, side) { function changedLineNums(rows, side) {
const set = new Set(); const set = new Set();
let ln = 0; rows.forEach(r => {
pairs.forEach(p => { if (r.type === 'del' && side === 'a') set.add(r.ln);
if (p.type==='same') { ln++; } if (r.type === 'add' && side === 'b') set.add(r.ln);
else if (p.type==='change') { ln++; set.add(ln); }
else if (p.type==='del' && side==='a') { ln++; set.add(ln); }
else if (p.type==='add' && side==='b') { ln++; set.add(ln); }
else if (p.type==='del' && side==='b') { /* b has no line here */ }
else if (p.type==='add' && side==='a') { /* a has no line here */ }
}); });
return set; return set;
} }
@@ -1011,11 +1001,10 @@ function TextDiff({ mobile }) {
e.target.value = ''; e.target.value = '';
} }
const added = diff ? diff.filter(d=>d.type==='add'||d.type==='change').length : 0; const added = diff ? diff.filter(d=>d.type==='add').length : 0;
const removed = diff ? diff.filter(d=>d.type==='del'||d.type==='change').length : 0; const removed = diff ? diff.filter(d=>d.type==='del').length : 0;
const same = diff ? diff.filter(d=>d.type==='same').length : 0; const same = diff ? diff.filter(d=>d.type==='same').length : 0;
// Which lines are changed in each textarea (for background highlight)
const changedA = diff ? changedLineNums(diff, 'a') : new Set(); const changedA = diff ? changedLineNums(diff, 'a') : new Set();
const changedB = diff ? changedLineNums(diff, 'b') : new Set(); const changedB = diff ? changedLineNums(diff, 'b') : new Set();
@@ -1023,14 +1012,13 @@ function TextDiff({ mobile }) {
boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:'1.6', padding:'7px 8px' }; boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:'1.6', padding:'7px 8px' };
function LineNumCol({ text, changedSet }) { function LineNumCol({ text, changedSet }) {
const lines = (text||' ').split('\n');
return ( return (
<div style={{ <div style={{
padding:'7px 6px', minWidth:36, textAlign:'right', userSelect:'none', padding:'7px 6px', minWidth:36, textAlign:'right', userSelect:'none',
fontFamily:"'Courier New',monospace", fontSize:12, lineHeight:'1.6', fontFamily:"'Courier New',monospace", fontSize:12, lineHeight:'1.6',
background:'rgba(0,0,0,0.2)', borderRight:'1px solid rgba(255,255,255,0.07)', background:'rgba(0,0,0,0.2)', borderRight:'1px solid rgba(255,255,255,0.07)',
whiteSpace:'pre', flexShrink:0}}> whiteSpace:'pre', flexShrink:0}}>
{lines.map((_, i) => ( {(text||' ').split('\n').map((_,i) => (
<div key={i} style={{ <div key={i} style={{
color: changedSet.has(i+1) ? '#ff6b9d' : 'rgba(255,255,255,0.25)', color: changedSet.has(i+1) ? '#ff6b9d' : 'rgba(255,255,255,0.25)',
background: changedSet.has(i+1) ? 'rgba(255,107,157,0.12)' : 'transparent', background: changedSet.has(i+1) ? 'rgba(255,107,157,0.12)' : 'transparent',
@@ -1073,9 +1061,9 @@ function TextDiff({ mobile }) {
{!!diff && ( {!!diff && (
<div style={{display:'flex', gap:10, alignItems:'center', flexWrap:'wrap'}}> <div style={{display:'flex', gap:10, alignItems:'center', flexWrap:'wrap'}}>
<span style={{background:'rgba(255,107,157,0.15)', color:'#ff6b9d', fontFamily:'monospace', <span style={{background:'rgba(255,107,157,0.15)', color:'#ff6b9d', fontFamily:'monospace',
fontSize:11, padding:'4px 10px', borderRadius:6}}> {removed} entfernt/geändert</span> fontSize:11, padding:'4px 10px', borderRadius:6}}> {removed} entfernt</span>
<span style={{background:'rgba(78,205,196,0.15)', color:'#4ecdc4', fontFamily:'monospace', <span style={{background:'rgba(78,205,196,0.15)', color:'#4ecdc4', fontFamily:'monospace',
fontSize:11, padding:'4px 10px', borderRadius:6}}>+ {added} hinzugefügt/geändert</span> fontSize:11, padding:'4px 10px', borderRadius:6}}>+ {added} hinzugefügt</span>
<span style={{background:'rgba(255,255,255,0.05)', color:'rgba(255,255,255,0.45)', fontFamily:'monospace', <span style={{background:'rgba(255,255,255,0.05)', color:'rgba(255,255,255,0.45)', fontFamily:'monospace',
fontSize:11, padding:'4px 10px', borderRadius:6}}>= {same} gleich</span> fontSize:11, padding:'4px 10px', borderRadius:6}}>= {same} gleich</span>
</div> </div>
@@ -1085,73 +1073,29 @@ function TextDiff({ mobile }) {
{!!diff && ( {!!diff && (
<div style={{borderRadius:9, border:'1px solid rgba(255,255,255,0.08)', <div style={{borderRadius:9, border:'1px solid rgba(255,255,255,0.08)',
overflow:'auto', maxHeight:520, background:'rgba(0,0,0,0.2)'}}> overflow:'auto', maxHeight:520, background:'rgba(0,0,0,0.2)'}}>
{/* Header */} <table style={{width:'100%', borderCollapse:'collapse', fontFamily:"'Courier New',monospace", fontSize:12}}>
<div style={{display:'grid', gridTemplateColumns:'36px 16px 1fr 36px 16px 1fr',
borderBottom:'1px solid rgba(255,255,255,0.1)', background:'rgba(255,255,255,0.04)'}}>
<div style={{gridColumn:'1/4', padding:'5px 10px', color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, borderRight:'1px solid rgba(255,255,255,0.08)'}}>Text A (Original)</div>
<div style={{gridColumn:'4/7', padding:'5px 10px', color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10}}>Text B (Neu)</div>
</div>
<table style={{width:'100%', borderCollapse:'collapse', fontFamily:"'Courier New',monospace", fontSize:12, tableLayout:'fixed'}}>
<colgroup>
<col style={{width:36}}/><col style={{width:16}}/><col/>
<col style={{width:36}}/><col style={{width:16}}/><col/>
</colgroup>
<tbody> <tbody>
{(()=>{ {diff.map((d,i) => (
let lnA=0, lnB=0; <tr key={i} style={{
return diff.map((d,i)=>{ background: d.type==='add' ? 'rgba(78,205,196,0.1)' : d.type==='del' ? 'rgba(255,107,157,0.1)' : 'transparent',
const isChange = d.type==='change'; borderBottom:'1px solid rgba(255,255,255,0.03)'}}>
const isDel = d.type==='del'; <td style={{width:36, textAlign:'right', padding:'2px 8px', userSelect:'none',
const isAdd = d.type==='add'; color:'rgba(255,255,255,0.2)', fontSize:11,
const isSame = d.type==='same'; borderRight:'1px solid rgba(255,255,255,0.05)'}}>
if (isSame) { lnA++; lnB++; } {d.ln}
if (isChange) { lnA++; lnB++; } </td>
if (isDel) lnA++; <td style={{width:20, textAlign:'center', padding:'2px 4px', userSelect:'none',
if (isAdd) lnB++; color: d.type==='add'?'#4ecdc4': d.type==='del'?'#ff6b9d':'rgba(255,255,255,0.15)',
fontWeight:700, fontSize:14,
const bgA = (isDel||isChange) ? 'rgba(255,107,157,0.1)' : 'transparent'; borderRight:'1px solid rgba(255,255,255,0.05)'}}>
const bgB = (isAdd||isChange) ? 'rgba(78,205,196,0.1)' : 'transparent'; {d.type==='add'?'+': d.type==='del'?'':''}
const colA = isDel||isChange ? '#ffb3cc' : 'rgba(255,255,255,0.65)'; </td>
const colB = isAdd||isChange ? '#a8f0eb' : 'rgba(255,255,255,0.65)'; <td style={{padding:'3px 10px', whiteSpace:'pre-wrap', wordBreak:'break-all',
const symA = isDel?'': isChange?'~':''; color: d.type==='add'?'#a8f0eb': d.type==='del'?'#ffb3cc':'rgba(255,255,255,0.65)'}}>
const symB = isAdd?'+': isChange?'~':''; {d.text || ' '}
const symColA = isDel?'#ff6b9d': isChange?'#ffe66d':'rgba(255,255,255,0.15)'; </td>
const symColB = isAdd?'#4ecdc4': isChange?'#ffe66d':'rgba(255,255,255,0.15)'; </tr>
))}
return (
<tr key={i} style={{borderBottom:'1px solid rgba(255,255,255,0.03)'}}>
{/* Side A */}
<td style={{background:bgA, textAlign:'right', padding:'2px 6px', color:'rgba(255,255,255,0.2)',
borderRight:'1px solid rgba(255,255,255,0.05)', userSelect:'none', fontSize:11}}>
{(isDel||isChange||isSame) ? lnA : ''}
</td>
<td style={{background:bgA, textAlign:'center', padding:'2px 3px', color:symColA,
fontWeight:700, fontSize:13, userSelect:'none',
borderRight:'1px solid rgba(255,255,255,0.06)'}}>
{symA}
</td>
<td style={{background:bgA, padding:'3px 8px', whiteSpace:'pre-wrap', wordBreak:'break-all',
color:colA, borderRight:'2px solid rgba(255,255,255,0.06)'}}>
{(isDel||isChange||isSame) ? (d.a ?? d.text ?? ' ') : ''}
</td>
{/* Side B */}
<td style={{background:bgB, textAlign:'right', padding:'2px 6px', color:'rgba(255,255,255,0.2)',
borderRight:'1px solid rgba(255,255,255,0.05)', userSelect:'none', fontSize:11}}>
{(isAdd||isChange||isSame) ? lnB : ''}
</td>
<td style={{background:bgB, textAlign:'center', padding:'2px 3px', color:symColB,
fontWeight:700, fontSize:13, userSelect:'none',
borderRight:'1px solid rgba(255,255,255,0.06)'}}>
{symB}
</td>
<td style={{background:bgB, padding:'3px 8px', whiteSpace:'pre-wrap', wordBreak:'break-all',
color:colB}}>
{(isAdd||isChange||isSame) ? (d.b ?? d.text ?? ' ') : ''}
</td>
</tr>
);
});
})()}
</tbody> </tbody>
</table> </table>
</div> </div>