Merge branch 'develop' into math-edit

This commit is contained in:
Elian Doran 2025-05-26 16:39:01 +03:00 committed by GitHub
commit 5fcf4afcfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 334 additions and 80 deletions

View File

@ -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. 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. 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.

View File

@ -9,7 +9,7 @@ import treeService from "./tree.js";
import FNote from "../entities/fnote.js"; import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js"; import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.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 { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import renderDoc from "./doc_renderer.js"; import renderDoc from "./doc_renderer.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
@ -106,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
await linkService.loadReferenceLinkTitle($(el)); await linkService.loadReferenceLinkTitle($(el));
} }
await applySyntaxHighlight($renderedContent); await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote) { } else if (note instanceof FNote) {
await renderChildrenList($renderedContent, note); await renderChildrenList($renderedContent, note);
} }

View File

@ -1,6 +1,6 @@
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { getCurrentLanguage } from "./i18n.js"; import { getCurrentLanguage } from "./i18n.js";
import { applySyntaxHighlight } from "./syntax_highlight.js"; import { formatCodeBlocks } from "./syntax_highlight.js";
export default function renderDoc(note: FNote) { export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => { return new Promise<JQuery<HTMLElement>>((resolve) => {
@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery<HTMLElement>) {
$img.attr("src", dir + "/" + $img.attr("src")); $img.attr("src", dir + "/" + $img.attr("src"));
}); });
applySyntaxHighlight($content); formatCodeBlocks($content);
} }
function getUrl(docNameValue: string, language: string) { function getUrl(docNameValue: string, language: string) {

View File

@ -1,21 +1,23 @@
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes } from "@triliumnext/highlightjs"; import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
import mime_types from "./mime_types.js"; import mime_types from "./mime_types.js";
import options from "./options.js"; import options from "./options.js";
import toast from "./toast.js";
import { t } from "./i18n.js";
let highlightingLoaded = false; 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. * 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. * @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
*/ */
export async function applySyntaxHighlight($container: JQuery<HTMLElement>) { export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
if (!isSyntaxHighlightEnabled()) { const syntaxHighlightingEnabled = isSyntaxHighlightEnabled();
return; if (syntaxHighlightingEnabled) {
await ensureMimeTypesForHighlighting();
} }
await ensureMimeTypesForHighlighting();
const codeBlocks = $container.find("pre code"); const codeBlocks = $container.find("pre code");
for (const codeBlock of codeBlocks) { for (const codeBlock of codeBlocks) {
const normalizedMimeType = extractLanguageFromClassList(codeBlock); const normalizedMimeType = extractLanguageFromClassList(codeBlock);
@ -23,10 +25,31 @@ export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
continue; continue;
} }
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); applyCopyToClipboardButton($(codeBlock));
if (syntaxHighlightingEnabled) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
}
} }
} }
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
const $copyButton = $("<button>")
.addClass("bx component btn tn-tool-button bx-copy copy-button")
.attr("title", t("code_block.copy_title"))
.on("click", () => {
const text = $codeBlock.text();
try {
navigator.clipboard.writeText(text);
toast.showMessage(t("code_block.copy_success"));
} catch (e) {
toast.showError(t("code_block.copy_failed"));
}
});
$codeBlock.parent().append($copyButton);
}
/** /**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js. * Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*/ */
@ -34,7 +57,7 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
$codeBlock.parent().toggleClass("hljs"); $codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text(); const text = $codeBlock.text();
let highlightedText = null; let highlightedText: HighlightResult | AutoHighlightResult | null = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) { if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
await ensureMimeTypesForHighlighting(); await ensureMimeTypesForHighlighting();
highlightedText = highlightAuto(text); highlightedText = highlightAuto(text);
@ -66,7 +89,7 @@ export async function ensureMimeTypesForHighlighting() {
export function loadHighlightingTheme(themeName: string) { export function loadHighlightingTheme(themeName: string) {
const themePrefix = "default:"; const themePrefix = "default:";
let theme = null; let theme: Theme | null = null;
if (themeName.includes(themePrefix)) { if (themeName.includes(themePrefix)) {
theme = Themes[themeName.substring(themePrefix.length)]; theme = Themes[themeName.substring(themePrefix.length)];
} }

View File

@ -529,6 +529,26 @@ pre:not(.hljs) {
font-size: 100%; font-size: 100%;
} }
pre {
position: relative;
}
pre > button.copy-button {
position: absolute;
top: 1em;
right: 1em;
opacity: 0.8;
}
pre > button.copy-button:hover {
color: inherit !important;
opacity: 1;
}
pre > button.copy-button:active {
background-color: unset !important;
}
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }

View File

@ -1830,7 +1830,10 @@
"word_wrapping": "Word wrapping", "word_wrapping": "Word wrapping",
"theme_none": "No syntax highlighting", "theme_none": "No syntax highlighting",
"theme_group_light": "Light themes", "theme_group_light": "Light themes",
"theme_group_dark": "Dark themes" "theme_group_dark": "Dark themes",
"copy_success": "The code block was copied to clipboard.",
"copy_failed": "The code block could not be copied to the clipboard due to lack of permissions.",
"copy_title": "Copy to clipboard"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Formatting" "title": "Formatting"

View File

@ -93,16 +93,6 @@ declare global {
getSelectedExternalLink(): string | undefined; getSelectedExternalLink(): string | undefined;
setSelectedExternalLink(externalLink: string | null | undefined); setSelectedExternalLink(externalLink: string | null | undefined);
setNote(noteId: string); setNote(noteId: string);
markRegExp(regex: RegExp, opts: {
element: string;
className: string;
separateWordSearch: boolean;
caseSensitive: boolean;
done?: () => void;
});
unmark(opts?: {
done: () => void;
});
} }
interface JQueryStatic { interface JQueryStatic {

View File

@ -248,10 +248,10 @@ export default class FindWidget extends NoteContextAwareWidget {
case "code": case "code":
return this.codeHandler; return this.codeHandler;
case "text": case "text":
return this.textHandler;
default:
const readOnly = await this.noteContext?.isReadOnly(); const readOnly = await this.noteContext?.isReadOnly();
return readOnly ? this.htmlHandler : this.textHandler; return readOnly ? this.htmlHandler : this.textHandler;
default:
console.warn("FindWidget: Unsupported note type for find widget", this.note?.type);
} }
} }

