From 66afc72d1975f5b187a99b45d689221255380954 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 02:28:25 +0000 Subject: [PATCH 01/25] chore(deps): update dependency electron to v36.3.1 --- apps/desktop/package.json | 2 +- apps/edit-docs/package.json | 2 +- apps/server/package.json | 2 +- pnpm-lock.yaml | 26 +++++++++++++------------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 71b61fcce..19d4231a3 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -17,7 +17,7 @@ "@types/electron-squirrel-startup": "1.0.2", "@triliumnext/server": "workspace:*", "copy-webpack-plugin": "13.0.0", - "electron": "36.2.1", + "electron": "36.3.1", "@electron-forge/cli": "7.8.1", "@electron-forge/maker-deb": "7.8.1", "@electron-forge/maker-dmg": "7.8.1", diff --git a/apps/edit-docs/package.json b/apps/edit-docs/package.json index d38029b5f..4710befa5 100644 --- a/apps/edit-docs/package.json +++ b/apps/edit-docs/package.json @@ -8,7 +8,7 @@ "@triliumnext/desktop": "workspace:*", "@types/fs-extra": "11.0.4", "copy-webpack-plugin": "13.0.0", - "electron": "36.2.1", + "electron": "36.3.1", "fs-extra": "11.3.0" }, "nx": { diff --git a/apps/server/package.json b/apps/server/package.json index e9c5d4703..f070b223c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -59,7 +59,7 @@ "debounce": "2.2.0", "debug": "4.4.1", "ejs": "3.1.10", - "electron": "36.2.1", + "electron": "36.3.1", "electron-debug": "4.1.0", "electron-window-state": "5.0.3", "escape-html": "1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f18e2fc1..709a7a182 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -359,7 +359,7 @@ importers: dependencies: '@electron/remote': specifier: 2.1.2 - version: 2.1.2(electron@36.2.1) + version: 2.1.2(electron@36.3.1) better-sqlite3: specifier: ^11.9.1 version: 11.10.0 @@ -413,8 +413,8 @@ importers: specifier: 13.0.0 version: 13.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.4)(webpack-cli@6.0.1(webpack@5.98.0))) electron: - specifier: 36.2.1 - version: 36.2.1 + specifier: 36.3.1 + version: 36.3.1 prebuild-install: specifier: ^7.1.1 version: 7.1.3 @@ -466,8 +466,8 @@ importers: specifier: 13.0.0 version: 13.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.4)(webpack-cli@6.0.1(webpack@5.98.0))) electron: - specifier: 36.2.1 - version: 36.2.1 + specifier: 36.3.1 + version: 36.3.1 fs-extra: specifier: 11.3.0 version: 11.3.0 @@ -486,7 +486,7 @@ importers: version: 7.1.1 '@electron/remote': specifier: 2.1.2 - version: 2.1.2(electron@36.2.1) + version: 2.1.2(electron@36.3.1) '@triliumnext/commons': specifier: workspace:* version: link:../../packages/commons @@ -635,8 +635,8 @@ importers: specifier: 3.1.10 version: 3.1.10 electron: - specifier: 36.2.1 - version: 36.2.1 + specifier: 36.3.1 + version: 36.3.1 electron-debug: specifier: 4.1.0 version: 4.1.0 @@ -7549,8 +7549,8 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} - electron@36.2.1: - resolution: {integrity: sha512-mm1Y+Ms46xcOTA69h8hpqfX392HfV4lga9aEkYkd/Syx1JBStvcACOIouCgGrnZpxNZPVS1jM8NTcMkNjuK6BQ==} + electron@36.3.1: + resolution: {integrity: sha512-LeOZ+tVahmctHaAssLCGRRUa2SAO09GXua3pKdG+WzkbSDMh+3iOPONNVPTqGp8HlWnzGj4r6mhsIbM2RgH+eQ==} engines: {node: '>= 12.20.55'} hasBin: true @@ -16161,9 +16161,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@electron/remote@2.1.2(electron@36.2.1)': + '@electron/remote@2.1.2(electron@36.3.1)': dependencies: - electron: 36.2.1 + electron: 36.3.1 '@electron/universal@2.0.2': dependencies: @@ -22019,7 +22019,7 @@ snapshots: - supports-color optional: true - electron@36.2.1: + electron@36.3.1: dependencies: '@electron/get': 2.0.3 '@types/node': 22.15.21 From 1bc2f876c239861e21f4919f3db34cb1fdf3bffa Mon Sep 17 00:00:00 2001 From: Arne Keller Date: Sun, 25 May 2025 20:30:42 +0200 Subject: [PATCH 02/25] 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 03/25] 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 04/25] 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 05/25] 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 06/25] 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 07/25] 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 08/25] 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 09/25] 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 10/25] 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 11/25] 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 12/25] 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 13/25] 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 5eecea52bf42c1434fe0a739c2f1f05cbef591c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 11:37:26 +0300 Subject: [PATCH 14/25] 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 15/25] 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 a77d89f4c7259a0b2049e649870c46703fd99776 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:18:21 +0300 Subject: [PATCH 16/25] 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 17/25] 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 18/25] 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 19/25] 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 = $("