import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch' ; import hljs from './highlight' ; import type { Ref } from 'vue' ; import type { Diffs , Lines , Mode } from './types' ; const MODIFIED_START_TAG = '<vue-diff-modified>' ; const MODIFIED_CLOSE_TAG = '</vue-diff-modified>' ; enum DiffType { removed = - 1 , equal = 0 , added = 1 , disabled = 2 } param diff const getDiffType = ( type ) => { if ( ! DiffType [ type ] ) return 'disabled' ; return DiffType [ type ] ; } ; param diffsMap const getSplitLines = ( diffsMap ) : Array < Lines > => { const result = [ ] ; const lineNum = { prev : 0 , current : 0 } ; diffsMap . map ( ( diffs ) => { const prevLines = diffs [ 0 ] [ 1 ] . replace ( /\n$/ , '' ) . split ( '\n' ) ; const currentLines = diffs [ 1 ] [ 1 ] . replace ( /\n$/ , '' ) . split ( '\n' ) ; const loopCount = Math . max ( prevLines . length , currentLines . length ) ; for ( let i = 0 ; i < loopCount ; i ++ ) { const hasPrevLine = getDiffType ( diffs [ 0 ] [ 0 ] ) !== 'disabled' && typeof prevLines [ i ] !== 'undefined' ; const hasCurrentLine = getDiffType ( diffs [ 1 ] [ 0 ] ) !== 'disabled' && typeof currentLines [ i ] !== 'undefined' ; if ( hasPrevLine ) lineNum . prev = lineNum . prev + 1 ; if ( hasCurrentLine ) lineNum . current = lineNum . current + 1 ; const chkWords = Boolean ( getDiffType ( diffs [ 0 ] [ 0 ] ) . match ( /added|removed/ ) && getDiffType ( diffs [ 1 ] [ 0 ] ) . match ( /added|removed/ ) ) ; result . push ( [ { type : hasPrevLine ? getDiffType ( diffs [ 0 ] [ 0 ] ) : 'disabled' , lineNum : hasPrevLine ? lineNum . prev : undefined , value : hasPrevLine ? prevLines [ i ] : undefined , chkWords } , { type : hasCurrentLine ? getDiffType ( diffs [ 1 ] [ 0 ] ) : 'disabled' , lineNum : hasCurrentLine ? lineNum . current : undefined , value : hasCurrentLine ? currentLines [ i ] : undefined , chkWords } ] ) ; } } ) ; return result ; } ; param diffsMap const getUnifiedLines = ( diffsMap ) : Array < Lines > => { const result = [ ] ; let lineNum = 0 ; diffsMap . map ( ( diffs ) => { const prevLines = diffs [ 0 ] [ 1 ] . replace ( /\n$/ , '' ) . split ( '\n' ) ; const currentLines = diffs [ 1 ] [ 1 ] . replace ( /\n$/ , '' ) . split ( '\n' ) ; prevLines . map ( value => { const type = getDiffType ( diffs [ 0 ] [ 0 ] ) ; if ( type !== 'removed' ) return ; result . push ( [ { type : getDiffType ( diffs [ 0 ] [ 0 ] ) , lineNum : undefined , value : value } ] ) ; } ) ; currentLines . map ( value => { const type = getDiffType ( diffs [ 1 ] [ 0 ] ) ; if ( type === 'disabled' ) return ; lineNum = lineNum + 1 ; result . push ( [ { type : getDiffType ( diffs [ 1 ] [ 0 ] ) , lineNum , value : value } ] ) ; } ) ; } ) ; return result ; } ; param mode param prev param current const renderLines = ( mode , prev , current ) : Array < Lines > => { function diffLines ( prev , current ) { const dmp = new DiffMatchPatch ( ) ; const a = dmp . diff_linesToChars_ ( prev , current ) ; const linePrev = a . chars1 ; const lineCurrent = a . chars2 ; const lineArray = a . lineArray ; const diffs = dmp . diff_main ( linePrev , lineCurrent , false ) ; dmp . diff_charsToLines_ ( diffs , lineArray ) ; return diffs ; } /**
     * stacked prev, current data
     */ const diffsMap = diffLines ( prev , current ) . reduce ( ( acc , curr ) => { const type = getDiffType ( curr [ 0 ] ) ; if ( type === 'equal' ) { acc . push ( [ curr ] ) ; } if ( type === 'removed' ) { acc . push ( [ curr ] ) ; } if ( type === 'added' ) { const prev = acc . length && acc [ acc . length - 1 ] [ 0 ] ? acc [ acc . length - 1 ] [ 0 ] : null ; if ( prev && getDiffType ( prev [ 0 ] ) === 'removed' ) { acc [ acc . length - 1 ] . push ( curr ) ; } else { acc . push ( [ curr ] ) ; } } return acc ; } , [ ] ) ; /**
     * Set index 1 in stacked data
     */ diffsMap . map ( ( diffs ) => { if ( diffs . length > 1 ) return ; const type = getDiffType ( diffs [ 0 ] [ 0 ] ) ; if ( type === 'added' ) { diffs . unshift ( [ 2 , '' ] ) ; } else if ( type === 'removed' ) { diffs . push ( [ 2 , '' ] ) ; } else if ( type === 'equal' ) { diffs . push ( [ ... diffs [ 0 ] ] ) ; } } ) ; /**
     * Get lines object on the mode
     */ if ( mode === 'split' ) { return getSplitLines ( diffsMap ) ; } else if ( mode === 'unified' ) { return getUnifiedLines ( diffsMap ) ; } else { return [ ] ; } } ; param prev param current const renderWords = ( prev , current ) => { /**
     * Set modified tags in changed words (removed -> added)
     */ const dmp = new DiffMatchPatch ( ) ; const diff = dmp . diff_main ( prev , current ) ; dmp . diff_cleanupSemantic ( diff ) ; return diff . filter ( result => getDiffType ( result [ 0 ] ) !== 'removed' ) . map ( result => { return getDiffType ( result [ 0 ] ) === 'added' ? `${ MODIFIED_START_TAG }${ result [ 1 ] }${ MODIFIED_CLOSE_TAG }` : result [ 1 ] ; } ) . join ( '' ) ; } ; /**
 * Set hightlight code
 * This function must calling at client only (use DOM)
 */ const setHighlightCode = ( { highlightCode , language , code } ) => { const hasModifiedTags = code . match ( new RegExp ( `(${ MODIFIED_START_TAG }|${ MODIFIED_CLOSE_TAG })` , 'g' ) ) ; if ( ! hasModifiedTags ) { highlightCode . value = hljs . highlight ( language , code ) . value ; return ; } /**
     * Explore highlight DOM extracted from pure code and compare the text with the original code code to generate the highlight code
     */ let originalCode = code ; const pureCode = code . replace ( new RegExp ( `(${ MODIFIED_START_TAG }|${ MODIFIED_CLOSE_TAG })` , 'g' ) , '' ) ; let pureElement = document . createElement ( 'div' ) ; pureElement . innerHTML = hljs . highlight ( language , pureCode ) . value ; let innerModifiedTag = false ; const diffElements = ( node ) => { node . childNodes . forEach ( child => { if ( child . nodeType === 1 ) { diffElements ( child as HTMLElement ) ; } if ( child . nodeType === 3 ) { if ( ! child . textContent ) return ; let oldContent = child . textContent ; let newContent = '' ; if ( innerModifiedTag ) { newContent = newContent + MODIFIED_START_TAG ; } while ( oldContent . length ) { if ( originalCode . startsWith ( MODIFIED_START_TAG ) ) { originalCode = originalCode . slice ( MODIFIED_START_TAG . length ) ; newContent = newContent + MODIFIED_START_TAG ; innerModifiedTag = true ; continue ; } if ( originalCode . startsWith ( MODIFIED_CLOSE_TAG ) ) { originalCode = originalCode . slice ( MODIFIED_CLOSE_TAG . length ) ; newContent = newContent + MODIFIED_CLOSE_TAG ; innerModifiedTag = false ; continue ; } const hasModifiedTag = originalCode . match ( new RegExp ( `(${ MODIFIED_START_TAG }|${ MODIFIED_CLOSE_TAG })` ) ) ; const originalCodeDiffLength = hasModifiedTag && hasModifiedTag . index ? hasModifiedTag . index : originalCode . length ; const nextDiffsLength = Math . min ( originalCodeDiffLength , oldContent . length ) ; newContent = newContent + originalCode . substring ( 0 , nextDiffsLength ) ; originalCode = originalCode . slice ( nextDiffsLength ) ; oldContent = oldContent . slice ( nextDiffsLength ) ; } if ( innerModifiedTag ) { newContent = newContent + MODIFIED_CLOSE_TAG ; } child . textContent = newContent ; } } ) ; } ; diffElements ( pureElement ) ; const startEntity = MODIFIED_START_TAG . replace ( '<' , '&lt;' ) . replace ( '>' , '&gt;' ) ; const closeEntity = MODIFIED_CLOSE_TAG . replace ( '<' , '&lt;' ) . replace ( '>' , '&gt;' ) ; highlightCode . value = pureElement . innerHTML . replace ( new RegExp ( startEntity , 'g' ) , '<span class="modified">' ) . replace ( new RegExp ( closeEntity , 'g' ) , '</span>' ) ; pureElement = null ; } ; export { MODIFIED_START_TAG , MODIFIED_CLOSE_TAG , getDiffType , getSplitLines , getUnifiedLines , renderLines , renderWords , setHighlightCode } ;