diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css
index fd8383130..7f67e4827 100644
--- a/apps/client/src/stylesheets/style.css
+++ b/apps/client/src/stylesheets/style.css
@@ -2432,4 +2432,8 @@ iframe.print-iframe {
bottom: 0;
width: 0;
height: 0;
+}
+
+.excalidraw.theme--dark canvas {
+ --theme-filter: invert(100%) hue-rotate(180deg);
}
\ No newline at end of file
diff --git a/apps/client/src/widgets/buttons/right_dropdown_button.ts b/apps/client/src/widgets/buttons/right_dropdown_button.ts
index 6d896eae2..7c43f14af 100644
--- a/apps/client/src/widgets/buttons/right_dropdown_button.ts
+++ b/apps/client/src/widgets/buttons/right_dropdown_button.ts
@@ -47,8 +47,9 @@ export default class RightDropdownButtonWidget extends BasicWidget {
}
});
- this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
- this.tooltip = new Tooltip(this.$tooltip[0], {
+ this.$widget.attr("title", this.title);
+ this.tooltip = Tooltip.getOrCreateInstance(this.$widget[0], {
+ trigger: "hover",
placement: handleRightToLeftPlacement(this.settings.titlePlacement),
fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ]
});
@@ -56,9 +57,7 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.$widget
.find(".right-dropdown-button")
.addClass(this.iconClass)
- .on("click", () => this.tooltip.hide())
- .on("mouseenter", () => this.tooltip.show())
- .on("mouseleave", () => this.tooltip.hide());
+ .on("click", () => this.tooltip.hide());
this.$widget.on("show.bs.dropdown", async () => {
await this.dropdownShown();
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index d29d7e275..2b5d1bdd0 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -141,7 +141,11 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
})
.then(({ $renderedContent, type }) => {
if (!contentRef.current) return;
- contentRef.current.replaceChildren(...$renderedContent);
+ if ($renderedContent[0].innerHTML) {
+ contentRef.current.replaceChildren(...$renderedContent);
+ } else {
+ contentRef.current.replaceChildren();
+ }
contentRef.current.classList.add(`type-${type}`);
highlightSearch(contentRef.current);
})
diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx
index 14adc6b4b..1c1c17502 100644
--- a/apps/client/src/widgets/ribbon/NoteActions.tsx
+++ b/apps/client/src/widgets/ribbon/NoteActions.tsx
@@ -46,7 +46,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const parentComponent = useContext(ParentComponent);
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
- const isInOptions = note.noteId.startsWith("_options");
+ const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation");
const isElectron = getIsElectron();
const isMac = getIsMac();
@@ -69,10 +69,10 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath,
defaultType: "single"
@@ -84,14 +84,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
-
+
branches.deleteNotes([note.getParentBranches()[0].branchId])}
/>
-
+
);
}
diff --git a/apps/server/src/routes/api/revisions.ts b/apps/server/src/routes/api/revisions.ts
index 055d0b75e..d126558f0 100644
--- a/apps/server/src/routes/api/revisions.ts
+++ b/apps/server/src/routes/api/revisions.ts
@@ -152,14 +152,14 @@ function restoreRevision(req: Request) {
}
function getEditedNotesOnDate(req: Request) {
- const noteIds = sql.getColumn(
- `
+ const noteIds = sql.getColumn(/*sql*/`\
SELECT notes.*
FROM notes
WHERE noteId IN (
SELECT noteId FROM notes
- WHERE notes.dateCreated LIKE :date
- OR notes.dateModified LIKE :date
+ WHERE
+ (notes.dateCreated LIKE :date OR notes.dateModified LIKE :date)
+ AND (noteId NOT LIKE '_%')
UNION ALL
SELECT noteId FROM revisions
WHERE revisions.dateLastEdited LIKE :date
diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts
index 6e027b7bd..e815de3f8 100644
--- a/apps/server/src/services/utils.spec.ts
+++ b/apps/server/src/services/utils.spec.ts
@@ -681,3 +681,34 @@ describe("#normalizeCustomHandlerPattern", () => {
});
});
});
+
+describe("#slugify", () => {
+ it("should return a slugified string", () => {
+ const testString = "This is a Test String! With unicode & Special #Chars.";
+ const expectedSlug = "this-is-a-test-string-with-unicode-special-chars";
+ const result = utils.slugify(testString);
+ expect(result).toBe(expectedSlug);
+ });
+
+ it("supports CJK characters without alteration", () => {
+ const testString = "测试中文字符";
+ const expectedSlug = "测试中文字符";
+ const result = utils.slugify(testString);
+ expect(result).toBe(expectedSlug);
+ });
+
+ it("supports Cyrillic characters without alteration", () => {
+ const testString = "Тестирование кириллических символов";
+ const expectedSlug = "тестирование-кириллических-символов";
+ const result = utils.slugify(testString);
+ expect(result).toBe(expectedSlug);
+ });
+
+ // preserves diacritic marks
+ it("preserves diacritic marks", () => {
+ const testString = "Café naïve façade jalapeño";
+ const expectedSlug = "café-naïve-façade-jalapeño";
+ const result = utils.slugify(testString);
+ expect(result).toBe(expectedSlug);
+ });
+});
diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts
index ca6b809bb..6d567f15a 100644
--- a/apps/server/src/services/utils.ts
+++ b/apps/server/src/services/utils.ts
@@ -497,6 +497,14 @@ export function formatSize(size: number | null | undefined) {
}
}
+function slugify(text: string) {
+ return text
+ .normalize("NFC") // keep composed form, preserves accents
+ .toLowerCase()
+ .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // replace non-letter/number with "-"
+ .replace(/(^-|-$)+/g, ""); // trim dashes
+}
+
export default {
compareVersions,
crash,
@@ -532,6 +540,7 @@ export default {
safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier,
stripTags,
+ slugify,
timeLimit,
toBase64,
toMap,
diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts
index 37162ea28..77f542ba2 100644
--- a/apps/server/src/share/routes.ts
+++ b/apps/server/src/share/routes.ts
@@ -175,7 +175,8 @@ function register(router: Router) {
appPath: isDev ? appPath : `../${appPath}`,
showLoginInShareTheme,
t,
- isDev
+ isDev,
+ utils
};
let useDefaultView = true;
diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs
index cc96cc4ca..2fd07c8a7 100644
--- a/packages/share-theme/src/templates/page.ejs
+++ b/packages/share-theme/src/templates/page.ejs
@@ -90,9 +90,9 @@ const currentTheme = note.getLabel("shareTheme") === "light" ? "light" : "dark";
const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark";
const headingRe = /()(.+?)(<\/h[1-6]>)/g;
const headingMatches = [...content.matchAll(headingRe)];
-const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-");
content = content.replaceAll(headingRe, (...match) => {
- match[0] = match[0].replace(match[3], `#${match[3]}`);
+ const slug = utils.slugify(utils.stripTags(match[2]));
+ match[0] = match[0].replace(match[3], `#${match[3]}`);
return match[0];
});
%>
diff --git a/packages/share-theme/src/templates/toc_item.ejs b/packages/share-theme/src/templates/toc_item.ejs
index b18b4a1a6..4346fe55a 100644
--- a/packages/share-theme/src/templates/toc_item.ejs
+++ b/packages/share-theme/src/templates/toc_item.ejs
@@ -1,12 +1,11 @@
<%
-const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-");
-const slug = slugify(entry.name);
+const strippedName = utils.stripTags(entry.name);
+const slug = utils.slugify(strippedName);
%>
-
- <%= entry.name %>
+ <%= strippedName %>
<% if (entry.children.length) { %>