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) { %>