View File

@ -1,6 +1,7 @@
// ck-find-result and ck-find-result_selected are the styles ck-editor // ck-find-result and ck-find-result_selected are the styles ck-editor
// uses for highlighting matches, use the same one on CodeMirror // uses for highlighting matches, use the same one on CodeMirror
// for consistency // for consistency
import type Mark from "mark.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type FindWidget from "./find.js"; import type FindWidget from "./find.js";
import type { FindResult } from "./find.js"; import type { FindResult } from "./find.js";
@ -13,6 +14,7 @@ export default class FindInHtml {
private parent: FindWidget; private parent: FindWidget;
private currentIndex: number; private currentIndex: number;
private $results: JQuery<HTMLElement> | null; private $results: JQuery<HTMLElement> | null;
private mark?: Mark;
constructor(parent: FindWidget) { constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
@ -21,21 +23,24 @@ export default class FindInHtml {
} }
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
await import("mark.js");
const $content = await this.parent?.noteContext?.getContentElement(); 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 wholeWordChar = wholeWord ? "\\b" : "";
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi"); const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
return new Promise<FindResult>((res) => { return new Promise<FindResult>((res) => {
$content?.unmark({ this.mark!.unmark({
done: () => { done: () => {
$content.markRegExp(regExp, { this.mark!.markRegExp(regExp, {
element: "span", element: "span",
className: FIND_RESULT_CSS_CLASSNAME, className: FIND_RESULT_CSS_CLASSNAME,
separateWordSearch: false,
caseSensitive: matchCase,
done: async () => { done: async () => {
this.$results = $content.find(`.${FIND_RESULT_CSS_CLASSNAME}`); this.$results = $content.find(`.${FIND_RESULT_CSS_CLASSNAME}`);
const scrollingContainer = $content[0].closest('.scrolling-container'); const scrollingContainer = $content[0].closest('.scrolling-container');
@ -73,10 +78,7 @@ export default class FindInHtml {
} }
async findBoxClosed(totalFound: number, currentFound: number) { async findBoxClosed(totalFound: number, currentFound: number) {
const $content = await this.parent?.noteContext?.getContentElement(); this.mark?.unmark();
if (typeof $content?.unmark === 'function') {
$content.unmark();
}
} }
async jumpTo() { async jumpTo() {

View File

@ -12,7 +12,7 @@ import { createChatSession, checkSessionExists, setupStreamingResponse, getDirec
import { extractInChatToolSteps } from "./message_processor.js"; import { extractInChatToolSteps } from "./message_processor.js";
import { validateEmbeddingProviders } from "./validation.js"; import { validateEmbeddingProviders } from "./validation.js";
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js"; import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import "../../stylesheets/llm_chat.css"; import "../../stylesheets/llm_chat.css";
@ -925,7 +925,7 @@ export default class LlmChatPanel extends BasicWidget {
// Apply syntax highlighting if this is the final update // Apply syntax highlighting if this is the final update
if (isDone) { if (isDone) {
applySyntaxHighlight($(assistantMessageEl as HTMLElement)); formatCodeBlocks($(assistantMessageEl as HTMLElement));
// Update message in the data model for storage // Update message in the data model for storage
// Find the last assistant message to update, or add a new one if none exists // Find the last assistant message to update, or add a new one if none exists

View File

@ -2,7 +2,7 @@
* Utility functions for LLM Chat * Utility functions for LLM Chat
*/ */
import { marked } from "marked"; import { marked } from "marked";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
/** /**
* Format markdown content for display * Format markdown content for display
@ -62,7 +62,7 @@ export function escapeHtml(text: string): string {
* Apply syntax highlighting to content * Apply syntax highlighting to content
*/ */
export function applyHighlighting(element: HTMLElement): void { export function applyHighlighting(element: HTMLElement): void {
applySyntaxHighlight($(element)); formatCodeBlocks($(element));
} }
/** /**

View File

@ -1,5 +1,5 @@
import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { CommandListenerData, EventData } from "../../components/app_context.js"; import type { CommandListenerData, EventData } from "../../components/app_context.js";
import { getLocaleById } from "../../services/i18n.js"; import { getLocaleById } from "../../services/i18n.js";
@ -125,7 +125,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
} }
await this.#applyInlineMermaid(); await this.#applyInlineMermaid();
await applySyntaxHighlight(this.$content); await formatCodeBlocks(this.$content);
} }
async #applyInlineMermaid() { async #applyInlineMermaid() {

View File

@ -215,8 +215,6 @@ class ListOrGridView extends ViewMode {
const highlightedTokens = this.parentNote.highlightedTokens || []; const highlightedTokens = this.parentNote.highlightedTokens || [];
if (highlightedTokens.length > 0) { if (highlightedTokens.length > 0) {
await import("mark.js");
const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|");
this.highlightRegex = new RegExp(regex, "gi"); this.highlightRegex = new RegExp(regex, "gi");
@ -320,11 +318,10 @@ class ListOrGridView extends ViewMode {
$expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded")));
if (this.highlightRegex) { 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", element: "span",
className: "ck-find-result", className: "ck-find-result"
separateWordSearch: false,
caseSensitive: false
}); });
} }
@ -362,11 +359,10 @@ class ListOrGridView extends ViewMode {
}); });
if (this.highlightRegex) { if (this.highlightRegex) {
$renderedContent.markRegExp(this.highlightRegex, { const Mark = new (await import("mark.js")).default($renderedContent[0]);
Mark.markRegExp(this.highlightRegex, {
element: "span", element: "span",
className: "ck-find-result", className: "ck-find-result"
separateWordSearch: false,
caseSensitive: false
}); });
} }

View File

@ -68,8 +68,8 @@ module.exports = {
] ]
}, },
rebuildConfig: { rebuildConfig: {
force: false, force: true,
onlyModules: [] extraModules: [ "better-sqlite3" ]
}, },
makers: [ makers: [
{ {

View File

@ -17,7 +17,7 @@
"@types/electron-squirrel-startup": "1.0.2", "@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/server": "workspace:*", "@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"electron": "36.2.1", "electron": "36.3.1",
"@electron-forge/cli": "7.8.1", "@electron-forge/cli": "7.8.1",
"@electron-forge/maker-deb": "7.8.1", "@electron-forge/maker-deb": "7.8.1",
"@electron-forge/maker-dmg": "7.8.1", "@electron-forge/maker-dmg": "7.8.1",

View File

@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*", "@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"electron": "36.2.1", "electron": "36.3.1",
"fs-extra": "11.3.0" "fs-extra": "11.3.0"
}, },
"nx": { "nx": {

View File

@ -33,24 +33,24 @@ if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
const NOTE_MAPPINGS: NoteMapping[] = [ const NOTE_MAPPINGS: NoteMapping[] = [
{ {
rootNoteId: "pOsGYCXsbNQG", rootNoteId: "pOsGYCXsbNQG",
path: path.join(DOCS_ROOT, "User Guide"), path: path.join(__dirname, DOCS_ROOT, "User Guide"),
format: "markdown" format: "markdown"
}, },
{ {
rootNoteId: "pOsGYCXsbNQG", rootNoteId: "pOsGYCXsbNQG",
path: USER_GUIDE_ROOT, path: path.join(__dirname, USER_GUIDE_ROOT),
format: "html", format: "html",
ignoredFiles: ["index.html", "navigation.html", "style.css", "User Guide.html"], ignoredFiles: ["index.html", "navigation.html", "style.css", "User Guide.html"],
exportOnly: true exportOnly: true
}, },
{ {
rootNoteId: "jdjRLhLV3TtI", rootNoteId: "jdjRLhLV3TtI",
path: path.join(DOCS_ROOT, "Developer Guide"), path: path.join(__dirname, DOCS_ROOT, "Developer Guide"),
format: "markdown" format: "markdown"
}, },
{ {
rootNoteId: "hD3V4hiu2VW4", rootNoteId: "hD3V4hiu2VW4",
path: path.join(DOCS_ROOT, "Release Notes"), path: path.join(__dirname, DOCS_ROOT, "Release Notes"),
format: "markdown" format: "markdown"
} }
]; ];

View File

@ -59,7 +59,7 @@
"debounce": "2.2.0", "debounce": "2.2.0",
"debug": "4.4.1", "debug": "4.4.1",
"ejs": "3.1.10", "ejs": "3.1.10",
"electron": "36.2.1", "electron": "36.3.1",
"electron-debug": "4.1.0", "electron-debug": "4.1.0",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"escape-html": "1.0.3", "escape-html": "1.0.3",
@ -115,8 +115,13 @@
"serve": { "serve": {
"executor": "@nx/js:node", "executor": "@nx/js:node",
"dependsOn": [ "dependsOn": [
{
"projects": [ "client" ],
"target": "serve"
},
"build-without-client" "build-without-client"
], ],
"continuous": true,
"options": { "options": {
"buildTarget": "server:build-without-client:development", "buildTarget": "server:build-without-client:development",
"runBuildTargetDependencies": false "runBuildTargetDependencies": false

View File

@ -1,5 +1,5 @@
<p>Trilium supports configuration via a file named <code>config.ini</code> and <p>Trilium supports configuration via a file named <code>config.ini</code> and
environment variables. Please review the file named <a href="https://github.com/TriliumNext/Notes/blob/develop/config-sample.ini">config-sample.ini</a> in environment variables. Please review the file named <a href="https://github.com/TriliumNext/Notes/blob/develop/apps/server/src/assets/config-sample.ini">config-sample.ini</a> in
the <a href="https://github.com/TriliumNext/Notes">Notes</a> repository to the <a href="https://github.com/TriliumNext/Notes">Notes</a> repository to
see what values are supported.</p> see what values are supported.</p>
<p>You can provide the same values via environment variables instead of the <code>config.ini</code> file, <p>You can provide the same values via environment variables instead of the <code>config.ini</code> file,

View File

@ -49,6 +49,9 @@
* Backend log: disable some editor features in order to increase performance for large logs (syntax highlighting, folding, etc.). * Backend log: disable some editor features in order to increase performance for large logs (syntax highlighting, folding, etc.).
* [Collapsible table of contents](https://github.com/TriliumNext/Notes/pull/1954) by @SiriusXT * [Collapsible table of contents](https://github.com/TriliumNext/Notes/pull/1954) by @SiriusXT
* Sessions (logins) are no longer stored as files in the data directory, but as entries in the database. This improves the session reliability on Windows platforms. * Sessions (logins) are no longer stored as files in the data directory, but as entries in the database. This improves the session reliability on Windows platforms.
* Code blocks in text notes:
* For editable notes, clicking on a code block will reveal a toolbar with a way to easily change the programming language and another button to copy the text to clipboard.
* For read-only notes, a floating button allows copying the code snippet to clipboard.
## 📖 Documentation ## 📖 Documentation

View File

@ -1,5 +1,5 @@
# Configuration (config.ini or environment variables) # 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: You can provide the same values via environment variables instead of the `config.ini` file, and these environment variables use the following format:

View File

@ -12,7 +12,7 @@
"server:test": "nx test server", "server:test": "nx test server",
"server:build": "nx build server", "server:build": "nx build server",
"server:coverage": "nx test server --coverage", "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", "server:start-prod": "nx run server:start-prod",
"electron:build": "nx build desktop", "electron:build": "nx build desktop",
"chore:ci-update-nightly-version": "tsx ./scripts/update-nightly-version.ts", "chore:ci-update-nightly-version": "tsx ./scripts/update-nightly-version.ts",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="fill: rgba(0, 0, 0, 1);transform: ;msFilter:;"><path d="M20 2H10c-1.103 0-2 .897-2 2v4H4c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2h10c1.103 0 2-.897 2-2v-4h4c1.103 0 2-.897 2-2V4c0-1.103-.897-2-2-2zM4 20V10h10l.002 10H4zm16-6h-4v-4c0-1.103-.897-2-2-2h-4V4h10v10z"></path></svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@ -1,4 +1,5 @@
import "ckeditor5/ckeditor5.css"; import "ckeditor5/ckeditor5.css";
import "./theme/code_block_toolbar.css";
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins"; import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins";
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5"; import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
export { EditorWatchdog } from "ckeditor5"; export { EditorWatchdog } from "ckeditor5";

View File

@ -23,6 +23,8 @@ import "@triliumnext/ckeditor5-mermaid/index.css";
import "@triliumnext/ckeditor5-admonition/index.css"; import "@triliumnext/ckeditor5-admonition/index.css";
import "@triliumnext/ckeditor5-footnotes/index.css"; import "@triliumnext/ckeditor5-footnotes/index.css";
import "@triliumnext/ckeditor5-math/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. * 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 +40,9 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [
MarkdownImportPlugin, MarkdownImportPlugin,
IncludeNote, IncludeNote,
Uploadfileplugin, Uploadfileplugin,
SyntaxHighlighting SyntaxHighlighting,
CodeBlockLanguageDropdown,
CodeBlockToolbar
]; ];
/** /**

View File

@ -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<CodeBlockLanguageDefinition>
): Collection<ListDropdownButtonDefinition> {
const editor = this.editor;
const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!;
const itemDefinitions = new Collection<ListDropdownButtonDefinition>();
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<CodeBlockLanguageDefinition>;
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 humanreadable 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;
}

View File

@ -0,0 +1,42 @@
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, CopyToClipboardButton ] as const;
}
afterInit() {
const editor = this.editor;
const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
widgetToolbarRepository.register("codeblock", {
items: [
"codeBlockDropdown",
"|",
"copyToClipboard"
],
balloonClassName: "ck-toolbar-container codeblock-language-list",
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;
}
});
}
}

View File

@ -0,0 +1,56 @@
import { ButtonView, Command, Plugin } from "ckeditor5";
import copyIcon from "../icons/copy.svg?raw";
export default class CopyToClipboardButton extends Plugin {
public init() {
const editor = this.editor;
editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor));
const componentFactory = editor.ui.componentFactory;
componentFactory.add("copyToClipboard", locale => {
const button = new ButtonView(locale);
button.set({
tooltip: "Copy to clipboard",
icon: copyIcon
});
this.listenTo(button, "execute", () => {
editor.execute("copyToClipboard");
});
return button;
});
}
}
export class CopyToClipboardCommand extends Command {
execute(...args: Array<unknown>) {
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.');
}
}
}

View File

@ -0,0 +1,4 @@
.ck.ck-balloon-panel.codeblock-language-list .ck-dropdown__panel {
max-height: 300px;
overflow-y: auto;
}

View File

@ -3,6 +3,7 @@ import { normalizeMimeTypeForCKEditor, type MimeType } from "@triliumnext/common
import syntaxDefinitions from "./syntax_highlighting.js"; import syntaxDefinitions from "./syntax_highlighting.js";
import { type Theme } from "./themes.js"; import { type Theme } from "./themes.js";
import { type HighlightOptions } from "highlight.js"; import { type HighlightOptions } from "highlight.js";
export type { HighlightResult, AutoHighlightResult } from "highlight.js";
export { default as Themes, type Theme } from "./themes.js"; export { default as Themes, type Theme } from "./themes.js";

26
pnpm-lock.yaml generated
View File

@ -351,7 +351,7 @@ importers:
dependencies: dependencies:
'@electron/remote': '@electron/remote':
specifier: 2.1.2 specifier: 2.1.2
version: 2.1.2(electron@36.2.1) version: 2.1.2(electron@36.3.1)
better-sqlite3: better-sqlite3:
specifier: ^11.9.1 specifier: ^11.9.1
version: 11.10.0 version: 11.10.0
@ -405,8 +405,8 @@ importers:
specifier: 13.0.0 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)) version: 13.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.4))
electron: electron:
specifier: 36.2.1 specifier: 36.3.1
version: 36.2.1 version: 36.3.1
prebuild-install: prebuild-install:
specifier: ^7.1.1 specifier: ^7.1.1
version: 7.1.3 version: 7.1.3
@ -461,8 +461,8 @@ importers:
specifier: 13.0.0 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)) version: 13.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.4))
electron: electron:
specifier: 36.2.1 specifier: 36.3.1
version: 36.2.1 version: 36.3.1
fs-extra: fs-extra:
specifier: 11.3.0 specifier: 11.3.0
version: 11.3.0 version: 11.3.0
@ -481,7 +481,7 @@ importers:
version: 7.1.1 version: 7.1.1
'@electron/remote': '@electron/remote':
specifier: 2.1.2 specifier: 2.1.2
version: 2.1.2(electron@36.2.1) version: 2.1.2(electron@36.3.1)
'@triliumnext/commons': '@triliumnext/commons':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/commons version: link:../../packages/commons
@ -627,8 +627,8 @@ importers:
specifier: 3.1.10 specifier: 3.1.10
version: 3.1.10 version: 3.1.10
electron: electron:
specifier: 36.2.1 specifier: 36.3.1
version: 36.2.1 version: 36.3.1
electron-debug: electron-debug:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
@ -7309,8 +7309,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
electron@36.2.1: electron@36.3.1:
resolution: {integrity: sha512-mm1Y+Ms46xcOTA69h8hpqfX392HfV4lga9aEkYkd/Syx1JBStvcACOIouCgGrnZpxNZPVS1jM8NTcMkNjuK6BQ==} resolution: {integrity: sha512-LeOZ+tVahmctHaAssLCGRRUa2SAO09GXua3pKdG+WzkbSDMh+3iOPONNVPTqGp8HlWnzGj4r6mhsIbM2RgH+eQ==}
engines: {node: '>= 12.20.55'} engines: {node: '>= 12.20.55'}
hasBin: true hasBin: true
@ -15656,9 +15656,9 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@electron/remote@2.1.2(electron@36.2.1)': '@electron/remote@2.1.2(electron@36.3.1)':
dependencies: dependencies:
electron: 36.2.1 electron: 36.3.1
'@electron/universal@2.0.2': '@electron/universal@2.0.2':
dependencies: dependencies:
@ -21262,7 +21262,7 @@ snapshots:
- supports-color - supports-color
optional: true optional: true
electron@36.2.1: electron@36.3.1:
dependencies: dependencies:
'@electron/get': 2.0.3 '@electron/get': 2.0.3
'@types/node': 22.15.21 '@types/node': 22.15.21