UNPKG

@brianlovin/notion-skills

Version:

Sync agent skills from a Notion database to Claude Code, Codex, OpenCode, Cursor, Gemini CLI.

110 lines 4.24 kB
/** * Tiny LCS-based line diff used to surface "what changed" when sync * pulls an updated skill. We don't ship this as a general-purpose * library — sync's specific needs are small files (SKILL.md typically * <500 lines), output rendered to the terminal, no need for the * Myers-Hunt-McIlroy bells and whistles. * * Pure: no I/O. Deterministic for a given input pair. */ /** * Line-by-line diff via standard LCS dynamic programming. Returns one * hunk per line in the merged result; callers usually fold consecutive * adds/removes into blocks for rendering. * * O(m·n) time + memory. Fine for skill-sized inputs. */ export function computeLineDiff(oldText, newText) { const oldLines = oldText.split("\n"); const newLines = newText.split("\n"); const m = oldLines.length; const n = newLines.length; // Suffix-LCS table: lcs[i][j] = LCS length of oldLines[i..] vs newLines[j..]. const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); for (let i = m - 1; i >= 0; i--) { for (let j = n - 1; j >= 0; j--) { if (oldLines[i] === newLines[j]) { lcs[i][j] = lcs[i + 1][j + 1] + 1; } else { lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]); } } } const hunks = []; let i = 0; let j = 0; while (i < m && j < n) { if (oldLines[i] === newLines[j]) { hunks.push({ type: "context", text: oldLines[i] }); i++; j++; } else if (lcs[i + 1][j] >= lcs[i][j + 1]) { hunks.push({ type: "remove", text: oldLines[i] }); i++; } else { hunks.push({ type: "add", text: newLines[j] }); j++; } } while (i < m) hunks.push({ type: "remove", text: oldLines[i++] }); while (j < n) hunks.push({ type: "add", text: newLines[j++] }); return hunks; } export function renderUnifiedDiff(hunks, options = {}) { const context = options.context ?? 2; const maxLines = options.maxLines ?? Infinity; // First pass: figure out which context lines to keep. A context line // is kept if it's within `context` lines of any change. const keep = new Array(hunks.length).fill(false); for (let i = 0; i < hunks.length; i++) { if (hunks[i].type !== "context") { const lo = Math.max(0, i - context); const hi = Math.min(hunks.length - 1, i + context); for (let k = lo; k <= hi; k++) keep[k] = true; } } if (!keep.some(Boolean)) return []; // Second pass: emit kept lines, with elide separators wherever a run // of skipped (out-of-context) lines exists — including before the // first kept line and after the last kept line. The leading/trailing // elides matter for our use case ("show me diff context"); without // them the user can't tell whether they're seeing the start of the // file or just the first change. const firstKept = keep.findIndex((k) => k); const lastKept = keep.length - 1 - [...keep].reverse().findIndex((k) => k); const out = []; if (firstKept > 0) out.push({ type: "elide", text: "…" }); let prev = -1; for (let i = firstKept; i <= lastKept; i++) { if (!keep[i]) continue; if (prev >= 0 && i > prev + 1) out.push({ type: "elide", text: "…" }); out.push({ type: hunks[i].type, text: hunks[i].text }); prev = i; } if (lastKept < hunks.length - 1) out.push({ type: "elide", text: "…" }); if (out.length <= maxLines) return out; // Truncation: keep the first maxLines-1 entries and add a tail // marker. Simpler than balancing head + tail since users can re-run // with --no-diff (or scroll back to the file) for the full picture. return [ ...out.slice(0, maxLines - 1), { type: "elide", text: `…(+${out.length - (maxLines - 1)} more lines)` }, ]; } /** True iff the diff contains any non-context hunks. */ export function hasChanges(hunks) { return hunks.some((h) => h.type !== "context"); } //# sourceMappingURL=diff.js.map