Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ Vue diff viewer plugin
* [props](#props)
- [Custom theme](#custom-theme)
- [Extend languages](#extend-languages)
- [Large text comparison](#large-text-comparison)

## Introduction

<img src="https://user-images.githubusercontent.com/25652218/104784360-7520e600-57cb-11eb-8abc-ce81dd309e05.png" alt="screenshot" style="max-width:100%;">

You can see the difference between the two codes with the `vue-diff` plugin.
This plugin dependent on <a href="https://github.com/kpdecker/jsdiff">diff</a> and <a href="https://github.com/highlightjs/highlight.js/">highlight.js</a>, shows similar results to other diff viewers (e.g., Github Desktop).
This plugin dependent on <a href="https://github.com/JackuB/diff-match-patch">diff-match-patch</a> and <a href="https://github.com/highlightjs/highlight.js/">highlight.js</a>, shows similar results to other diff viewers (e.g., Github Desktop).
Here is the <a href="https://hoiheart.github.io/vue-diff/demo/index.html" target="_blank" style="font-size: 1.2em; text-decoration: underline;">demo</a>

## Features

* [x] Support split / unified mode
* [x] Support multiple languages and can be extended
* [X] Support two themes (dark / light) and can be customized
* [ ] Virtual scroll for large text comparison
* [ ] Support IE11 (IE 11 support for Vue@3 is still pending)

## Install plugin
Expand Down Expand Up @@ -145,4 +147,8 @@ VueDiff.hljs.registerLanguage('yaml', yaml)
app.use(VueDiff)
```

> <a href="https://github.com/highlightjs/highlight.js/blob/master/SUPPORTED_LANGUAGES.md">Check supported languages of Highlight.js</a>
> <a href="https://github.com/highlightjs/highlight.js/blob/master/SUPPORTED_LANGUAGES.md">Check supported languages of Highlight.js</a>

## Large text comparison

⚠️ It's still hard to compare large texts. Virtual scroll for Vue3 must be created or found.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"vue": "^3.0.0"
},
"dependencies": {
"diff": "^5.0.0",
"diff-match-patch": "^1.0.5",
"highlight.js": "^10.5.0"
},
"devDependencies": {
Expand All @@ -46,7 +46,7 @@
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-typescript": "^6.1.0",
"@semantic-release/git": "^9.0.0",
"@types/diff": "^5.0.0",
"@types/diff-match-patch": "^1.0.32",
"@types/highlight.js": "^10.1.0",
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.33.0",
Expand Down
8 changes: 7 additions & 1 deletion src/Diff.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ export default defineComponent({
() => props.prev,
() => props.current
], () => {
lines.value = renderLines(props.mode, props.prev, props.current)
const render = renderLines(props.mode, props.prev, props.current)

if (render.length > 1000) {
console.warn('Comparison of many lines is not recommended because rendering delays occur.')
}

lines.value = render
}, { immediate: true })

return { lines }
Expand Down
82 changes: 52 additions & 30 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import * as Diff from 'diff'
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch'
import hljs from './highlight'

import type { Ref } from 'vue'
import type { Change } from 'diff'
import type { Diff } from 'diff-match-patch'

type Mode = 'split' | 'unified'
type Theme = 'dark' | 'light' | 'custom'
type Role = 'prev' | 'current' | 'unified'

enum Type {
removed = -1,
equal = 0,
added = 1,
disabled = 2
}

interface Line {
type: 'added' | 'removed' | 'equal' | 'disabled';
type: string;
lineNum?: number;
value?: string;
chkWords?: boolean;
}

type Lines = Array<Line>
type Diffs = Array<Change>
type Diffs = Array<Diff>

const MODIFIED_START_TAG = '<vue-diff-modified>'
const MODIFIED_CLOSE_TAG = '</vue-diff-modified>'
Expand All @@ -25,9 +32,9 @@ const MODIFIED_CLOSE_TAG = '</vue-diff-modified>'
* Get diff type
* @param diff
*/
const getDiffType = (diff: Change) => {
if (!diff.count) return 'disabled'
return diff.added ? 'added' : diff.removed ? 'removed' : 'equal'
const getDiffType = (type: Type) => {
if (!Type[type]) return 'disabled'
return Type[type]
}

/**
Expand All @@ -42,28 +49,28 @@ const getSplitLines = (diffsMap: Array<Diffs>): Array<Lines> => {
}

diffsMap.map((diffs) => {
const prevLines = diffs[0].value.replace(/\n$/, '').split('\n')
const currentLines = diffs[1].value.replace(/\n$/, '').split('\n')
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]) !== 'disabled'
const hasCurrentLine = getDiffType(diffs[1]) !== 'disabled'
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(diffs[0].count === diffs[1].count && getDiffType(diffs[0]).match(/added|removed/) && getDiffType(diffs[1]).match(/added|removed/))
const chkWords = Boolean(getDiffType(diffs[0][0]).match(/added|removed/) && getDiffType(diffs[1][0]).match(/added|removed/))

result.push([
{
type: getDiffType(diffs[0]),
type: hasPrevLine ? getDiffType(diffs[0][0]) : 'disabled',
lineNum: hasPrevLine ? lineNum.prev : undefined,
value: hasPrevLine ? prevLines[i] : undefined,
chkWords
},
{
type: getDiffType(diffs[1]),
type: hasCurrentLine ? getDiffType(diffs[1][0]) : 'disabled',
lineNum: hasCurrentLine ? lineNum.current : undefined,
value: hasCurrentLine ? currentLines[i] : undefined,
chkWords
Expand All @@ -84,33 +91,33 @@ const getUnifiedLines = (diffsMap: Array<Diffs>): Array<Lines> => {
let lineNum = 0

diffsMap.map((diffs) => {
const prevLines = diffs[0].value.replace(/\n$/, '').split('\n')
const currentLines = diffs[1].value.replace(/\n$/, '').split('\n')
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])
const type = getDiffType(diffs[0][0])

if (type !== 'removed') return

result.push([
{
type: getDiffType(diffs[0]),
type: getDiffType(diffs[0][0]),
lineNum: undefined,
value: value
}
])
})

currentLines.map(value => {
const type = getDiffType(diffs[1])
const type = getDiffType(diffs[1][0])

if (type === 'disabled') return

lineNum = lineNum + 1

result.push([
{
type: getDiffType(diffs[1]),
type: getDiffType(diffs[1][0]),
lineNum,
value: value
}
Expand All @@ -128,11 +135,22 @@ const getUnifiedLines = (diffsMap: Array<Diffs>): Array<Lines> => {
* @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 = Diff.diffLines(prev, current).reduce((acc: Array<Diffs>, curr) => {
const type = getDiffType(curr)
const diffsMap = diffLines(prev, current).reduce((acc: Array<Diffs>, curr) => {
const type = getDiffType(curr[0])

if (type === 'equal') {
acc.push([curr]) // Push index 0
Expand All @@ -143,7 +161,8 @@ const renderLines = (mode: Mode, prev: string, current: string): Array<Lines> =>
}

if (type === 'added') {
if (acc.length && acc[acc.length - 1][0] && acc[acc.length - 1][0].removed) {
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
Expand All @@ -159,14 +178,14 @@ const renderLines = (mode: Mode, prev: string, current: string): Array<Lines> =>
diffsMap.map((diffs) => {
if (diffs.length > 1) return // Return if has index 0, 1

const type = getDiffType(diffs[0])
const type = getDiffType(diffs[0][0])

if (type === 'added') {
diffs.unshift({ value: '' }) // Set empty data
diffs.unshift([2, '']) // Set empty data
} else if (type === 'removed') {
diffs.push({ value: '' }) // Set empty data
diffs.push([2, '']) // Set empty data
} else if (type === 'equal') {
diffs.push({ ...diffs[0] }) // Set same data
diffs.push([...diffs[0]]) // Set same data
}
})

Expand All @@ -191,8 +210,11 @@ const renderWords = (prev: string, current: string) => {
/**
* Set modified tags in changed words (removed -> added)
*/
return Diff.diffWords(prev, current).filter(word => getDiffType(word) !== 'removed').map(word => {
return getDiffType(word) === 'added' ? `${MODIFIED_START_TAG}${word.value}${MODIFIED_CLOSE_TAG}` : word.value
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('')
}

Expand Down Expand Up @@ -269,4 +291,4 @@ const setHighlightCode = ({ highlightCode, language, code }: { highlightCode: Re
}

export { MODIFIED_START_TAG, MODIFIED_CLOSE_TAG, getDiffType, getSplitLines, getUnifiedLines, renderLines, renderWords, setHighlightCode }
export type { Mode, Theme, Role, Change, Lines, Line }
export type { Mode, Theme, Role, Lines, Line }