diff --git a/src/formatters/console.ts b/src/formatters/console.ts index 6666b8b0..1deba496 100644 --- a/src/formatters/console.ts +++ b/src/formatters/console.ts @@ -12,19 +12,42 @@ import type { TextDiffDelta, } from '../types'; -const colors: { [key: string]: chalk.Chalk | undefined } = { - added: chalk.green, - deleted: chalk.red, - movedestination: chalk.gray, - moved: chalk.yellow, - unchanged: chalk.gray, - error: chalk.white.bgRed, - textDiffLine: chalk.gray, -}; +export interface ConsoleFormatterOptions { + colors: { + added: chalk.Chalk; + deleted: chalk.Chalk; + movedestination: chalk.Chalk; + moved: chalk.Chalk; + unchanged: chalk.Chalk; + error: chalk.Chalk; + textDiffLine: chalk.Chalk; + + // TODO: should all of the DeltaType's have a color? + [key: string]: chalk.Chalk | undefined + }; + + omitUnchangedAfter?: number; +} + +const defaultOptions: ConsoleFormatterOptions = { + omitUnchangedAfter: undefined, + colors: { + added: chalk.green, + deleted: chalk.red, + movedestination: chalk.gray, + moved: chalk.yellow, + unchanged: chalk.gray, + error: chalk.white.bgRed, + textDiffLine: chalk.gray, + } +} + interface ConsoleFormatterContext extends BaseFormatterContext { indentLevel?: number; indentPad?: string; + unchangedCounter: number; + omittedCount: number; outLine: () => void; indent: (levels?: number) => void; color?: (chalk.Chalk | undefined)[]; @@ -33,13 +56,21 @@ interface ConsoleFormatterContext extends BaseFormatterContext { } class ConsoleFormatter extends BaseFormatter { - constructor() { + + public readonly options: ConsoleFormatterOptions; + constructor(options?: Partial) { super(); this.includeMoveDestinations = false; + this.options = { + ...defaultOptions, + ...options, + }; } prepareContext(context: Partial) { super.prepareContext(context); + context.unchangedCounter = 0; + context.omittedCount = 0; context.indent = function (levels) { this.indentLevel = (this.indentLevel || 0) + (typeof levels === 'undefined' ? 1 : levels); @@ -70,7 +101,7 @@ class ConsoleFormatter extends BaseFormatter { } typeFormattterErrorFormatter(context: ConsoleFormatterContext, err: unknown) { - context.pushColor(colors.error); + context.pushColor(this.options.colors.error); // eslint-disable-next-line @typescript-eslint/restrict-template-expressions context.out(`[ERROR]${err}`); context.popColor(); @@ -85,7 +116,7 @@ class ConsoleFormatter extends BaseFormatter { context.indent(); for (let i = 0, l = lines.length; i < l; i++) { const line = lines[i]; - context.pushColor(colors.textDiffLine); + context.pushColor(this.options.colors.textDiffLine); context.out(`${line.location.line},${line.location.chr} `); context.popColor(); const pieces = line.pieces; @@ -95,7 +126,7 @@ class ConsoleFormatter extends BaseFormatter { pieceIndex++ ) { const piece = pieces[pieceIndex]; - context.pushColor(colors[piece.type]); + context.pushColor(this.options.colors[piece.type]); context.out(piece.text); context.popColor(); } @@ -111,7 +142,7 @@ class ConsoleFormatter extends BaseFormatter { type: DeltaType, nodeType: NodeType, ) { - context.pushColor(colors[type]); + context.pushColor(this.options.colors[type]); if (type === 'node') { context.out(nodeType === 'array' ? '[' : '{'); context.indent(); @@ -137,7 +168,27 @@ class ConsoleFormatter extends BaseFormatter { type: DeltaType, nodeType: NodeType, ) { - context.pushColor(colors[type]); + if (this.options.omitUnchangedAfter && this.options.omitUnchangedAfter > 0) { + if (type === 'unchanged') { + context.unchangedCounter++; + if (context.unchangedCounter >= this.options.omitUnchangedAfter) { + context.omittedCount++; + return; + } + } else { + if (context.omittedCount > 0) { + context.pushColor(this.options.colors.unchanged); + context.out(`... omitted ${context.omittedCount} unchanged fields`); + context.outLine(); + context.popColor(); + } + + context.omittedCount = 0 + context.unchangedCounter = 0; + } + } + + context.pushColor(this.options.colors[type]); context.out(`${leftKey}: `); if (type === 'node') { context.out(nodeType === 'array' ? '[' : '{'); @@ -153,6 +204,10 @@ class ConsoleFormatter extends BaseFormatter { nodeType: NodeType, isLast: boolean, ) { + if (context.omittedCount > 0) { + return; // skip node end when omitting unchanged... + } + if (type === 'node') { context.indent(-1); context.out(nodeType === 'array' ? ']' : `}${isLast ? '' : ','}`); @@ -171,6 +226,11 @@ class ConsoleFormatter extends BaseFormatter { if (typeof left === 'undefined') { return; } + + if (context.omittedCount > 0) { + return; // skip node end when omitting unchanged... + } + this.formatValue(context, left); } @@ -199,11 +259,11 @@ class ConsoleFormatter extends BaseFormatter { } format_modified(context: ConsoleFormatterContext, delta: ModifiedDelta) { - context.pushColor(colors.deleted); + context.pushColor(this.options.colors.deleted); this.formatValue(context, delta[0]); context.popColor(); context.out(' => '); - context.pushColor(colors.added); + context.pushColor(this.options.colors.added); this.formatValue(context, delta[1]); context.popColor(); } diff --git a/test/index.spec.ts b/test/index.spec.ts index 4f2dfda7..4a1ff718 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -3,6 +3,8 @@ import * as jsondiffpatch from '../src/main'; import lcs from '../src/filters/lcs'; import examples from './examples/diffpatch'; +import ConsoleFormatter from '../src/formatters/console'; +import { format } from '../src/formatters/html'; describe('jsondiffpatch', () => { it('has a diff method', () => { @@ -746,6 +748,84 @@ describe('DiffPatcher', () => { expectFormat(before, after, expectedHtml(expectedDiff)); }); }); + + describe('console', () => { + let instance: jsondiffpatch.DiffPatcher; + let formatter: ConsoleFormatter; + + beforeAll(() => { + instance = new DiffPatcher(); + formatter = new ConsoleFormatter({ + omitUnchangedAfter: 5 + }) + }); + + const unchanged = (value: string): string => formatter.options.colors.unchanged(value); + const deleted = (value: string): string => formatter.options.colors.deleted(value); + const added = (value: string): string => formatter.options.colors.added(value); + + it('should format diffs', () => { + const left = { + foo: 'foo', + bar: 'bar', + baz: 'baz', + }; + const right = { + foo: 'baz', + bar: 'bar', + } + + const delta = instance.diff(left, right); + const diff = formatter.format(delta, left); + // console.log(diff); // remove to see in console with colors. + + // this check feels a little brittle but can't think of a better option.. + expect(diff).toEqual(`\ +{ + ${unchanged('bar: ')}${unchanged('"bar"')} + ${deleted('baz: ')}${deleted('"baz"')} + foo: ${deleted('"foo"')} => ${added('"baz"')} +}`) + }); + + it('should omit unchanged lines', () => { + const lots = {} as any; + for (let i = 0; i < 1000; i++) { + const v = `baz${i.toString().padStart(3, '0')}` + lots[v] = v; + } + + const left = { + foo: 'foo', + ...lots, + bar: 'bar', + baz: 'baz', + }; + const right = { + foo: 'baz', + ...lots, + bar: 'bar', + baz: 'foo', + } + + const delta = instance.diff(left, right); + const diff = formatter.format(delta, left); + // console.log(diff); remove to see in console with colors + + // this check feels a little brittle but can't think of a better option.. + expect(diff).toEqual(`\ +{ + ${unchanged('bar: ')}${unchanged('"bar"')} + baz: ${deleted('"baz"')} => ${added('"foo"')} + ${unchanged('baz000: ')}${unchanged('"baz000"')} + ${unchanged('baz001: ')}${unchanged('"baz001"')} + ${unchanged('baz002: ')}${unchanged('"baz002"')} + ${unchanged('baz003: ')}${unchanged('"baz003"')} + ${unchanged('... omitted 996 unchanged fields')} + foo: ${deleted('"foo"')} => ${added('"baz"')} +}`) + }); + }) }); });