Use js-diff instead of diff-match-patch for word level diffs

This commit is contained in:
thiloho
2025-04-10 20:54:58 +02:00
parent ebff67e8a7
commit 7dcbd5e9d9
5 changed files with 33 additions and 30 deletions

View File

@@ -10,7 +10,7 @@ let
web = buildNpmPackage { web = buildNpmPackage {
name = "web-app"; name = "web-app";
src = ../web-app; src = ../web-app;
npmDepsHash = "sha256-J58LwSEQa0p6J6h/wPhpGY/60n9a7TOV5WfNm4K1NH0="; npmDepsHash = "sha256-ab7MJ5vl6XNaAHTyzRxj/Zpk1nEKQLzGmPGJdDrdemg=";
npmFlags = [ "--legacy-peer-deps" ]; npmFlags = [ "--legacy-peer-deps" ];
installPhase = '' installPhase = ''
mkdir -p $out/web-app mkdir -p $out/web-app

View File

@@ -8,7 +8,7 @@
"name": "web-app", "name": "web-app",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"diff-match-patch": "1.0.5", "diff": "7.0.0",
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"isomorphic-dompurify": "2.22.0", "isomorphic-dompurify": "2.22.0",
"marked": "15.0.7", "marked": "15.0.7",
@@ -20,7 +20,7 @@
"@sveltejs/adapter-node": "5.2.12", "@sveltejs/adapter-node": "5.2.12",
"@sveltejs/kit": "2.20.2", "@sveltejs/kit": "2.20.2",
"@sveltejs/vite-plugin-svelte": "5.0.3", "@sveltejs/vite-plugin-svelte": "5.0.3",
"@types/diff-match-patch": "1.0.36", "@types/diff": "7.0.2",
"@types/eslint": "9.6.1", "@types/eslint": "9.6.1",
"@types/eslint__js": "9.14.0", "@types/eslint__js": "9.14.0",
"@types/eslint-config-prettier": "6.11.3", "@types/eslint-config-prettier": "6.11.3",
@@ -1425,10 +1425,10 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/diff-match-patch": { "node_modules/@types/diff": {
"version": "1.0.36", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -2125,11 +2125,14 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/diff-match-patch": { "node_modules/diff": {
"version": "1.0.5", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "Apache-2.0" "license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.2.4", "version": "3.2.4",

View File

@@ -19,7 +19,7 @@
"@sveltejs/adapter-node": "5.2.12", "@sveltejs/adapter-node": "5.2.12",
"@sveltejs/kit": "2.20.2", "@sveltejs/kit": "2.20.2",
"@sveltejs/vite-plugin-svelte": "5.0.3", "@sveltejs/vite-plugin-svelte": "5.0.3",
"@types/diff-match-patch": "1.0.36", "@types/diff": "7.0.2",
"@types/eslint": "9.6.1", "@types/eslint": "9.6.1",
"@types/eslint__js": "9.14.0", "@types/eslint__js": "9.14.0",
"@types/eslint-config-prettier": "6.11.3", "@types/eslint-config-prettier": "6.11.3",
@@ -38,7 +38,7 @@
"vite": "6.2.5" "vite": "6.2.5"
}, },
"dependencies": { "dependencies": {
"diff-match-patch": "1.0.5", "diff": "7.0.0",
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"isomorphic-dompurify": "2.22.0", "isomorphic-dompurify": "2.22.0",
"marked": "15.0.7", "marked": "15.0.7",

View File

@@ -3,6 +3,7 @@ import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
import type { ChangeLog, User, Collab } from "$lib/db-schema"; import type { ChangeLog, User, Collab } from "$lib/db-schema";
import DiffMatchPatch from "diff-match-patch"; import DiffMatchPatch from "diff-match-patch";
import { PAGINATION_MAX_ITEMS } from "$lib/utils"; import { PAGINATION_MAX_ITEMS } from "$lib/utils";
import * as Diff from "diff";
export const load: PageServerLoad = async ({ parent, fetch, params, url }) => { export const load: PageServerLoad = async ({ parent, fetch, params, url }) => {
const userFilter = url.searchParams.get("user"); const userFilter = url.searchParams.get("user");
@@ -76,21 +77,18 @@ export const actions: Actions = {
computeDiff: async ({ request, fetch }) => { computeDiff: async ({ request, fetch }) => {
const data = await request.formData(); const data = await request.formData();
const dmp = new DiffMatchPatch();
const htmlDiff = (oldValue: string, newValue: string) => { const htmlDiff = (oldValue: string, newValue: string) => {
const diff = dmp.diff_main(oldValue, newValue); const diff = Diff.diffWordsWithSpace(oldValue, newValue);
return diff return diff
.map(([op, text]) => { .map((part) => {
const escapedText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;"); const escapedText = part.value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
switch (op) { if (part.added) {
case 1:
return `<ins>${escapedText}</ins>`; return `<ins>${escapedText}</ins>`;
case -1: } else if (part.removed) {
return `<del>${escapedText}</del>`; return `<del>${escapedText}</del>`;
default: } else {
return escapedText; return escapedText;
} }
}) })
@@ -112,8 +110,12 @@ export const actions: Actions = {
return { return {
logId: data.get("id"), logId: data.get("id"),
currentDiff: htmlDiff( currentDiff: htmlDiff(
JSON.stringify(log.old_value, null, 2), JSON.stringify(log.old_value, null, 2)
.replace(/\\r\\n|\\n|\\r/g, "\n")
.replace(/\\\"/g, '"'),
JSON.stringify(log.new_value, null, 2) JSON.stringify(log.new_value, null, 2)
.replace(/\\r\\n|\\n|\\r/g, "\n")
.replace(/\\\"/g, '"')
) )
}; };
} }

View File

@@ -141,9 +141,7 @@
<button type="submit">Compute diff</button> <button type="submit">Compute diff</button>
</form> </form>
{#if form?.logId === id && form?.currentDiff} {#if form?.logId === id && form?.currentDiff}
<pre>{@html form.currentDiff <pre>{@html form.currentDiff}</pre>
.replace(/\\\"/g, '"')
.replace(/\\r\\n|\\n|\\r/g, "\n")}</pre>
{/if} {/if}
{/if} {/if}