From fd0f0196cc542be1fe486662ac7e33d7212d245a Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Sat, 3 May 2025 21:01:56 +0200 Subject: [PATCH 01/62] feat(server): add option to mount database read-only --- apps/server/src/services/config.ts | 6 +++++- apps/server/src/services/sql.ts | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/server/src/services/config.ts b/apps/server/src/services/config.ts index 1d7cc9dec..2089c03ce 100644 --- a/apps/server/src/services/config.ts +++ b/apps/server/src/services/config.ts @@ -21,6 +21,7 @@ export interface TriliumConfig { noAuthentication: boolean; noBackup: boolean; noDesktopIcon: boolean; + readOnly: boolean; }; Network: { host: string; @@ -62,7 +63,10 @@ const config: TriliumConfig = { envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false, noDesktopIcon: - envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false + envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false, + + readOnly: + envToBoolean(process.env.TRILIUM_GENERAL_READONLY) || iniConfig.General.readOnly || false }, Network: { diff --git a/apps/server/src/services/sql.ts b/apps/server/src/services/sql.ts index f686b0876..84ddf107b 100644 --- a/apps/server/src/services/sql.ts +++ b/apps/server/src/services/sql.ts @@ -13,18 +13,20 @@ import Database from "better-sqlite3"; import ws from "./ws.js"; import becca_loader from "../becca/becca_loader.js"; import entity_changes from "./entity_changes.js"; +import config from "./config.js"; let dbConnection: DatabaseType = buildDatabase(); let statementCache: Record = {}; function buildDatabase() { + // for integration tests, ignore the config's readOnly setting if (process.env.TRILIUM_INTEGRATION_TEST === "memory") { return buildIntegrationTestDatabase(); } else if (process.env.TRILIUM_INTEGRATION_TEST === "memory-no-store") { return new Database(":memory:"); } - return new Database(dataDir.DOCUMENT_PATH); + return new Database(dataDir.DOCUMENT_PATH, { readonly: config.General.readOnly }); } function buildIntegrationTestDatabase(dbPath?: string) { From 2427addf658f4243c3a7b89be20f1f9d2a9be5f8 Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 17:24:00 +0200 Subject: [PATCH 02/62] feat(server): override options for read-only database --- apps/server/src/routes/api/options.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index a31206404..3a0a9b06b 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -7,6 +7,7 @@ import ValidationError from "../../errors/validation_error.js"; import type { Request } from "express"; import { changeLanguage, getLocales } from "../../services/i18n.js"; import type { OptionNames } from "@triliumnext/commons"; +import config from "../../services/config.js"; // options allowed to be updated directly in the Options dialog const ALLOWED_OPTIONS = new Set([ @@ -127,6 +128,12 @@ function getOptions() { } resultMap["isPasswordSet"] = optionMap["passwordVerificationHash"] ? "true" : "false"; + // if database is read-only, disable editing in UI by setting 0 here + if (config.General.readOnly) { + resultMap["autoReadonlySizeText"] = "0"; + resultMap["autoReadonlySizeCode"] = "0"; + resultMap["databaseReadonly"] = "true"; + } return resultMap; } From 262ec45fe0bdab615e83a7adb2e956e509d264fa Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Fri, 23 May 2025 17:03:07 +0800 Subject: [PATCH 03/62] feat(math): support multi-line formula editing --- packages/ckeditor5-math/src/mathui.ts | 20 +++++++++ .../ckeditor5-math/src/ui/mainformview.ts | 6 +-- packages/ckeditor5-math/src/ui/mathview.ts | 2 +- packages/ckeditor5-math/src/utils.ts | 45 +------------------ 4 files changed, 25 insertions(+), 48 deletions(-) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index f3c86af49..cc402c1e6 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -110,6 +110,26 @@ export default class MathUI extends Plugin { cancel(); } ); + // Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line + formView.keystrokes.set('enter', (data, cancel) => { + if (!data.shiftKey) { + formView.fire('submit'); + cancel(); + } + }); + + // Allow the textarea to be resizable + formView.mathInputView.fieldView.once('render', () => { + const textarea = formView.mathInputView.fieldView.element; + if (!textarea) return; + textarea.focus(); + Object.assign(textarea.style, { + resize: 'both', + height: '100px', + minWidth: '100%', + }); + }); + return formView; } diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index af86fd77c..751c3c679 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -1,16 +1,16 @@ -import { ButtonView, createLabeledInputText, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type InputTextView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5'; +import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5'; import { IconCheck, IconCancel } from "@ckeditor/ckeditor5-icons"; import { extractDelimiters, hasDelimiters } from '../utils.js'; import MathView from './mathview.js'; import '../../theme/mathform.css'; import type { KatexOptions } from '../typings-external.js'; -class MathInputView extends LabeledFieldView { +class MathInputView extends LabeledFieldView { public value: null | string = null; public isReadOnly = false; constructor( locale: Locale ) { - super( locale, createLabeledInputText ); + super( locale, createLabeledTextarea ); } } diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index 541227e43..fab16262e 100644 --- a/packages/ckeditor5-math/src/ui/mathview.ts +++ b/packages/ckeditor5-math/src/ui/mathview.ts @@ -49,7 +49,7 @@ export default class MathView extends View { this.setTemplate( { tag: 'div', attributes: { - class: [ 'ck', 'ck-math-preview' ] + class: [ 'ck', 'ck-math-preview', 'ck-reset_all-excluded' ] } } ); } diff --git a/packages/ckeditor5-math/src/utils.ts b/packages/ckeditor5-math/src/utils.ts index 3a29d3fc2..2e120b6c2 100644 --- a/packages/ckeditor5-math/src/utils.ts +++ b/packages/ckeditor5-math/src/utils.ts @@ -94,7 +94,6 @@ export async function renderEquation( el => { renderMathJax3( equation, el, display, () => { if ( preview ) { - moveAndScaleElement( element, el ); el.style.visibility = 'visible'; } } ); @@ -115,7 +114,6 @@ export async function renderEquation( if ( preview && isMathJaxVersion2( MathJax ) ) { // eslint-disable-next-line new-cap MathJax.Hub.Queue( () => { - moveAndScaleElement( element, el ); el.style.visibility = 'visible'; } ); } @@ -139,7 +137,6 @@ export async function renderEquation( } ); } if ( preview ) { - moveAndScaleElement( element, el ); el.style.visibility = 'visible'; } } @@ -295,47 +292,7 @@ function getPreviewElement( previewEl.setAttribute( 'id', previewUid ); previewEl.classList.add( ...previewClassName ); previewEl.style.visibility = 'hidden'; - document.body.appendChild( previewEl ); - - let ticking = false; - - const renderTransformation = () => { - if ( !ticking ) { - window.requestAnimationFrame( () => { - if ( previewEl ) { - moveElement( element, previewEl ); - ticking = false; - } - } ); - - ticking = true; - } - }; - - // Create scroll listener for following - window.addEventListener( 'resize', renderTransformation ); - window.addEventListener( 'scroll', renderTransformation ); + element.appendChild( previewEl ); } return previewEl; } - -function moveAndScaleElement( parent: HTMLElement, child: HTMLElement ) { - // Move to right place - moveElement( parent, child ); - - // Scale parent element same as preview - const domRect = child.getBoundingClientRect(); - parent.style.width = domRect.width + 'px'; - parent.style.height = domRect.height + 'px'; -} - -function moveElement( parent: HTMLElement, child: HTMLElement ) { - const domRect = parent.getBoundingClientRect(); - const left = window.scrollX + domRect.left; - const top = window.scrollY + domRect.top; - child.style.position = 'absolute'; - child.style.left = left + 'px'; - child.style.top = top + 'px'; - child.style.zIndex = 'var(--ck-z-panel)'; - child.style.pointerEvents = 'none'; -} From 923316e14e344b0dadc4735cd5b4ca20c8f3a0de Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 17:24:00 +0200 Subject: [PATCH 04/62] feat(client): handle read-only database --- apps/client/src/components/note_context.ts | 4 ++++ apps/client/src/widgets/floating_buttons/edit_button.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index dd2391b67..c958512c6 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -254,6 +254,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> return false; } + if (options.is("databaseReadonly")) { + return true; + } + if (this.note.isLabelTruthy("readOnly")) { return true; } diff --git a/apps/client/src/widgets/floating_buttons/edit_button.ts b/apps/client/src/widgets/floating_buttons/edit_button.ts index 2a11f0d01..344447f31 100644 --- a/apps/client/src/widgets/floating_buttons/edit_button.ts +++ b/apps/client/src/widgets/floating_buttons/edit_button.ts @@ -6,6 +6,7 @@ import { t } from "../../services/i18n.js"; import LoadResults from "../../services/load_results.js"; import type { AttributeRow } from "../../services/load_results.js"; import FNote from "../../entities/fnote.js"; +import options from "../../services/options.js"; export default class EditButton extends OnClickButtonWidget { isEnabled(): boolean { @@ -27,6 +28,10 @@ export default class EditButton extends OnClickButtonWidget { } async refreshWithNote(note: FNote): Promise { + if (options.is("databaseReadonly")) { + this.toggleInt(false); + return; + } if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { this.toggleInt(false); } else { From 8d21b3a8c5095b1f2c9cdcaf11635648ce17dd88 Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 18:36:13 +0200 Subject: [PATCH 05/62] fix(client): read-only handling in canvas note --- apps/client/src/widgets/type_widgets/canvas.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/type_widgets/canvas.ts b/apps/client/src/widgets/type_widgets/canvas.ts index 962a7ff4a..490350686 100644 --- a/apps/client/src/widgets/type_widgets/canvas.ts +++ b/apps/client/src/widgets/type_widgets/canvas.ts @@ -3,6 +3,7 @@ import utils from "../../services/utils.js"; import linkService from "../../services/link.js"; import server from "../../services/server.js"; import type FNote from "../../entities/fnote.js"; +import options from "../../services/options.js"; import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types"; import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types"; import type { JSX } from "react"; @@ -447,6 +448,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget { } onChangeHandler() { + if (options.is("databaseReadonly")) { + return; + } // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc. // make sure only when a new element is added, we actually save something. const isNewSceneVersion = this.isNewSceneVersion(); @@ -540,7 +544,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { this.saveData(); }, onChange: () => this.onChangeHandler(), - viewModeEnabled: false, + viewModeEnabled: options.is("databaseReadonly"), zenModeEnabled: false, gridModeEnabled: false, isCollaborating: false, @@ -567,6 +571,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget { * info: sceneVersions are not incrementing. it seems to be a pseudo-random number */ isNewSceneVersion() { + if (options.is("databaseReadonly")) { + return false; + } + const sceneVersion = this.getSceneVersion(); return ( From 6f5304467a3496b0d8fbf0ce6ee4d7e2f8285a8d Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 18:36:58 +0200 Subject: [PATCH 06/62] fix(client): read-only handling of recent notes --- apps/client/src/components/note_context.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index c958512c6..074e03e4c 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -159,6 +159,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> } saveToRecentNotes(resolvedNotePath: string) { + if (options.is("databaseReadonly")) { + return; + } setTimeout(async () => { // we include the note in the recent list only if the user stayed on the note at least 5 seconds if (resolvedNotePath && resolvedNotePath === this.notePath) { From bd0cb91171c90e20919fa40cb76b28f4d96773c5 Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 18:45:52 +0200 Subject: [PATCH 07/62] feat(server): log ignored DB changes in wrapper --- apps/server/src/services/sql.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/server/src/services/sql.ts b/apps/server/src/services/sql.ts index 84ddf107b..ace408d39 100644 --- a/apps/server/src/services/sql.ts +++ b/apps/server/src/services/sql.ts @@ -210,6 +210,13 @@ function getColumn(query: string, params: Params = []): T[] { } function execute(query: string, params: Params = []): RunResult { + if (config.General.readOnly && (query.startsWith("UPDATE") || query.startsWith("INSERT") || query.startsWith("DELETE"))) { + log.error(`read-only DB ignored: ${query} with parameters ${JSON.stringify(params)}`); + return { + changes: 0, + lastInsertRowid: 0 + }; + } return wrap(query, (s) => s.run(params)) as RunResult; } From 23422731e26a27cea00f56e8e7e954653455e894 Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 18:50:32 +0200 Subject: [PATCH 08/62] fix(client): handle read-only in note tree auto-collapse --- apps/client/src/widgets/note_tree.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 9b8ab10cd..825e6d8f8 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -1172,16 +1172,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { let noneCollapsedYet = true; - this.tree.getRootNode().visit((node) => { - if (node.isExpanded() && !noteIdsToKeepExpanded.has(node.data.noteId)) { - node.setExpanded(false); + if (!options.is("databaseReadonly")) { + // can't change expanded notes when database is readonly + this.tree.getRootNode().visit((node) => { + if (node.isExpanded() && !noteIdsToKeepExpanded.has(node.data.noteId)) { + node.setExpanded(false); - if (noneCollapsedYet) { - toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity")); - noneCollapsedYet = false; + if (noneCollapsedYet) { + toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity")); + noneCollapsedYet = false; + } } - } - }, false); + }, false); + } this.filterHoistedBranch(true); }, 600 * 1000); From 5acdb698bba7a3334323a8b2a9ce6613c85b945d Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 19:54:45 +0200 Subject: [PATCH 09/62] fix(server): don't optimize database in read-only mode --- apps/server/src/services/sql_init.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/server/src/services/sql_init.ts b/apps/server/src/services/sql_init.ts index f5f7e4e48..5fb0bd573 100644 --- a/apps/server/src/services/sql_init.ts +++ b/apps/server/src/services/sql_init.ts @@ -186,6 +186,9 @@ function setDbAsInitialized() { } function optimize() { + if (config.General.readOnly) { + return; + } log.info("Optimizing database"); const start = Date.now(); From 50d045b70e77f7d1940d77fd6d638e7848336a3f Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Wed, 21 May 2025 19:54:45 +0200 Subject: [PATCH 10/62] fix(client): don't save note tab context for read-only database --- apps/client/src/components/tab_manager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index cf2876a5a..fa83470ce 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -44,6 +44,9 @@ export default class TabManager extends Component { if (!appContext.isMainWindow) { return; } + if (options.is("databaseReadonly")) { + return; + } const openNoteContexts = this.noteContexts .map((nc) => nc.getPojoState()) From 1bc2f876c239861e21f4919f3db34cb1fdf3bffa Mon Sep 17 00:00:00 2001 From: Arne Keller Date: Sun, 25 May 2025 20:30:42 +0200 Subject: [PATCH 11/62] Update Repology table in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02a9c969b..84b64afc8 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Download the binary release for your platform from the [latest release page](htt If your distribution is listed in the table below, use your distribution's package. -[![Packaging status](https://repology.org/badge/vertical-allrepos/trilium-next-desktop.svg)](https://repology.org/project/trilium-next-desktop/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable. From cb8a08d590a47b7959a770cd63887822660c4878 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 25 May 2025 22:20:02 +0300 Subject: [PATCH 12/62] chore(nx): run client server automatically --- apps/server/package.json | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/package.json b/apps/server/package.json index df48848cc..56677667e 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -115,8 +115,13 @@ "serve": { "executor": "@nx/js:node", "dependsOn": [ + { + "projects": [ "client" ], + "target": "serve" + }, "build-without-client" ], + "continuous": true, "options": { "buildTarget": "server:build-without-client:development", "runBuildTargetDependencies": false diff --git a/package.json b/package.json index c20da6d06..d31ebd33b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "server:test": "nx test server", "server:build": "nx build server", "server:coverage": "nx test server --coverage", - "server:start": "nx run-many --target=serve --projects=client,server --parallel", + "server:start": "nx run server:serve", "server:start-prod": "nx run server:start-prod", "electron:build": "nx build desktop", "chore:ci-update-nightly-version": "tsx ./scripts/update-nightly-version.ts", From b4df8f75b9fa1ce0c0044e8968fa2b3570a22ba5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 25 May 2025 23:00:53 +0300 Subject: [PATCH 13/62] fix(client/search): search broken due to highlighting --- .../widgets/view_widgets/list_or_grid_view.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/list_or_grid_view.ts b/apps/client/src/widgets/view_widgets/list_or_grid_view.ts index e01fd0c4c..54b83b971 100644 --- a/apps/client/src/widgets/view_widgets/list_or_grid_view.ts +++ b/apps/client/src/widgets/view_widgets/list_or_grid_view.ts @@ -215,8 +215,6 @@ class ListOrGridView extends ViewMode { const highlightedTokens = this.parentNote.highlightedTokens || []; if (highlightedTokens.length > 0) { - await import("mark.js"); - const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); this.highlightRegex = new RegExp(regex, "gi"); @@ -320,11 +318,10 @@ class ListOrGridView extends ViewMode { $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); if (this.highlightRegex) { - $card.find(".note-book-title").markRegExp(this.highlightRegex, { + const Mark = new (await import("mark.js")).default($card.find(".note-book-title")[0]); + Mark.markRegExp(this.highlightRegex, { element: "span", - className: "ck-find-result", - separateWordSearch: false, - caseSensitive: false + className: "ck-find-result" }); } @@ -362,11 +359,10 @@ class ListOrGridView extends ViewMode { }); if (this.highlightRegex) { - $renderedContent.markRegExp(this.highlightRegex, { + const Mark = new (await import("mark.js")).default($renderedContent[0]); + Mark.markRegExp(this.highlightRegex, { element: "span", - className: "ck-find-result", - separateWordSearch: false, - caseSensitive: false + className: "ck-find-result" }); } From 2f406aea83157107e36e350120618131f7c378f2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 25 May 2025 23:09:16 +0300 Subject: [PATCH 14/62] fix(client/search): broken search in read-only text --- apps/client/src/widgets/find.ts | 4 ++-- apps/client/src/widgets/find_in_html.ts | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/client/src/widgets/find.ts b/apps/client/src/widgets/find.ts index 0239251cb..fc8bebb88 100644 --- a/apps/client/src/widgets/find.ts +++ b/apps/client/src/widgets/find.ts @@ -248,10 +248,10 @@ export default class FindWidget extends NoteContextAwareWidget { case "code": return this.codeHandler; case "text": - return this.textHandler; - default: const readOnly = await this.noteContext?.isReadOnly(); return readOnly ? this.htmlHandler : this.textHandler; + default: + console.warn("FindWidget: Unsupported note type for find widget", this.note?.type); } } diff --git a/apps/client/src/widgets/find_in_html.ts b/apps/client/src/widgets/find_in_html.ts index fcbc35173..304bea656 100644 --- a/apps/client/src/widgets/find_in_html.ts +++ b/apps/client/src/widgets/find_in_html.ts @@ -1,6 +1,7 @@ // ck-find-result and ck-find-result_selected are the styles ck-editor // uses for highlighting matches, use the same one on CodeMirror // for consistency +import type Mark from "mark.js"; import utils from "../services/utils.js"; import type FindWidget from "./find.js"; import type { FindResult } from "./find.js"; @@ -13,6 +14,7 @@ export default class FindInHtml { private parent: FindWidget; private currentIndex: number; private $results: JQuery | null; + private mark?: Mark; constructor(parent: FindWidget) { this.parent = parent; @@ -21,21 +23,24 @@ export default class FindInHtml { } async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { - await import("mark.js"); - const $content = await this.parent?.noteContext?.getContentElement(); + if (!$content || !$content.length) { + return Promise.resolve({ totalFound: 0, currentFound: 0 }); + } + + if (!this.mark) { + this.mark = new (await import("mark.js")).default($content[0]); + } const wholeWordChar = wholeWord ? "\\b" : ""; const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi"); return new Promise((res) => { - $content?.unmark({ + this.mark!.unmark({ done: () => { - $content.markRegExp(regExp, { + this.mark!.markRegExp(regExp, { element: "span", className: FIND_RESULT_CSS_CLASSNAME, - separateWordSearch: false, - caseSensitive: matchCase, done: async () => { this.$results = $content.find(`.${FIND_RESULT_CSS_CLASSNAME}`); const scrollingContainer = $content[0].closest('.scrolling-container'); @@ -73,10 +78,7 @@ export default class FindInHtml { } async findBoxClosed(totalFound: number, currentFound: number) { - const $content = await this.parent?.noteContext?.getContentElement(); - if (typeof $content?.unmark === 'function') { - $content.unmark(); - } + this.mark?.unmark(); } async jumpTo() { From bab8d6f12a99ad6664075dc65bb87bce05788589 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 25 May 2025 23:12:54 +0300 Subject: [PATCH 15/62] refactor(client): remove unused type --- apps/client/src/types.d.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index 9970b99d3..113b94d76 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -93,16 +93,6 @@ declare global { getSelectedExternalLink(): string | undefined; setSelectedExternalLink(externalLink: string | null | undefined); setNote(noteId: string); - markRegExp(regex: RegExp, opts: { - element: string; - className: string; - separateWordSearch: boolean; - caseSensitive: boolean; - done?: () => void; - }); - unmark(opts?: { - done: () => void; - }); } interface JQueryStatic { From ff990839cb54893a904a9102a0df5431155b58ae Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Sun, 25 May 2025 13:43:00 -0700 Subject: [PATCH 16/62] fix broken link to config-sample.ini --- .../Advanced Usage/Configuration (config.ini or e.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md b/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md index b616f4097..56a164621 100644 --- a/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md +++ b/docs/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.md @@ -1,5 +1,5 @@ # Configuration (config.ini or environment variables) -Trilium supports configuration via a file named `config.ini` and environment variables. Please review the file named [config-sample.ini](https://github.com/TriliumNext/Notes/blob/develop/config-sample.ini) in the [Notes](https://github.com/TriliumNext/Notes) repository to see what values are supported. +Trilium supports configuration via a file named `config.ini` and environment variables. Please review the file named [config-sample.ini](https://github.com/TriliumNext/Notes/blob/develop/apps/server/src/assets/config-sample.ini) in the [Notes](https://github.com/TriliumNext/Notes) repository to see what values are supported. You can provide the same values via environment variables instead of the `config.ini` file, and these environment variables use the following format: @@ -27,4 +27,4 @@ The code will: 1. First load the `config.ini` file as before 2. Then scan all environment variables for ones starting with `TRILIUM_` 3. Parse these variables into section/key pairs -4. Merge them with the config from the file, with environment variables taking precedence \ No newline at end of file +4. Merge them with the config from the file, with environment variables taking precedence From 3d22a64b5aea8672d226bcb7a6dec1649a49454e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 08:18:54 +0300 Subject: [PATCH 17/62] chore(docs): update public documentation as well --- .../Advanced Usage/Configuration (config.ini or e.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html index a77f87038..923cbdf03 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Configuration (config.ini or e.html @@ -1,5 +1,5 @@

Trilium supports configuration via a file named config.ini and - environment variables. Please review the file named config-sample.ini in + environment variables. Please review the file named config-sample.ini in the Notes repository to see what values are supported.

You can provide the same values via environment variables instead of the config.ini file, From 3091eb831a0bcde1a6be10a04343dc8d8acd2028 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 08:20:39 +0300 Subject: [PATCH 18/62] fix(client): cannot build due to import error in some circumstances --- apps/client/{vite.config.ts => vite.config.mts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/client/{vite.config.ts => vite.config.mts} (100%) diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.mts similarity index 100% rename from apps/client/vite.config.ts rename to apps/client/vite.config.mts From bab679fd2a780d8a437ea10ed39f08c8c1dae7f4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 08:20:48 +0300 Subject: [PATCH 19/62] fix(edit-docs): not working under NixOS --- apps/edit-docs/src/electron-docs-main.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/edit-docs/src/electron-docs-main.ts b/apps/edit-docs/src/electron-docs-main.ts index 7f293af49..e1d5c0cad 100644 --- a/apps/edit-docs/src/electron-docs-main.ts +++ b/apps/edit-docs/src/electron-docs-main.ts @@ -33,24 +33,24 @@ if (!DOCS_ROOT || !USER_GUIDE_ROOT) { const NOTE_MAPPINGS: NoteMapping[] = [ { rootNoteId: "pOsGYCXsbNQG", - path: path.join(DOCS_ROOT, "User Guide"), + path: path.join(__dirname, DOCS_ROOT, "User Guide"), format: "markdown" }, { rootNoteId: "pOsGYCXsbNQG", - path: USER_GUIDE_ROOT, + path: path.join(__dirname, USER_GUIDE_ROOT), format: "html", ignoredFiles: ["index.html", "navigation.html", "style.css", "User Guide.html"], exportOnly: true }, { rootNoteId: "jdjRLhLV3TtI", - path: path.join(DOCS_ROOT, "Developer Guide"), + path: path.join(__dirname, DOCS_ROOT, "Developer Guide"), format: "markdown" }, { rootNoteId: "hD3V4hiu2VW4", - path: path.join(DOCS_ROOT, "Release Notes"), + path: path.join(__dirname, DOCS_ROOT, "Release Notes"), format: "markdown" } ]; From a1d5719fe06d41b3c8b480b0bcbd94c22e4c528d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 09:17:35 +0300 Subject: [PATCH 20/62] feat(ckeditor5): create an empty toolbar for code blocks --- packages/ckeditor5/src/plugins.ts | 4 +- .../src/plugins/code_block_toolbar.ts | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/ckeditor5/src/plugins/code_block_toolbar.ts diff --git a/packages/ckeditor5/src/plugins.ts b/packages/ckeditor5/src/plugins.ts index 19fbc748e..c53dfb924 100644 --- a/packages/ckeditor5/src/plugins.ts +++ b/packages/ckeditor5/src/plugins.ts @@ -23,6 +23,7 @@ import "@triliumnext/ckeditor5-mermaid/index.css"; import "@triliumnext/ckeditor5-admonition/index.css"; import "@triliumnext/ckeditor5-footnotes/index.css"; import "@triliumnext/ckeditor5-math/index.css"; +import CodeBlockToolbar from "./plugins/code_block_toolbar.js"; /** * Plugins that are specific to Trilium and not part of the CKEditor 5 core, included in both text editors but not in the attribute editor. @@ -38,7 +39,8 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [ MarkdownImportPlugin, IncludeNote, Uploadfileplugin, - SyntaxHighlighting + SyntaxHighlighting, + CodeBlockToolbar ]; /** diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts new file mode 100644 index 000000000..58e322381 --- /dev/null +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -0,0 +1,45 @@ +import { CodeBlock, Plugin, Position, ViewDocumentFragment, WidgetToolbarRepository, type Node, type ViewNode } from "ckeditor5"; + +export default class CodeBlockToolbar extends Plugin { + + static get requires() { + return [ WidgetToolbarRepository, CodeBlock ] as const; + } + + afterInit() { + const editor = this.editor; + const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository); + + widgetToolbarRepository.register("codeblock", { + items: [ + { + label: "Hello", + items: [ + { + label: "world", + items: [] + } + ] + } + ], + getRelatedElement(selection) { + const selectionPosition = selection.getFirstPosition(); + if (!selectionPosition) { + return null; + } + + let parent: ViewNode | ViewDocumentFragment | null = selectionPosition.parent; + while (parent) { + if (parent.is("element", "pre")) { + return parent; + } + + parent = parent.parent; + } + + return null; + } + }); + } + +} From 178ce310643ee122991bf8dcfb66a27a3fd1f86a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 10:07:52 +0300 Subject: [PATCH 21/62] feat(ckeditor5/codeblock): add language dropdown --- .../src/plugins/code_block_toolbar.ts | 103 ++++++++++++++++-- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 58e322381..259f98bc4 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -1,4 +1,4 @@ -import { CodeBlock, Plugin, Position, ViewDocumentFragment, WidgetToolbarRepository, type Node, type ViewNode } from "ckeditor5"; +import { Editor, CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode, type ListDropdownButtonDefinition, Collection, type CodeBlockCommand, ViewModel, createDropdown, addListToDropdown, DropdownButtonView } from "ckeditor5"; export default class CodeBlockToolbar extends Plugin { @@ -6,21 +6,44 @@ export default class CodeBlockToolbar extends Plugin { return [ WidgetToolbarRepository, CodeBlock ] as const; } + public init(): void { + const editor = this.editor; + const componentFactory = editor.ui.componentFactory; + + const normalizedLanguageDefs = this._getNormalizedAndLocalizedLanguageDefinitions(editor); + const itemDefinitions = this._getLanguageListItemDefinitions(normalizedLanguageDefs); + const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; + + componentFactory.add("codeBlockDropdown", locale => { + const dropdownView = createDropdown(this.editor.locale, DropdownButtonView); + dropdownView.buttonView.set({ + withText: true + }); + dropdownView.bind( 'isEnabled' ).to( command, 'value', value => !!value ); + dropdownView.buttonView.bind( 'label' ).to( command, 'value', (value) => { + const itemDefinition = normalizedLanguageDefs.find((def) => def.language === value); + return itemDefinition?.label; + }); + dropdownView.on( 'execute', evt => { + editor.execute( 'codeBlock', { + language: ( evt.source as any )._codeBlockLanguage, + forceValue: true + }); + + editor.editing.view.focus(); + }); + addListToDropdown(dropdownView, itemDefinitions); + return dropdownView; + }); + } + afterInit() { const editor = this.editor; const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository); widgetToolbarRepository.register("codeblock", { items: [ - { - label: "Hello", - items: [ - { - label: "world", - items: [] - } - ] - } + "codeBlockDropdown" ], getRelatedElement(selection) { const selectionPosition = selection.getFirstPosition(); @@ -42,4 +65,64 @@ export default class CodeBlockToolbar extends Plugin { }); } + // Adapted from packages/ckeditor5-code-block/src/codeblockui.ts + private _getLanguageListItemDefinitions( + normalizedLanguageDefs: Array + ): Collection { + const editor = this.editor; + const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; + const itemDefinitions = new Collection(); + + for ( const languageDef of normalizedLanguageDefs ) { + const definition: ListDropdownButtonDefinition = { + type: 'button', + model: new ViewModel( { + _codeBlockLanguage: languageDef.language, + label: languageDef.label, + role: 'menuitemradio', + withText: true + } ) + }; + + definition.model.bind( 'isOn' ).to( command, 'value', value => { + return value === definition.model._codeBlockLanguage; + } ); + + itemDefinitions.add( definition ); + } + + return itemDefinitions; + } + + // Adapted from packages/ckeditor5-code-block/src/utils.ts + private _getNormalizedAndLocalizedLanguageDefinitions(editor: Editor) { + const languageDefs = editor.config.get( 'codeBlock.languages' ) as Array; + for ( const def of languageDefs ) { + if ( def.class === undefined ) { + def.class = `language-${ def.language }`; + } + } + return languageDefs; + } + +} + +interface CodeBlockLanguageDefinition { + + /** + * The name of the language that will be stored in the model attribute. Also, when `class` + * is not specified, it will be used to create the CSS class associated with the language (prefixed by "language-"). + */ + language: string; + + /** + * The human–readable label associated with the language and displayed in the UI. + */ + label: string; + + /** + * The CSS class associated with the language. When not specified the `language` + * property is used to create a class prefixed by "language-". + */ + class?: string; } From 751ed0b5d4706faeae53efb9c647f7571e3b3e2b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 10:53:12 +0300 Subject: [PATCH 22/62] refactor(ckeditor5/codeblock): split dropdown into own plugin --- packages/ckeditor5/src/plugins.ts | 2 + .../plugins/code_block_language_dropdown.ts | 103 ++++++++++++++++++ .../src/plugins/code_block_toolbar.ts | 96 +--------------- 3 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 packages/ckeditor5/src/plugins/code_block_language_dropdown.ts diff --git a/packages/ckeditor5/src/plugins.ts b/packages/ckeditor5/src/plugins.ts index c53dfb924..29abde14c 100644 --- a/packages/ckeditor5/src/plugins.ts +++ b/packages/ckeditor5/src/plugins.ts @@ -24,6 +24,7 @@ import "@triliumnext/ckeditor5-admonition/index.css"; import "@triliumnext/ckeditor5-footnotes/index.css"; import "@triliumnext/ckeditor5-math/index.css"; import CodeBlockToolbar from "./plugins/code_block_toolbar.js"; +import CodeBlockLanguageDropdown from "./plugins/code_block_language_dropdown.js"; /** * Plugins that are specific to Trilium and not part of the CKEditor 5 core, included in both text editors but not in the attribute editor. @@ -40,6 +41,7 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [ IncludeNote, Uploadfileplugin, SyntaxHighlighting, + CodeBlockLanguageDropdown, CodeBlockToolbar ]; diff --git a/packages/ckeditor5/src/plugins/code_block_language_dropdown.ts b/packages/ckeditor5/src/plugins/code_block_language_dropdown.ts new file mode 100644 index 000000000..7b384a784 --- /dev/null +++ b/packages/ckeditor5/src/plugins/code_block_language_dropdown.ts @@ -0,0 +1,103 @@ +import { Editor, CodeBlock, Plugin, type ListDropdownButtonDefinition, Collection, type CodeBlockCommand, ViewModel, createDropdown, addListToDropdown, DropdownButtonView } from "ckeditor5"; + +/** + * Toolbar item which displays the list of languages in a dropdown, with the text visible (similar to the headings switcher), as opposed to the default split button implementation. + */ +export default class CodeBlockLanguageDropdown extends Plugin { + + static get requires() { + return [ CodeBlock ]; + } + + public init() { + const editor = this.editor; + const componentFactory = editor.ui.componentFactory; + + const normalizedLanguageDefs = this._getNormalizedAndLocalizedLanguageDefinitions(editor); + const itemDefinitions = this._getLanguageListItemDefinitions(normalizedLanguageDefs); + const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; + + componentFactory.add("codeBlockDropdown", locale => { + const dropdownView = createDropdown(this.editor.locale, DropdownButtonView); + dropdownView.buttonView.set({ + withText: true + }); + dropdownView.bind( 'isEnabled' ).to( command, 'value', value => !!value ); + dropdownView.buttonView.bind( 'label' ).to( command, 'value', (value) => { + const itemDefinition = normalizedLanguageDefs.find((def) => def.language === value); + return itemDefinition?.label; + }); + dropdownView.on( 'execute', evt => { + editor.execute( 'codeBlock', { + language: ( evt.source as any )._codeBlockLanguage, + forceValue: true + }); + + editor.editing.view.focus(); + }); + addListToDropdown(dropdownView, itemDefinitions); + return dropdownView; + }); + } + + // Adapted from packages/ckeditor5-code-block/src/codeblockui.ts + private _getLanguageListItemDefinitions( + normalizedLanguageDefs: Array + ): Collection { + const editor = this.editor; + const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; + const itemDefinitions = new Collection(); + + for ( const languageDef of normalizedLanguageDefs ) { + const definition: ListDropdownButtonDefinition = { + type: 'button', + model: new ViewModel( { + _codeBlockLanguage: languageDef.language, + label: languageDef.label, + role: 'menuitemradio', + withText: true + } ) + }; + + definition.model.bind( 'isOn' ).to( command, 'value', value => { + return value === definition.model._codeBlockLanguage; + } ); + + itemDefinitions.add( definition ); + } + + return itemDefinitions; + } + + // Adapted from packages/ckeditor5-code-block/src/utils.ts + private _getNormalizedAndLocalizedLanguageDefinitions(editor: Editor) { + const languageDefs = editor.config.get( 'codeBlock.languages' ) as Array; + for ( const def of languageDefs ) { + if ( def.class === undefined ) { + def.class = `language-${ def.language }`; + } + } + return languageDefs; + } + +} + +interface CodeBlockLanguageDefinition { + + /** + * The name of the language that will be stored in the model attribute. Also, when `class` + * is not specified, it will be used to create the CSS class associated with the language (prefixed by "language-"). + */ + language: string; + + /** + * The human–readable label associated with the language and displayed in the UI. + */ + label: string; + + /** + * The CSS class associated with the language. When not specified the `language` + * property is used to create a class prefixed by "language-". + */ + class?: string; +} diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 259f98bc4..8e4e1081a 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -1,40 +1,10 @@ -import { Editor, CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode, type ListDropdownButtonDefinition, Collection, type CodeBlockCommand, ViewModel, createDropdown, addListToDropdown, DropdownButtonView } from "ckeditor5"; +import { CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode } from "ckeditor5"; +import CodeBlockLanguageDropdown from "./code_block_language_dropdown"; export default class CodeBlockToolbar extends Plugin { static get requires() { - return [ WidgetToolbarRepository, CodeBlock ] as const; - } - - public init(): void { - const editor = this.editor; - const componentFactory = editor.ui.componentFactory; - - const normalizedLanguageDefs = this._getNormalizedAndLocalizedLanguageDefinitions(editor); - const itemDefinitions = this._getLanguageListItemDefinitions(normalizedLanguageDefs); - const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; - - componentFactory.add("codeBlockDropdown", locale => { - const dropdownView = createDropdown(this.editor.locale, DropdownButtonView); - dropdownView.buttonView.set({ - withText: true - }); - dropdownView.bind( 'isEnabled' ).to( command, 'value', value => !!value ); - dropdownView.buttonView.bind( 'label' ).to( command, 'value', (value) => { - const itemDefinition = normalizedLanguageDefs.find((def) => def.language === value); - return itemDefinition?.label; - }); - dropdownView.on( 'execute', evt => { - editor.execute( 'codeBlock', { - language: ( evt.source as any )._codeBlockLanguage, - forceValue: true - }); - - editor.editing.view.focus(); - }); - addListToDropdown(dropdownView, itemDefinitions); - return dropdownView; - }); + return [ WidgetToolbarRepository, CodeBlock, CodeBlockLanguageDropdown ] as const; } afterInit() { @@ -65,64 +35,4 @@ export default class CodeBlockToolbar extends Plugin { }); } - // Adapted from packages/ckeditor5-code-block/src/codeblockui.ts - private _getLanguageListItemDefinitions( - normalizedLanguageDefs: Array - ): Collection { - const editor = this.editor; - const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; - const itemDefinitions = new Collection(); - - for ( const languageDef of normalizedLanguageDefs ) { - const definition: ListDropdownButtonDefinition = { - type: 'button', - model: new ViewModel( { - _codeBlockLanguage: languageDef.language, - label: languageDef.label, - role: 'menuitemradio', - withText: true - } ) - }; - - definition.model.bind( 'isOn' ).to( command, 'value', value => { - return value === definition.model._codeBlockLanguage; - } ); - - itemDefinitions.add( definition ); - } - - return itemDefinitions; - } - - // Adapted from packages/ckeditor5-code-block/src/utils.ts - private _getNormalizedAndLocalizedLanguageDefinitions(editor: Editor) { - const languageDefs = editor.config.get( 'codeBlock.languages' ) as Array; - for ( const def of languageDefs ) { - if ( def.class === undefined ) { - def.class = `language-${ def.language }`; - } - } - return languageDefs; - } - -} - -interface CodeBlockLanguageDefinition { - - /** - * The name of the language that will be stored in the model attribute. Also, when `class` - * is not specified, it will be used to create the CSS class associated with the language (prefixed by "language-"). - */ - language: string; - - /** - * The human–readable label associated with the language and displayed in the UI. - */ - label: string; - - /** - * The CSS class associated with the language. When not specified the `language` - * property is used to create a class prefixed by "language-". - */ - class?: string; } From aff5a9c31d716b84cd44d449e52bd21e57e5df1f Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 26 May 2025 16:05:27 +0800 Subject: [PATCH 23/62] style(math): Set the default width of the math formula editing dialog --- packages/ckeditor5-math/src/mathui.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index cc402c1e6..851c86e63 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -126,6 +126,7 @@ export default class MathUI extends Plugin { Object.assign(textarea.style, { resize: 'both', height: '100px', + width: '400px', minWidth: '100%', }); }); From 5eecea52bf42c1434fe0a739c2f1f05cbef591c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 11:37:26 +0300 Subject: [PATCH 24/62] feat(ckeditor5/codeblock): add copy icon --- packages/ckeditor5/src/icons/copy.svg | 1 + .../src/plugins/code_block_toolbar.ts | 7 +++++-- .../src/plugins/copy_to_clipboard_button.ts | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 packages/ckeditor5/src/icons/copy.svg create mode 100644 packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts diff --git a/packages/ckeditor5/src/icons/copy.svg b/packages/ckeditor5/src/icons/copy.svg new file mode 100644 index 000000000..41710638b --- /dev/null +++ b/packages/ckeditor5/src/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 8e4e1081a..4f886efa8 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -1,10 +1,11 @@ import { CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode } from "ckeditor5"; import CodeBlockLanguageDropdown from "./code_block_language_dropdown"; +import CopyToClipboardButton from "./copy_to_clipboard_button"; export default class CodeBlockToolbar extends Plugin { static get requires() { - return [ WidgetToolbarRepository, CodeBlock, CodeBlockLanguageDropdown ] as const; + return [ WidgetToolbarRepository, CodeBlock, CodeBlockLanguageDropdown, CopyToClipboardButton ] as const; } afterInit() { @@ -13,7 +14,9 @@ export default class CodeBlockToolbar extends Plugin { widgetToolbarRepository.register("codeblock", { items: [ - "codeBlockDropdown" + "codeBlockDropdown", + "|", + "copyToClipboard" ], getRelatedElement(selection) { const selectionPosition = selection.getFirstPosition(); diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts new file mode 100644 index 000000000..2b67ea820 --- /dev/null +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -0,0 +1,21 @@ +import { ButtonView, Plugin } from "ckeditor5"; +import copyIcon from "../icons/copy.svg?raw"; + +export default class CopyToClipboardButton extends Plugin { + + public init() { + const editor = this.editor; + const componentFactory = editor.ui.componentFactory; + + componentFactory.add("copyToClipboard", locale => { + const button = new ButtonView(locale); + button.set({ + tooltip: "Copy to clipboard", + icon: copyIcon + }); + + return button; + }); + } + +} From fc83f67d7cc090b479035a225f82387263d3381b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 11:37:44 +0300 Subject: [PATCH 25/62] chore(ckeditor5/codeblock): add command for copying to clipboard --- .../src/plugins/copy_to_clipboard_button.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index 2b67ea820..1de7e6f0e 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -1,8 +1,16 @@ -import { ButtonView, Plugin } from "ckeditor5"; +import { ButtonView, Command, Plugin } from "ckeditor5"; import copyIcon from "../icons/copy.svg?raw"; export default class CopyToClipboardButton extends Plugin { + static get requires() { + return [ CopyToClipboardEditing, CopyToClipboardUI ]; + } + +} + +export class CopyToClipboardUI extends Plugin { + public init() { const editor = this.editor; const componentFactory = editor.ui.componentFactory; @@ -14,8 +22,28 @@ export default class CopyToClipboardButton extends Plugin { icon: copyIcon }); + this.listenTo(button, "execute", () => { + editor.execute("copyToClipboard"); + }); + return button; }); } } + +export class CopyToClipboardEditing extends Plugin { + + public init() { + this.editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor)); + } + +} + +export class CopyToClipboardCommand extends Command { + + execute(...args: Array) { + console.log("Copy to clipboard!"); + } + +} From 32fd575cc46c63d0fdb0328082912d020a1d3468 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 26 May 2025 17:17:18 +0800 Subject: [PATCH 26/62] fix(math edit): preserve line breaks --- packages/ckeditor5-math/src/mathediting.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-math/src/mathediting.ts b/packages/ckeditor5-math/src/mathediting.ts index bfd08ed37..0c51653c3 100644 --- a/packages/ckeditor5-math/src/mathediting.ts +++ b/packages/ckeditor5-math/src/mathediting.ts @@ -27,6 +27,18 @@ export default class MathEditing extends Plugin { public init(): void { const editor = this.editor; + + const originalProcessor = editor.data.processor; + const originalToView = originalProcessor.toView.bind(originalProcessor); + const mathSpanRegex = /([\s\S]*?)<\/span>/g; + originalProcessor.toView = (data: string) => { + // Preprocessing: preserve line breaks inside math formulas by replacing \n with + const processedData = data.replace(mathSpanRegex, (_, content) => + `${content.replace(/\n/g, '___MATH_TEX_LF___')}` + ); + return originalToView(processedData); + }; + editor.commands.add( 'math', new MathCommand( editor ) ); this._defineSchema(); @@ -120,8 +132,7 @@ export default class MathEditing extends Plugin { model: ( viewElement, { writer } ) => { const child = viewElement.getChild( 0 ); if ( child?.is( '$text' ) ) { - const equation = child.data.trim(); - + const equation = child.data.trim().replace(/___MATH_TEX_LF___/g, '\n'); const params = Object.assign( extractDelimiters( equation ), { type: mathConfig.forceOutputType ? mathConfig.outputType : From a77d89f4c7259a0b2049e649870c46703fd99776 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:18:21 +0300 Subject: [PATCH 27/62] feat(ckeditor5/codeblock): implement copy to clipboard function --- .../src/plugins/copy_to_clipboard_button.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index 1de7e6f0e..d212c1e49 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -43,7 +43,29 @@ export class CopyToClipboardEditing extends Plugin { export class CopyToClipboardCommand extends Command { execute(...args: Array) { - console.log("Copy to clipboard!"); + const editor = this.editor; + const model = editor.model; + const selection = model.document.selection; + + const codeBlockEl = selection.getFirstPosition()?.findAncestor("codeBlock"); + if (!codeBlockEl) { + console.warn("Unable to find code block element to copy from."); + return; + } + + const codeText = Array.from(codeBlockEl.getChildren()) + .map(child => "data" in child ? child.data : "\n") + .join(""); + + if (codeText) { + navigator.clipboard.writeText(codeText).then(() => { + console.log('Code block copied to clipboard'); + }).catch(err => { + console.error('Failed to copy code block', err); + }); + } else { + console.warn('No code block selected or found.'); + } } } From 622d026efc9f3e5e7f5ef5b23b2c8bc9f56bfc94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:23:11 +0300 Subject: [PATCH 28/62] refactor(ckeditor5/codeblock): simplify copy clipboard plugin --- .../src/plugins/copy_to_clipboard_button.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index d212c1e49..281259c17 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -3,18 +3,11 @@ import copyIcon from "../icons/copy.svg?raw"; export default class CopyToClipboardButton extends Plugin { - static get requires() { - return [ CopyToClipboardEditing, CopyToClipboardUI ]; - } - -} - -export class CopyToClipboardUI extends Plugin { - public init() { const editor = this.editor; - const componentFactory = editor.ui.componentFactory; + editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor)); + const componentFactory = editor.ui.componentFactory; componentFactory.add("copyToClipboard", locale => { const button = new ButtonView(locale); button.set({ @@ -32,14 +25,6 @@ export class CopyToClipboardUI extends Plugin { } -export class CopyToClipboardEditing extends Plugin { - - public init() { - this.editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor)); - } - -} - export class CopyToClipboardCommand extends Command { execute(...args: Array) { From 4752db6bc5ad4ca1c188a281cf8a838fa7bf2c63 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:35:30 +0300 Subject: [PATCH 29/62] style(ckeditor5/codeblock): limit language selector height --- packages/ckeditor5/src/index.ts | 1 + packages/ckeditor5/src/plugins/code_block_toolbar.ts | 1 + packages/ckeditor5/src/theme/code_block_toolbar.css | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 packages/ckeditor5/src/theme/code_block_toolbar.css diff --git a/packages/ckeditor5/src/index.ts b/packages/ckeditor5/src/index.ts index 1a614f8e8..8dc0e3611 100644 --- a/packages/ckeditor5/src/index.ts +++ b/packages/ckeditor5/src/index.ts @@ -1,4 +1,5 @@ import "ckeditor5/ckeditor5.css"; +import "./theme/code_block_toolbar.css"; import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins"; import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5"; export { EditorWatchdog } from "ckeditor5"; diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 4f886efa8..ff9014fd8 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -18,6 +18,7 @@ export default class CodeBlockToolbar extends Plugin { "|", "copyToClipboard" ], + balloonClassName: "ck-toolbar-container codeblock-language-list", getRelatedElement(selection) { const selectionPosition = selection.getFirstPosition(); if (!selectionPosition) { diff --git a/packages/ckeditor5/src/theme/code_block_toolbar.css b/packages/ckeditor5/src/theme/code_block_toolbar.css new file mode 100644 index 000000000..0776571b4 --- /dev/null +++ b/packages/ckeditor5/src/theme/code_block_toolbar.css @@ -0,0 +1,4 @@ +.ck.ck-balloon-panel.codeblock-language-list .ck-dropdown__panel { + max-height: 300px; + overflow-y: auto; +} \ No newline at end of file From 02e2b5d4ad4a0bfc0e7c7f1ad3c3fcd1b03c2d53 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 15:17:10 +0300 Subject: [PATCH 30/62] feat(client): add a copy button to read-only text --- apps/client/src/services/content_renderer.ts | 4 ++-- apps/client/src/services/doc_renderer.ts | 4 ++-- apps/client/src/services/syntax_highlight.ts | 22 ++++++++++++++----- apps/client/src/stylesheets/style.css | 20 +++++++++++++++++ .../src/widgets/llm_chat/llm_chat_panel.ts | 4 ++-- apps/client/src/widgets/llm_chat/utils.ts | 4 ++-- .../widgets/type_widgets/read_only_text.ts | 4 ++-- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 0664f6a5c..08ed561ff 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -9,7 +9,7 @@ import treeService from "./tree.js"; import FNote from "../entities/fnote.js"; import FAttachment from "../entities/fattachment.js"; import imageContextMenuService from "../menus/image_context_menu.js"; -import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js"; +import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; import renderDoc from "./doc_renderer.js"; import { t } from "../services/i18n.js"; @@ -106,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery>((resolve) => { @@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery) { $img.attr("src", dir + "/" + $img.attr("src")); }); - applySyntaxHighlight($content); + formatCodeBlocks($content); } function getUrl(docNameValue: string, language: string) { diff --git a/apps/client/src/services/syntax_highlight.ts b/apps/client/src/services/syntax_highlight.ts index 7dfb29f30..0cb7cbf2d 100644 --- a/apps/client/src/services/syntax_highlight.ts +++ b/apps/client/src/services/syntax_highlight.ts @@ -6,16 +6,16 @@ let highlightingLoaded = false; /** * Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks. + * Additionally, adds a "Copy to clipboard" button. * * @param $container the container under which to look for code blocks and to apply syntax highlighting to them. */ -export async function applySyntaxHighlight($container: JQuery) { - if (!isSyntaxHighlightEnabled()) { - return; +export async function formatCodeBlocks($container: JQuery) { + const syntaxHighlightingEnabled = isSyntaxHighlightEnabled(); + if (syntaxHighlightingEnabled) { + await ensureMimeTypesForHighlighting(); } - await ensureMimeTypesForHighlighting(); - const codeBlocks = $container.find("pre code"); for (const codeBlock of codeBlocks) { const normalizedMimeType = extractLanguageFromClassList(codeBlock); @@ -23,10 +23,20 @@ export async function applySyntaxHighlight($container: JQuery) { continue; } - applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + applyCopyToClipboardButton($(codeBlock)); + + if (syntaxHighlightingEnabled) { + applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + } } } +export function applyCopyToClipboardButton($codeBlock: JQuery) { + const $copyButton = $("