From 347da8abdecddaf997915c01ec76d9274ac5c0e3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 08:16:26 +0300 Subject: [PATCH 1/9] fix(collections/list): excessive spacing in empty note content (closes #7319) --- .../src/widgets/collections/legacy/ListOrGridView.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); }) From 94d62f810a5748b6dececf7b78561a242e084da2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:06:13 +0300 Subject: [PATCH 2/9] fix(share): heading and navigation not supporting CJK (closes #6430) --- apps/server/src/services/utils.ts | 8 ++++++++ apps/server/src/share/routes.ts | 5 +++-- packages/share-theme/src/templates/page.ejs | 1 - packages/share-theme/src/templates/toc_item.ejs | 1 - 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index ca6b809bb..75d6b564c 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) { } } +export function slugify(text: string) { + return text + .normalize("NFKD") // handles accents like é → e + .toLowerCase() + .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // keep Unicode letters/numbers + .replace(/(^-|-$)+/g, ""); // trim leading/trailing dashes +} + export default { compareVersions, crash, diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index 37162ea28..f8bce49b0 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -14,7 +14,7 @@ import log from "../services/log.js"; import type SNote from "./shaca/entities/snote.js"; import type SBranch from "./shaca/entities/sbranch.js"; import type SAttachment from "./shaca/entities/sattachment.js"; -import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; +import utils, { isDev, safeExtractMessageAndStackFromError, slugify } from "../services/utils.js"; import options from "../services/options.js"; import { t } from "i18next"; import ejs from "ejs"; @@ -175,7 +175,8 @@ function register(router: Router) { appPath: isDev ? appPath : `../${appPath}`, showLoginInShareTheme, t, - isDev + isDev, + slugify }; let useDefaultView = true; diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index cc96cc4ca..0a227a32c 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -90,7 +90,6 @@ 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]}`); 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..726ca4cca 100644 --- a/packages/share-theme/src/templates/toc_item.ejs +++ b/packages/share-theme/src/templates/toc_item.ejs @@ -1,5 +1,4 @@ <% -const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-"); const slug = slugify(entry.name); %> From d2b6014b499f774c4edc9e4d015d571736ee90d9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:19:54 +0300 Subject: [PATCH 3/9] fix(share): HTML tags displayed escaped in headings --- apps/server/src/services/utils.ts | 3 ++- apps/server/src/share/routes.ts | 4 ++-- packages/share-theme/src/templates/page.ejs | 3 ++- packages/share-theme/src/templates/toc_item.ejs | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 75d6b564c..8a4d7c7aa 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -497,7 +497,7 @@ export function formatSize(size: number | null | undefined) { } } -export function slugify(text: string) { +function slugify(text: string) { return text .normalize("NFKD") // handles accents like é → e .toLowerCase() @@ -540,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 f8bce49b0..77f542ba2 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -14,7 +14,7 @@ import log from "../services/log.js"; import type SNote from "./shaca/entities/snote.js"; import type SBranch from "./shaca/entities/sbranch.js"; import type SAttachment from "./shaca/entities/sattachment.js"; -import utils, { isDev, safeExtractMessageAndStackFromError, slugify } from "../services/utils.js"; +import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/utils.js"; import options from "../services/options.js"; import { t } from "i18next"; import ejs from "ejs"; @@ -176,7 +176,7 @@ function register(router: Router) { showLoginInShareTheme, t, isDev, - slugify + utils }; let useDefaultView = true; diff --git a/packages/share-theme/src/templates/page.ejs b/packages/share-theme/src/templates/page.ejs index 0a227a32c..2fd07c8a7 100644 --- a/packages/share-theme/src/templates/page.ejs +++ b/packages/share-theme/src/templates/page.ejs @@ -91,7 +91,8 @@ const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark"; const headingRe = /()(.+?)(<\/h[1-6]>)/g; const headingMatches = [...content.matchAll(headingRe)]; 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 726ca4cca..4346fe55a 100644 --- a/packages/share-theme/src/templates/toc_item.ejs +++ b/packages/share-theme/src/templates/toc_item.ejs @@ -1,11 +1,11 @@ <% -const slug = slugify(entry.name); +const strippedName = utils.stripTags(entry.name); +const slug = utils.slugify(strippedName); %> -
  • - <%= entry.name %> + <%= strippedName %> <% if (entry.children.length) { %> From 0fa1c0f5c40870d178f145e74e516fc165db0299 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:26:15 +0300 Subject: [PATCH 4/9] chore(share): improve slugification to strip diacritics cleanly --- apps/server/src/services/utils.spec.ts | 30 ++++++++++++++++++++++++++ apps/server/src/services/utils.ts | 3 ++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts index 6e027b7bd..b7e57b441 100644 --- a/apps/server/src/services/utils.spec.ts +++ b/apps/server/src/services/utils.spec.ts @@ -681,3 +681,33 @@ 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); + }); + + it("removes diacritic marks from characters", () => { + const testString = "Café naïve façade jalapeño"; + const expectedSlug = "cafe-naive-facade-jalapeno"; + 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 8a4d7c7aa..cdaa9d719 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -499,7 +499,8 @@ export function formatSize(size: number | null | undefined) { function slugify(text: string) { return text - .normalize("NFKD") // handles accents like é → e + .normalize("NFKD") // decompose accents + .replace(/\p{Mark}/gu, "") // remove diacritics cleanly .toLowerCase() .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // keep Unicode letters/numbers .replace(/(^-|-$)+/g, ""); // trim leading/trailing dashes From aae90ede192b5e658d32b870f375d49a52c3ba41 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:29:47 +0300 Subject: [PATCH 5/9] chore(share): keep diacritics in slug instead of stripping them in --- apps/server/src/services/utils.spec.ts | 5 +++-- apps/server/src/services/utils.ts | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts index b7e57b441..e815de3f8 100644 --- a/apps/server/src/services/utils.spec.ts +++ b/apps/server/src/services/utils.spec.ts @@ -704,9 +704,10 @@ describe("#slugify", () => { expect(result).toBe(expectedSlug); }); - it("removes diacritic marks from characters", () => { + // preserves diacritic marks + it("preserves diacritic marks", () => { const testString = "Café naïve façade jalapeño"; - const expectedSlug = "cafe-naive-facade-jalapeno"; + 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 cdaa9d719..6d567f15a 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -499,11 +499,10 @@ export function formatSize(size: number | null | undefined) { function slugify(text: string) { return text - .normalize("NFKD") // decompose accents - .replace(/\p{Mark}/gu, "") // remove diacritics cleanly + .normalize("NFC") // keep composed form, preserves accents .toLowerCase() - .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // keep Unicode letters/numbers - .replace(/(^-|-$)+/g, ""); // trim leading/trailing dashes + .replace(/[^\p{Letter}\p{Number}]+/gu, "-") // replace non-letter/number with "-" + .replace(/(^-|-$)+/g, ""); // trim dashes } export default { From af95d85b734831217ff9fd5608a27e7f17a1e2df Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 14:43:04 +0300 Subject: [PATCH 6/9] chore(client): disable import/export for help notes --- apps/client/src/widgets/ribbon/NoteActions.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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])} /> - + ); } From 5d0669b4640e4209adbcf5da8f542356c721889b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 16:11:05 +0300 Subject: [PATCH 7/9] fix(canvas): images appearing muted in dark mode (closes #5708) --- apps/client/src/stylesheets/style.css | 4 ++++ 1 file changed, 4 insertions(+) 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 From e94b5ac07abd79cb62ad5206b44f81bf3dbc5881 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 16:42:27 +0300 Subject: [PATCH 8/9] fix(server): edited notes listing automatically generated hidden notes (closes #5683) --- apps/server/src/routes/api/revisions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From f95082ccdb17c0b9145aa70f871098460e6029f7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 23 Oct 2025 16:57:48 +0300 Subject: [PATCH 9/9] fix(server): calendar tooltip positioning (closes #5675) --- apps/client/src/widgets/buttons/right_dropdown_button.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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();