UNPKG

@leonwerth/vue-diff

Version:

Vue diff

405 lines (351 loc) 10.7 kB
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' import { ref, onMounted, onBeforeUnmount } from 'vue' const MODIFIED_START_TAG = '<vue-diff-modified>' const MODIFIED_CLOSE_TAG = '</vue-diff-modified>' enum DiffType { removed = -1, equal = 0, added = 1, disabled = 2, } /** * Get diff type * @param diff */ const getDiffType = (type: DiffType) => { if (!DiffType[type]) return 'disabled'; return DiffType[type]; }; /** * Get lines object on the split mode * @param diffsMap */ const getSplitLines = (diffsMap: Array<Diffs>): Array<Lines> => { const result: Array<Lines> = []; // Array(0): prev, Array(1): current 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; }; /** * Get lines object on the unified mode * @param diffsMap */ const getUnifiedLines = (diffsMap: Array<Diffs>): Array<Lines> => { const result: Array<Lines> = []; // Array(0) 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; }; /** * Render of objects separated by lines * @param mode * @param prev * @param current */ const renderLines = ( mode: Mode, prev: string, current: string, ): Array<Lines> => { function diffLines(prev: string, current: string) { 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: Array<Diffs>, curr) => { const type = getDiffType(curr[0]); if (type === 'equal') { acc.push([curr]); // Push index 0 } if (type === 'removed') { acc.push([curr]); // Push index 0 } 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); // Push index 1 if index 0 has removed data in last array } else { acc.push([curr]); // Push index 0 } } return acc; }, [], ); /** * Set index 1 in stacked data */ diffsMap.map(diffs => { if (diffs.length > 1) return; // Return if has index 0, 1 const type = getDiffType(diffs[0][0]); if (type === 'added') { diffs.unshift([2, '']); // Set empty data } else if (type === 'removed') { diffs.push([2, '']); // Set empty data } else if (type === 'equal') { diffs.push([...diffs[0]]); // Set same data } }); /** * Get lines object on the mode */ if (mode === 'split') { return getSplitLines(diffsMap); } else if (mode === 'unified') { return getUnifiedLines(diffsMap); } else { return []; } }; /** * Render with modified tags * @param prev * @param current */ const renderWords = (prev: string, current: string) => { /** * Set modified tags in changed words (removed -> added) */ const dmp = new DiffMatchPatch(); const diff = dmp.diff_main(prev, current); dmp.diff_cleanupSemantic(diff); // console.log(diff, '2222222') let word = diff .filter(result => getDiffType(result[0]) !== 'removed') .map(result => { // console.log(result, '4444444') return getDiffType(result[0]) === 'added' ? `${MODIFIED_START_TAG}${result[1]}${MODIFIED_CLOSE_TAG}` : result[1]; }) .join(''); return word }; /** * Set hightlight code * This function must calling at client only (use DOM) */ const setHighlightCode = ({ highlightCode, language, code, }: { highlightCode: Ref; language: string; code: string; }) => { const hasModifiedTags = code.match( new RegExp(`(${MODIFIED_START_TAG}|${MODIFIED_CLOSE_TAG})`, 'g'), ); if (!hasModifiedTags) { // highlightCode.value = hljs.highlight(code, { language }).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; // original code with modified tags const pureCode = code.replace( new RegExp(`(${MODIFIED_START_TAG}|${MODIFIED_CLOSE_TAG})`, 'g'), '', ); // Without modified tags let pureElement = document.createElement('div'); // pureElement.innerHTML = hljs.highlight(pureCode, { language }).value; // Highlight DOM without modified tags // Modified span is created per highlight operator and causes it to continue let innerModifiedTag = false; const diffElements = (node: HTMLElement) => { node.childNodes.forEach(child => { if (child.nodeType === 1) { diffElements(child as HTMLElement); } // Compare text nodes and check changed text if (child.nodeType === 3) { if (!child.textContent) return; let oldContent = child.textContent; let newContent = ''; if (innerModifiedTag) { // If it continues within the modified range newContent = newContent + MODIFIED_START_TAG; } while (oldContent.length) { if (originalCode.startsWith(MODIFIED_START_TAG)) { // Add modified start tag originalCode = originalCode.slice(MODIFIED_START_TAG.length); newContent = newContent + MODIFIED_START_TAG; innerModifiedTag = true; // Start modified continue; } if (originalCode.startsWith(MODIFIED_CLOSE_TAG)) { // Add modified close tag originalCode = originalCode.slice(MODIFIED_CLOSE_TAG.length); newContent = newContent + MODIFIED_CLOSE_TAG; innerModifiedTag = false; // End modified continue; } // Add words before modified tag 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) { // If the loop is finished without a modified close, it is still within the modified range. newContent = newContent + MODIFIED_CLOSE_TAG; } child.textContent = newContent; // put as entity code because change textContent } }); }; 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>'); // @ts-ignore pureElement = null; }; /** * 获取浏览器可视区域的宽高 */ export function useWinSize() { const size = ref({ width: 1920, height: 1080 }); function onResize() { size.value = { width: window.innerWidth, height: window.innerHeight }; } onMounted(() => { window.addEventListener('resize', onResize); }); onBeforeUnmount(() => { window.removeEventListener('resize', onResize); }); return size; } function uuid() { const len = 32; //32长度 let radix = 16; //16进制 const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); const uuid = []; let i; radix = radix || chars.length; if (len) { for (i = 0; i < len; i++) { uuid[i] = chars[0 | (Math.random() * radix)]; } } else { let r; uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; uuid[14] = '4'; for (i = 0; i < 36; i++) { if (!uuid[i]) { r = 0 | (Math.random() * 16); uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r]; } } } return uuid.join(''); } export { MODIFIED_START_TAG, MODIFIED_CLOSE_TAG, getDiffType, getSplitLines, getUnifiedLines, renderLines, renderWords, setHighlightCode, uuid };