From 0ae25d22129c97af992b43af865d0cce898dc7cc Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Thu, 4 Sep 2025 10:53:46 +0800 Subject: [PATCH] feat: show source diff between note and revision --- apps/client/src/services/utils.ts | 49 +++++++++++++++ .../src/translations/en/translation.json | 5 ++ apps/client/src/widgets/dialogs/revisions.tsx | 63 +++++++++++++++++-- .../widgets/type_widgets/read_only_code.ts | 51 +-------------- 4 files changed, 115 insertions(+), 53 deletions(-) diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 77fec1366..bf3894474 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -297,6 +297,54 @@ function isHtmlEmpty(html: string) { ); } +function formatHtml(html: string) { + let indent = "\n"; + const tab = "\t"; + let i = 0; + let pre: { indent: string; tag: string }[] = []; + + html = html + .replace(new RegExp("
((.|\\t|\\n|\\r)+)?"), function (x) { + pre.push({ indent: "", tag: x }); + return "<--TEMPPRE" + i++ + "/-->"; + }) + .replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) { + let ret; + const tagRegEx = /<\/?([^\s/>]+)/.exec(x); + let tag = tagRegEx ? tagRegEx[1] : ""; + let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x); + + if (p) { + const pInd = parseInt(p[1]); + pre[pInd].indent = indent; + } + + if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) { + // self closing tag + ret = indent + x; + } else { + if (x.indexOf("") < 0) { + //open tag + if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length); + else ret = indent + x; + !p && (indent += tab); + } else { + //close tag + indent = indent.substr(0, indent.length - 1); + if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length); + else ret = indent + x; + } + } + return ret; + }); + + for (i = pre.length; i--;) { + html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("
", "\n").replace("", pre[i].indent + ""));
+ }
+
+ return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
+}
+
export async function clearBrowserCache() {
if (isElectron()) {
const win = dynamicRequire("@electron/remote").getCurrentWindow();
@@ -855,6 +903,7 @@ export default {
getNoteTypeClass,
getMimeTypeClass,
isHtmlEmpty,
+ formatHtml,
clearBrowserCache,
copySelectionToClipboard,
dynamicRequire,
diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json
index d76843a27..1fb32af95 100644
--- a/apps/client/src/translations/en/translation.json
+++ b/apps/client/src/translations/en/translation.json
@@ -263,6 +263,11 @@
"confirm_delete_all": "Do you want to delete all revisions of this note?",
"no_revisions": "No revisions for this note yet...",
"restore_button": "Restore",
+ "diff_button": "Diff",
+ "content_button": "Content",
+ "diff_button_title": "Show note source diff",
+ "content_button_title": "Show revision content",
+ "diff_not_available": "Diff isn't available.",
"confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.",
"delete_button": "Delete",
"confirm_delete": "Do you want to delete this revision?",
diff --git a/apps/client/src/widgets/dialogs/revisions.tsx b/apps/client/src/widgets/dialogs/revisions.tsx
index 0fa4f956e..78f4468ae 100644
--- a/apps/client/src/widgets/dialogs/revisions.tsx
+++ b/apps/client/src/widgets/dialogs/revisions.tsx
@@ -18,12 +18,15 @@ import open from "../../services/open";
import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import { useTriliumEvent } from "../react/hooks";
+import { diffWords } from "diff";
export default function RevisionsDialog() {
const [ note, setNote ] = useState((.|\\t|\\n|\\r)+)?"), function (x) { - pre.push({ indent: "", tag: x }); - return "<--TEMPPRE" + i++ + "/-->"; - }) - .replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) { - let ret; - const tagRegEx = /<\/?([^\s/>]+)/.exec(x); - let tag = tagRegEx ? tagRegEx[1] : ""; - let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x); - - if (p) { - const pInd = parseInt(p[1]); - pre[pInd].indent = indent; - } - - if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) { - // self closing tag - ret = indent + x; - } else { - if (x.indexOf("") < 0) { - //open tag - if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length); - else ret = indent + x; - !p && (indent += tab); - } else { - //close tag - indent = indent.substr(0, indent.length - 1); - if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length); - else ret = indent + x; - } - } - return ret; - }); - - for (i = pre.length; i--;) { - html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("
", "\n").replace("", pre[i].indent + ""));
- }
-
- return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
- }
}