diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 77fec13668..a8f4f567fe 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("
([\\s\\S]+?)?"), 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/stylesheets/style.css b/apps/client/src/stylesheets/style.css
index 2aefbbc015..e20c63ee0d 100644
--- a/apps/client/src/stylesheets/style.css
+++ b/apps/client/src/stylesheets/style.css
@@ -2375,4 +2375,13 @@ 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 3bffc9e317..638501c453 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_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",
"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..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";
@@ -18,12 +19,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