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.
[![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.

View File

@ -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<HT
await linkService.loadReferenceLinkTitle($(el));
}
await applySyntaxHighlight($renderedContent);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote) {
await renderChildrenList($renderedContent, note);
}

View File

@ -1,6 +1,6 @@
import type FNote from "../entities/fnote.js";
import { getCurrentLanguage } from "./i18n.js";
import { applySyntaxHighlight } from "./syntax_highlight.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery<HTMLElement>) {
$img.attr("src", dir + "/" + $img.attr("src"));
});
applySyntaxHighlight($content);
formatCodeBlocks($content);
}
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 options from "./options.js";
import toast from "./toast.js";
import { t } from "./i18n.js";
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<HTMLElement>) {
if (!isSyntaxHighlightEnabled()) {
return;
export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
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 +25,31 @@ export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
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.
*/
@ -34,7 +57,7 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
$codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text();
let highlightedText = null;
let highlightedText: HighlightResult | AutoHighlightResult | null = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
await ensureMimeTypesForHighlighting();
highlightedText = highlightAuto(text);
@ -66,7 +89,7 @@ export async function ensureMimeTypesForHighlighting() {
export function loadHighlightingTheme(themeName: string) {
const themePrefix = "default:";
let theme = null;
let theme: Theme | null = null;
if (themeName.includes(themePrefix)) {
theme = Themes[themeName.substring(themePrefix.length)];
}

View File

@ -529,6 +529,26 @@ pre:not(.hljs) {
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 {
cursor: pointer;
}

View File

@ -1830,7 +1830,10 @@
"word_wrapping": "Word wrapping",
"theme_none": "No syntax highlighting",
"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": {
"title": "Formatting"

View File

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

View File

@ -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);
}
}

View File

@ -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<HTMLElement> | 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<FindResult>((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() {

View File

@ -12,7 +12,7 @@ import { createChatSession, checkSessionExists, setupStreamingResponse, getDirec
import { extractInChatToolSteps } from "./message_processor.js";
import { validateEmbeddingProviders } from "./validation.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";
@ -925,7 +925,7 @@ export default class LlmChatPanel extends BasicWidget {
// Apply syntax highlighting if this is the final update
if (isDone) {
applySyntaxHighlight($(assistantMessageEl as HTMLElement));
formatCodeBlocks($(assistantMessageEl as HTMLElement));
// Update message in the data model for storage
// 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
*/
import { marked } from "marked";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
/**
* Format markdown content for display
@ -62,7 +62,7 @@ export function escapeHtml(text: string): string {
* Apply syntax highlighting to content
*/
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 { applySyntaxHighlight } from "../../services/syntax_highlight.js";
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import type FNote from "../../entities/fnote.js";
import type { CommandListenerData, EventData } from "../../components/app_context.js";
import { getLocaleById } from "../../services/i18n.js";
@ -125,7 +125,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
}
await this.#applyInlineMermaid();
await applySyntaxHighlight(this.$content);
await formatCodeBlocks(this.$content);
}
async #applyInlineMermaid() {

View File

@ -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"
});
}

View File

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

View File

@ -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",

View File

@ -12,7 +12,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": {

View File

@ -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"
}
];

View File

@ -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",
@ -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

View File

@ -1,5 +1,5 @@
<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
see what values are supported.</p>
<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.).
* [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.
* 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

View File

@ -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
4. Merge them with the config from the file, with environment variables taking precedence

View File

@ -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",

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 "./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";

View File

@ -23,6 +23,8 @@ 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";
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.
@ -38,7 +40,9 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [
MarkdownImportPlugin,
IncludeNote,
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 { type Theme } from "./themes.js";
import { type HighlightOptions } from "highlight.js";
export type { HighlightResult, AutoHighlightResult } from "highlight.js";
export { default as Themes, type Theme } from "./themes.js";

26
pnpm-lock.yaml generated
View File

@ -351,7 +351,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
@ -405,8 +405,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))
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
@ -461,8 +461,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))
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
@ -481,7 +481,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
@ -627,8 +627,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
@ -7309,8 +7309,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
@ -15656,9 +15656,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:
@ -21262,7 +21262,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