import * as plugins from './smartdiff.plugins.js'; // ============================================================================= // Types // ============================================================================= export interface IDiffSegment { type: 'equal' | 'insert' | 'delete'; value: string; } export interface ILineDiff { lineNumber: number; type: 'equal' | 'insert' | 'delete'; value: string; } // ============================================================================= // ANSI Color Helpers (for console output) // ============================================================================= const ANSI = { red: '\x1b[31m', green: '\x1b[32m', gray: '\x1b[90m', reset: '\x1b[0m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m', }; // ============================================================================= // Backward Compatible API (createDiff / applyPatch) // ============================================================================= /** * Creates a compact JSON-encoded diff between two strings. * Format: [[0, length], [1, "inserted text"], [-1, length], ...] * - [0, n] = keep n characters from original * - [1, "text"] = insert "text" * - [-1, n] = delete n characters from original */ export const createDiff = (originalArg: string, revisionArg: string): string => { const changes = plugins.diff.diffChars(originalArg, revisionArg); const result: Array<[number, string | number]> = []; for (const change of changes) { if (change.added) { // INSERT: store the actual text result.push([1, change.value]); } else if (change.removed) { // DELETE: store just the length result.push([-1, change.value.length]); } else { // EQUAL: store just the length result.push([0, change.value.length]); } } return JSON.stringify(result); }; /** * Applies a diff to the original text to reconstruct the revised text. */ export const applyPatch = (originalArg: string, deltaStringArg: string): string => { const deltaArg: Array<[number, string | number]> = JSON.parse(deltaStringArg); let result = ''; let index = 0; for (const item of deltaArg) { const operation = item[0]; const value = item[1]; if (operation === -1) { // DELETE: skip characters in original index += value as number; } else if (operation === 0) { // KEEP: copy characters from original const length = value as number; result += originalArg.slice(index, index + length); index += length; } else { // INSERT: add the new text result += value as string; } } return result; }; // ============================================================================= // Character-Level Diff // ============================================================================= /** * Get character-level diff segments between two strings. */ export const getCharDiff = (original: string, revision: string): IDiffSegment[] => { const changes = plugins.diff.diffChars(original, revision); return changes.map((change) => ({ type: change.added ? 'insert' : change.removed ? 'delete' : 'equal', value: change.value, })); }; /** * Format character diff for console output with ANSI colors. * - Deletions: red background * - Insertions: green background */ export const formatCharDiffForConsole = (original: string, revision: string): string => { const segments = getCharDiff(original, revision); let result = ''; for (const segment of segments) { switch (segment.type) { case 'delete': result += `${ANSI.bgRed}${segment.value}${ANSI.reset}`; break; case 'insert': result += `${ANSI.bgGreen}${segment.value}${ANSI.reset}`; break; default: result += segment.value; } } return result; }; /** * Format character diff as HTML with styled spans. */ export const formatCharDiffAsHtml = (original: string, revision: string): string => { const segments = getCharDiff(original, revision); let result = ''; for (const segment of segments) { const escaped = escapeHtml(segment.value); switch (segment.type) { case 'delete': result += `${escaped}`; break; case 'insert': result += `${escaped}`; break; default: result += `${escaped}`; } } return result; }; // ============================================================================= // Word-Level Diff // ============================================================================= /** * Get word-level diff segments between two strings. */ export const getWordDiff = (original: string, revision: string): IDiffSegment[] => { const changes = plugins.diff.diffWords(original, revision); return changes.map((change) => ({ type: change.added ? 'insert' : change.removed ? 'delete' : 'equal', value: change.value, })); }; /** * Format word diff for console output with ANSI colors. */ export const formatWordDiffForConsole = (original: string, revision: string): string => { const segments = getWordDiff(original, revision); let result = ''; for (const segment of segments) { switch (segment.type) { case 'delete': result += `${ANSI.red}${segment.value}${ANSI.reset}`; break; case 'insert': result += `${ANSI.green}${segment.value}${ANSI.reset}`; break; default: result += segment.value; } } return result; }; /** * Format word diff as HTML with styled spans. */ export const formatWordDiffAsHtml = (original: string, revision: string): string => { const segments = getWordDiff(original, revision); let result = ''; for (const segment of segments) { const escaped = escapeHtml(segment.value); switch (segment.type) { case 'delete': result += `${escaped}`; break; case 'insert': result += `${escaped}`; break; default: result += `${escaped}`; } } return result; }; // ============================================================================= // Line-Level Diff // ============================================================================= /** * Get line-level diff between two strings. */ export const getLineDiff = (original: string, revision: string): ILineDiff[] => { const changes = plugins.diff.diffLines(original, revision); const result: ILineDiff[] = []; let lineNumber = 1; for (const change of changes) { const lines = change.value.split('\n'); // Remove last empty element if the value ends with newline if (lines[lines.length - 1] === '') { lines.pop(); } for (const line of lines) { result.push({ lineNumber: change.removed ? lineNumber : change.added ? -1 : lineNumber, type: change.added ? 'insert' : change.removed ? 'delete' : 'equal', value: line, }); if (!change.added) { lineNumber++; } } } return result; }; /** * Format line diff for console output in unified diff style with ANSI colors. */ export const formatLineDiffForConsole = (original: string, revision: string): string => { const changes = plugins.diff.diffLines(original, revision); let result = ''; for (const change of changes) { const lines = change.value.split('\n'); // Remove last empty element if the value ends with newline if (lines[lines.length - 1] === '') { lines.pop(); } for (const line of lines) { if (change.removed) { result += `${ANSI.red}- ${line}${ANSI.reset}\n`; } else if (change.added) { result += `${ANSI.green}+ ${line}${ANSI.reset}\n`; } else { result += `${ANSI.gray} ${line}${ANSI.reset}\n`; } } } return result; }; /** * Format line diff as HTML with styled divs. */ export const formatLineDiffAsHtml = (original: string, revision: string): string => { const changes = plugins.diff.diffLines(original, revision); let result = '