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 1/5] 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 77fec13668..bf3894474b 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 d76843a270..1fb32af952 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 0fa4f956e3..78f4468aea 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;
- }
}
From fa2188f087ef084e5ce632335bf91fa0d30c5591 Mon Sep 17 00:00:00 2001
From: SiriusXT <1160925501@qq.com>
Date: Thu, 4 Sep 2025 17:36:19 +0800
Subject: [PATCH 2/5] fix: improve tag regex handling when formatting
HTML strings
---
apps/client/src/services/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts
index bf3894474b..a8f4f567fe 100644
--- a/apps/client/src/services/utils.ts
+++ b/apps/client/src/services/utils.ts
@@ -304,7 +304,7 @@ function formatHtml(html: string) {
let pre: { indent: string; tag: string }[] = [];
html = html
- .replace(new RegExp("((.|\\t|\\n|\\r)+)?
"), function (x) {
+ .replace(new RegExp("([\\s\\S]+?)?
"), function (x) {
pre.push({ indent: "", tag: x });
return "<--TEMPPRE" + i++ + "/-->";
})
From 1c451fb98a29101dfca9a57c4266679261bc18a8 Mon Sep 17 00:00:00 2001
From: SiriusXT <1160925501@qq.com>
Date: Thu, 4 Sep 2025 18:47:14 +0800
Subject: [PATCH 3/5] fix: adapt diff highlight for dark theme
---
apps/client/src/widgets/dialogs/revisions.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/client/src/widgets/dialogs/revisions.tsx b/apps/client/src/widgets/dialogs/revisions.tsx
index 78f4468aea..af64e2d84a 100644
--- a/apps/client/src/widgets/dialogs/revisions.tsx
+++ b/apps/client/src/widgets/dialogs/revisions.tsx
@@ -305,9 +305,9 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: { noteConte
const diff = diffWords(noteContent, itemContent);
diffHtml = diff.map(part => {
if (part.added) {
- return `${utils.escapeHtml(part.value)}`;
+ return `${utils.escapeHtml(part.value)}`;
} else if (part.removed) {
- return `${utils.escapeHtml(part.value)}`;
+ return `${utils.escapeHtml(part.value)}`;
} else {
return utils.escapeHtml(part.value);
}
From c60c738c7e2ac153631efc862c160683b7c8cb65 Mon Sep 17 00:00:00 2001
From: SiriusXT <1160925501@qq.com>
Date: Thu, 4 Sep 2025 21:34:13 +0800
Subject: [PATCH 4/5] feat: show source diff between note and revision
---
apps/client/src/stylesheets/style.css | 10 ++
.../src/translations/en/translation.json | 8 +-
apps/client/src/widgets/dialogs/revisions.tsx | 113 +++++++++++-------
3 files changed, 81 insertions(+), 50 deletions(-)
diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css
index 2aefbbc015..98b012ea54 100644
--- a/apps/client/src/stylesheets/style.css
+++ b/apps/client/src/stylesheets/style.css
@@ -2375,4 +2375,14 @@ footer.webview-footer button {
max-width: 25vw;
overflow: hidden;
text-overflow: ellipsis;
+}
+
+
+.revision-diff-added {
+ background: rgba(100, 200, 100, 0.5);
+}
+
+.revision-diff-removed {
+ background: rgba(255, 100, 100, 0.5);
+ text-decoration: line-through;
}
\ No newline at end of file
diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json
index 1fb32af952..9d7fff76f5 100644
--- a/apps/client/src/translations/en/translation.json
+++ b/apps/client/src/translations/en/translation.json
@@ -263,10 +263,10 @@
"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_on": "Show diff",
+ "diff_off": "Show content",
+ "diff_on_hint": "Click to show note source diff",
+ "diff_off_hint": "Click to show note 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",
diff --git a/apps/client/src/widgets/dialogs/revisions.tsx b/apps/client/src/widgets/dialogs/revisions.tsx
index af64e2d84a..65c7dfd2c6 100644
--- a/apps/client/src/widgets/dialogs/revisions.tsx
+++ b/apps/client/src/widgets/dialogs/revisions.tsx
@@ -7,6 +7,7 @@ import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
import Button from "../react/Button";
+import FormToggle from "../react/FormToggle";
import Modal from "../react/Modal";
import FormList, { FormListItem } from "../react/FormList";
import utils from "../../services/utils";
@@ -59,17 +60,36 @@ export default function RevisionsDialog() {
helpPageId="vZWERwf8U3nx"
bodyStyle={{ display: "flex", height: "80vh" }}
header={
- (!!revisions?.length && {
- const text = t("revisions.confirm_delete_all");
+ !!revisions?.length && (
+ <>
+ {["text", "code", "mermaid"].includes(currentRevision?.type ?? "") && (
+ setShowDiff(newValue)}
+ switchOnName={t("revisions.diff_on")}
+ switchOffName={t("revisions.diff_off")}
+ switchOnTooltip={t("revisions.diff_on_hint")}
+ switchOffTooltip={t("revisions.diff_off_hint")}
+ />
+ )}
+
+ {
+ const text = t("revisions.confirm_delete_all");
- if (note && await dialog.confirm(text)) {
- await server.remove(`notes/${note.noteId}/revisions`);
- setRevisions([]);
- setCurrentRevision(undefined);
- toast.showMessage(t("revisions.revisions_deleted"));
- }
- }}/>)
+ if (note && await dialog.confirm(text)) {
+ await server.remove(`notes/${note.noteId}/revisions`);
+ setRevisions([]);
+ setCurrentRevision(undefined);
+ toast.showMessage(t("revisions.revisions_deleted"));
+ }
+ }}
+ />
+ >
+ )
}
footer={ }
footerStyle={{ paddingTop: 0, paddingBottom: 0 }}
@@ -103,10 +123,9 @@ export default function RevisionsDialog() {
}}>
{
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
@@ -131,12 +150,11 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
);
}
-function RevisionPreview({noteContent, revisionItem, setShown, showDiff, setShowDiff, onRevisionDeleted }: {
+function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevisionDeleted }: {
noteContent?: string,
- revisionItem?: RevisionItem,
- setShown: Dispatch>,
+ revisionItem?: RevisionItem,
showDiff: boolean,
- setShowDiff: Dispatch>,
+ setShown: Dispatch>,
onRevisionDeleted?: () => void
}) {
const [ fullRevision, setFullRevision ] = useState();
@@ -156,17 +174,6 @@ function RevisionPreview({noteContent, revisionItem, setShown, showDiff, setShow
{(revisionItem &&
{(!revisionItem.isProtected || protected_session_holder.isProtectedSessionAvailable()) &&
<>
- {["text", "code", "mermaid"].includes(revisionItem.type) && (
- {
- setShowDiff(!showDiff);
- }}
- />
- )}
-
}
-function RevisionContentDiff({ noteContent, itemContent, itemType }: { noteContent?: string, itemContent: string | Buffer | undefined, itemType: string }) {
- let diffHtml: string;
+function RevisionContentDiff({ noteContent, itemContent, itemType }: {
+ noteContent?: string,
+ itemContent: string | Buffer | undefined,
+ itemType: string
+}) {
+ const contentRef = useRef(null);
- if (noteContent && typeof itemContent === "string") {
- if (itemType === "text") {
- noteContent = utils.formatHtml(noteContent);
- itemContent = utils.formatHtml(itemContent);
+ useEffect(() => {
+ if (!noteContent || typeof itemContent !== "string") {
+ if (contentRef.current) {
+ contentRef.current.textContent = t("revisions.diff_not_available");
+ }
+ return;
}
- const diff = diffWords(noteContent, itemContent);
- diffHtml = diff.map(part => {
+
+ let processedNoteContent = noteContent;
+ let processedItemContent = itemContent;
+
+ if (itemType === "text") {
+ processedNoteContent = utils.formatHtml(noteContent);
+ processedItemContent = utils.formatHtml(itemContent);
+ }
+
+ const diff = diffWords(processedNoteContent, processedItemContent);
+ const diffHtml = diff.map(part => {
if (part.added) {
- return `${utils.escapeHtml(part.value)}`;
+ return `${utils.escapeHtml(part.value)}`;
} else if (part.removed) {
- return `${utils.escapeHtml(part.value)}`;
+ return `${utils.escapeHtml(part.value)}`;
} else {
return utils.escapeHtml(part.value);
}
}).join("");
- } else {
- return <>{t("revisions.diff_not_available")}>
- }
-
- return (
-
- );
+
+ if (contentRef.current) {
+ contentRef.current.innerHTML = diffHtml;
+ }
+ }, [noteContent, itemContent, itemType]);
+
+ return ;
}
function RevisionFooter({ note }: { note?: FNote }) {
From 5e572a8c6afa2471237027bfb64762f5a25bfe40 Mon Sep 17 00:00:00 2001
From: SiriusXT <1160925501@qq.com>
Date: Thu, 4 Sep 2025 22:07:04 +0800
Subject: [PATCH 5/5] fix: remove unnecessary line breaks
---
apps/client/src/stylesheets/style.css | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css
index 98b012ea54..e20c63ee0d 100644
--- a/apps/client/src/stylesheets/style.css
+++ b/apps/client/src/stylesheets/style.css
@@ -2377,7 +2377,6 @@ footer.webview-footer button {
text-overflow: ellipsis;
}
-
.revision-diff-added {
background: rgba(100, 200, 100, 0.5);
}