All files / helpers/diff js-diff.engine.ts

100% Statements 15/15
81.25% Branches 13/16
100% Functions 5/5
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57  1x   1x     13x           44x   1x   22x           49x             22x   3x       12x         1x         5x             1x        
import type { DiffChange, DiffEngine, JsDiffOptions } from './diff.types';
import { DEFAULT_JS_DIFF_OPTIONS } from './diff.types';
import type { Change } from 'diff';
import { diffChars, diffLines, diffSentences, diffWords, diffWordsWithSpace } from 'diff';
 
function createWordSegmenter(): Intl.Segmenter | undefined {
    return typeof Intl.Segmenter !== 'undefined' ? new Intl.Segmenter(undefined, { granularity: 'word' }) : undefined;
}
 
// Workaround for jsdiff bug: Intl.Segmenter treats "space + combining mark" as a single
// non-word segment, but diffWords' postProcess assumes space is always a separate suffix.
// Orphaned combining marks (after whitespace) are wiki-markup artifacts with no visual effect.
const stripOrphanedCombiningMarks = (text: string): string => text.replace(/(\s)[\u0300-\u036F\u0483-\u0489]+/g, '$1');
 
export class JsDiffEngine implements DiffEngine {
    computeDiff(oldText: string, newText: string, options: JsDiffOptions = DEFAULT_JS_DIFF_OPTIONS): DiffChange[] {
        const changes = this.computeChanges(
            stripOrphanedCombiningMarks(oldText),
            stripOrphanedCombiningMarks(newText),
            options
        );
 
        return changes.map(change => ({
            type: change.added ? 'insert' : change.removed ? 'delete' : 'equal',
            text: change.value,
        }));
    }
 
    private computeChanges(oldText: string, newText: string, options: JsDiffOptions): Change[] {
        switch (options.granularity) {
            case 'chars':
                return diffChars(oldText, newText, {
                    ignoreCase: options.ignoreCase,
                });
            case 'words':
                return diffWords(oldText, newText, {
                    ignoreCase: options.ignoreCase,
                    intlSegmenter: options.intlSegmenter ? createWordSegmenter() : undefined,
                });
            case 'wordsWithSpace':
                return diffWordsWithSpace(oldText, newText, {
                    ignoreCase: options.ignoreCase,
                    intlSegmenter: options.intlSegmenter ? createWordSegmenter() : undefined,
                });
            case 'lines':
                return diffLines(oldText, newText, {
                    stripTrailingCr: options.stripTrailingCr,
                    newlineIsToken: options.newlineIsToken,
                    ignoreNewlineAtEof: options.ignoreNewlineAtEof,
                    ignoreWhitespace: options.ignoreWhitespace,
                });
            case 'sentences':
                return diffSentences(oldText, newText);
        }
    }
}