CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
- GET /versions lists the automatic backups of the selected file (timestamp,
size); ?show=<backup> renders a line diff against the current content
- POST /restore validates the backup against the content schema and restores
it atomically; the pre-restore state gets a fresh backup first, so a
restore itself is reversible; audited as version_restored
- dependency-free LCS line diff (scripts/cms-diff.js) with add/del
highlighting and context trimming; backup names validated against a strict
pattern (path traversal impossible)
- new 🕘 Verziók entry in the CMS bottom bar
- refactor: security/infra helpers extracted to scripts/cms-core.js to keep
content-editor.js under the 400-line hard limit
- integration test: list, diff, restore + reversibility backup, traversal
rejection, CSRF enforcement, auth
Closes MITHOME-64
66 lines
2.2 KiB
JavaScript
66 lines
2.2 KiB
JavaScript
// Dependency-free line diff (LCS) for the CMS version comparison view.
|
|
// Input lines are plain text; output entries are typed add/del/ctx rows.
|
|
|
|
function diffLines(oldLines, newLines) {
|
|
const n = oldLines.length;
|
|
const m = newLines.length;
|
|
// LCS lengths DP (files are small, a few hundred lines — O(n*m) is fine)
|
|
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
for (let i = n - 1; i >= 0; i--) {
|
|
for (let j = m - 1; j >= 0; j--) {
|
|
dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
}
|
|
}
|
|
const out = [];
|
|
let i = 0;
|
|
let j = 0;
|
|
while (i < n && j < m) {
|
|
if (oldLines[i] === newLines[j]) {
|
|
out.push({ type: 'ctx', text: oldLines[i] });
|
|
i++;
|
|
j++;
|
|
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
out.push({ type: 'del', text: oldLines[i] });
|
|
i++;
|
|
} else {
|
|
out.push({ type: 'add', text: newLines[j] });
|
|
j++;
|
|
}
|
|
}
|
|
while (i < n) { out.push({ type: 'del', text: oldLines[i] }); i++; }
|
|
while (j < m) { out.push({ type: 'add', text: newLines[j] }); j++; }
|
|
return out;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
// Keep only ±contextAround context lines around changes to keep pages small.
|
|
function trimContext(entries, contextAround = 3) {
|
|
const keep = new Array(entries.length).fill(false);
|
|
entries.forEach((e, idx) => {
|
|
if (e.type !== 'ctx') {
|
|
for (let k = Math.max(0, idx - contextAround); k <= Math.min(entries.length - 1, idx + contextAround); k++) keep[k] = true;
|
|
}
|
|
});
|
|
const out = [];
|
|
let skipping = false;
|
|
entries.forEach((e, idx) => {
|
|
if (keep[idx]) { out.push(e); skipping = false; }
|
|
else if (!skipping) { out.push({ type: 'skip', text: '…' }); skipping = true; }
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function renderDiffHtml(oldText, newText) {
|
|
const entries = trimContext(diffLines(oldText.split('\n'), newText.split('\n')));
|
|
return entries.map(e => `<div class="diff-${e.type}">${escapeHtml(e.text) || ' '}</div>`).join('\n');
|
|
}
|
|
|
|
module.exports = { diffLines, trimContext, renderDiffHtml, escapeHtml };
|