Skip to main content

ckeditor5-plugin-development

Write, extend, and review CKEditor 5 plugins in the Trilium (TriliumNext Notes) monorepo — the rich-text-note editor under packages/ckeditor5, whose plugins live in src/plugins/. Use when building or reviewing a Trilium CKEditor 5 feature/plugin, or when working with the editing engine (model, view, schema, conversion/upcast-downcast), commands, the UI library (buttons, dropdowns, dialogs, balloons, toolbars), widgets (block/inline, toWidget, nested editables), keystrokes, localization (t()), registering a plugin into plugins.ts / the editor classes / toolbar.ts, persisting a `data-trilium-*` attribute through the model→view→data→markdown→share pipeline, or adding a new plugin folder under src/plugins/. Covers the architecture, idiomatic patterns, Trilium packaging/registration, code-style conventions, and a review checklist.

Quellinformationen

Repository
TriliumNext/Trilium
Letzte Quellaktivität
3. Oktober 2026 um 12:15
Erkannte Sprache von SKILL.md
Englisch
Sterne
38.213
Forks
2.569

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
12 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
ckeditor5-plugin-development
description
Write, extend, and review CKEditor 5 plugins in the Trilium (TriliumNext Notes) monorepo — the rich-text-note editor under packages/ckeditor5, whose plugins live in src/plugins/. Use when building or reviewing a Trilium CKEditor 5 feature/plugin, or when working with the editing engine (model, view, schema, conversion/upcast-downcast), commands, the UI library (buttons, dropdowns, dialogs, balloons, toolbars), widgets (block/inline, toWidget, nested editables), keystrokes, localization (t()), registering a plugin into plugins.ts / the editor classes / toolbar.ts, persisting a `data-trilium-*` attribute through the model→view→data→markdown→share pipeline, or adding a new plugin folder under src/plugins/. Covers the architecture, idiomatic patterns, Trilium packaging/registration, code-style conventions, and a review checklist.
# CKEditor 5 plugin development (Trilium monorepo) CKEditor 5 is **plugin-based**: every feature — even typing and `<p>` support — is a plugin. Without plugins the editor is an empty API. This skill is specific to **Trilium (TriliumNext Notes)**, whose rich-text note editor is built from the CKEditor 5 library (external dep, **CKEditor 5 48 or later**) plus Trilium's own plugins. Both the editor build and the plugins live in `packages/ckeditor5` (`@triliumnext/ckeditor5`): each feature is a folder under `src/plugins/` — admonition, collapsible, footnotes, keyboard_marker, math, mermaid, mention, snippets and the rest — with its tests co-located beside it. No CKEditor feature ships as its own workspace package any more. The editor is consumed by `apps/client` (the text note widget). This skill distills how to write new Trilium plugins and review existing ones idiomatically. ## When to use this skill Use it whenever the task involves a Trilium CKEditor 5 plugin/feature: creating one (a folder under `packages/ckeditor5/src/plugins/` — separate packages are not the pattern any more), extending one, debugging editing behavior, registering a plugin so it reaches the editor, or reviewing plugin code for correctness and convention compliance. Trigger concepts include: model/view/schema, conversion (upcast/downcast), `Command`, `editor.model.change()`, `ButtonView`/`componentFactory`, widgets (`toWidget`), `ContextualBalloon`/`Dialog`, `editor.keystrokes`, `t()` localization, the `plugins.ts` registry / editor classes / `toolbar.ts`. ## The three pillars These are the library's internal layers (upstream packages `ckeditor5-core`/`-engine`/`-ui`); in Trilium you never import them by those paths — everything comes from the `ckeditor5` aggregate (see below). They describe how the engine is organized: 1. **Core editor architecture** (library `ckeditor5-core`) — glue classes: `Editor`, `Plugin`, `Command`, plus the event/observable system. 2. **Editing engine** (library `ckeditor5-engine`) — the custom MVC data **model**, the **view** (virtual DOM), **schema**, and **conversion** between them. The biggest piece. 3. **UI library** (library `ckeditor5-ui`) — MVC views, templates, and components (buttons, dropdowns, dialogs, toolbars). Mental model of the engine: there is **one model document** that is **converted** into two views — the **editing view** (what the user sees/edits) and the **data view** (input/output for `getData()`/`setData()`/paste). You almost always change the **model**; converters render it to the view. Never hand-edit the view to represent model state. ``` data (HTML) ──upcast──▶ MODEL ──editing downcast──▶ editing view ──render──▶ DOM (contentEditable) │ └────data downcast──────▶ data view ──▶ getData()/output HTML ``` ## Importing CKEditor in Trilium Import everything from the single **`ckeditor5`** aggregate package (**48 or later**; it is a `peerDependency` + `devDependency` of every plugin package). There is no premium package: every premium plugin Trilium used has an in-tree GPL replacement, and the editor always runs under the `GPL` license key: ```ts import { Plugin, ButtonView, Command, _setModelData } from 'ckeditor5'; ``` - **Cross-plugin** imports inside `packages/ckeditor5` are **relative**, e.g. `import Kbd from './keyboard_marker/keyboard_marker.js';`. The only workspace-package import in-tree under `src/plugins/`, and the aggregate registers them from `plugins.ts`. - **Every import includes its file extension** (`.js`/`.ts`/`.json`) — enforced by `eslint-config-ckeditor5` (`require-file-extensions-in-imports`), with `allow-imports-only-from-main-package-entry-point` and `no-legacy-imports` also active. - The `@ckeditor/ckeditor5-*` deep paths you'll see in the library's own source (and cited in `references/core-plugin-patterns.md`) resolve to the same symbols, but in Trilium you always import from the `ckeditor5` aggregate to avoid duplicate-module-instance problems. The only routine exceptions are dev/debug packages: `@ckeditor/ckeditor5-icons` and the **CKEditor Inspector** (`import CKEditorInspector from '@ckeditor/ckeditor5-inspector';`). ## Plugin anatomy A plugin `extends Plugin` (from `'ckeditor5'`). There is **no** `isOfficialPlugin`/`isPremiumPlugin` flag in Trilium plugins. (License headers are not uniform across packages — some, e.g. admonition, prefix files with a CKSource header; others don't. Match the package you're in; see `references/conventions.md`.) ```ts import { Plugin } from 'ckeditor5'; import FooEditing from './fooediting.js'; import FooUI from './fooui.js'; export default class Foo extends Plugin { // Dependencies — the editor loads these automatically before this plugin. static get requires() { return [ FooEditing, FooUI ] as const; } // Stable name (PascalCase = the package/folder) for editor.plugins.get( 'Foo' ). static get pluginName() { return 'Foo' as const; } init() { const editor = this.editor; // the editor that loaded this plugin // Register schema, converters, commands, UI, keystrokes, listeners… } afterInit() { // Runs after ALL plugins' init(). Use it when you depend on another // plugin's runtime state (e.g. registering a widget toolbar). } // init()/afterInit() may return a Promise. Plugin extends a base that provides // destroy() and this.listenTo()/this.stopListening() (auto-cleaned on destroy). } ``` Plugin folder layout (e.g. `packages/ckeditor5/src/plugins/admonition/`), all files flat and snake_case: `{feature}.ts` glue, `{feature}_editing.ts`, `{feature}_ui.ts`, optional `{feature}_command.ts`. Put the `declare module 'ckeditor5'` augmentation at the **bottom of the glue file**: every folded-in plugin dropped its separate `augmentation.ts` and `index.ts` barrel that way, so consumers import the specific module they need. (`syntax_highlighting` still uses the older separate-file shape — leave it, but don't copy it.) Complex plugins add `constants.ts` (`ELEMENTS`/`ATTRIBUTES`/`COMMANDS`/`CLASSES`), `utils.ts` (model-query helpers), and split `schema.ts`/`converters.ts`. Tests sit beside the source as `*.spec.ts`. Assets live in the package's shared folders, not per plugin: stylesheets in `packages/ckeditor5/src/theme/{feature}.css` (imported from the glue plugin) and icons in `packages/ckeditor5/src/icons/`, prefixed where the name would otherwise be generic (`mermaid-info.svg`). A plugin derived from third-party code also keeps a `README.md` recording its provenance, and a `LICENSE.md` where upstream requires one. See `references/conventions.md`. Key rules (inherited from the upstream conventions via `eslint-config-ckeditor5`): - Every feature is a plugin; plugins are **highly granular** and should know **as little about other plugins as possible** (communicate via commands, events, and the schema). - **Split editing from UI.** The standard pattern is three plugins: - `Feature` — the **glue** plugin: `static get requires() { return [ FeatureEditing, FeatureUI ] as const; }` - `FeatureEditing` — schema, conversion, commands (works headless / server-side). - `FeatureUI` — buttons, dropdowns, balloons registered in `componentFactory`. This enables reuse (someone can take your editing layer and write a different UI). Simple text-attribute features can reuse the built-in `AttributeCommand` inline (see keyboard-marker). - Register UI in `editor.ui.componentFactory.add( 'name', locale => view )`, then the component `'name'` is added to Trilium's toolbar config (`apps/client/.../text/toolbar.ts`). - Make features self-configuring: pre-configure the schema and provide config defaults via `editor.config.define( 'feature', { … } )`, read with `editor.config.get( 'feature.key' )`. - SVG icons are imported with `?raw` straight into the file that uses them (`import fooIcon from '../../icons/foo.svg?raw';`); there is no `icons` export any more. - Every editor icon, CKEditor's and ours, is also a glyph in the built-in `cke` icon font that the User Guide uses to name toolbar buttons (`cke-table-merge-cell`, `cke-trilium-kbd`). Adding, renaming or removing an SVG, or bumping `ckeditor5`, means regenerating that font and checking the docs for the old class. See "The `cke` icon pack" in `references/ui-and-localization.md`. ## Minimal end-to-end example (inline text attribute) A "highlight" feature = a `$text` attribute ↔ `<mark>` element, a command, a button, a keystroke. This is the canonical shape for inline styling features. ```js import { Plugin, Command, ButtonView } from 'ckeditor5'; class HighlightCommand extends Command { refresh() { const { document, schema } = this.editor.model; this.value = document.selection.getAttribute( 'highlight' ); this.isEnabled = schema.checkAttributeInSelection( document.selection, 'highlight' ); } execute() { const model = this.editor.model; const selection = model.document.selection; const newValue = !this.value; model.change( writer => { if ( !selection.isCollapsed ) { for ( const range of model.schema.getValidRanges( selection.getRanges(), 'highlight' ) ) { newValue ? writer.setAttribute( 'highlight', true, range ) : writer.removeAttribute( 'highlight', range ); } } newValue ? writer.setSelectionAttribute( 'highlight', true ) : writer.removeSelectionAttribute( 'highlight' ); } ); } } export default class Highlight extends Plugin { init() { const editor = this.editor; // 1. Schema: allow the attribute on text. editor.model.schema.extend( '$text', { allowAttributes: 'highlight' } ); // 2. Conversion: model attribute 'highlight' <-> view <mark>. editor.conversion.attributeToElement( { model: 'highlight', view: 'mark' } ); // 3. Command. editor.commands.add( 'highlight', new HighlightCommand( editor ) ); // 4. UI button, reactive to command state. editor.ui.componentFactory.add( 'highlight', locale => { const button = new ButtonView( locale ); const command = editor.commands.get( 'highlight' ); button.set( { label: editor.t( 'Highlight' ), withText: true, isToggleable: true, tooltip: true } ); button.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); button.on( 'execute', () => { editor.execute( 'highlight' ); editor.editing.view.focus(); } ); return button; } ); // 5. Keystroke. editor.keystrokes.set( 'Ctrl+Alt+H', 'highlight' ); } } ``` The same five steps (schema → conversion → command → UI → keystroke) recur in almost every feature. For elements/objects/widgets you `schema.register(...)` and use `elementToElement` converters instead of `attributeToElement`; see `references/widgets.md`. ## Development workflow 1. **Write the plugin.** A folder under `packages/ckeditor5/src/plugins/`. Separate workspace packages are no longer the pattern — every one that existed has been folded in, since none had consumers outside the aggregate or was ever published. See `references/tooling-and-packaging.md` ("Where a new plugin goes"). 2. **Register it so it reaches the editor** (full flow in `references/tooling-and-packaging.md`): - For a new workspace package, add `"@triliumnext/ckeditor5-<feature>": "workspace:*"` to `packages/ckeditor5/package.json`. - Import it in `packages/ckeditor5/src/plugins.ts` and add it to the right array — `CORE_PLUGINS` (minimal/attribute editor), `TRILIUM_PLUGINS` (in-repo `src/plugins/`), or `EXTERNAL_PLUGINS` (the `@triliumnext` workspace packages). These compose into `COMMON_PLUGINS`, which the editor classes in `packages/ckeditor5/src/index.ts` expose as `static builtinPlugins`. - Add the component name to the toolbar in `apps/client/src/widgets/type_widgets/text/toolbar.ts`. - **Always reach for the CKEditor 5 Inspector** while developing — it shows the live model, view, schema, commands, and selection. `import CKEditorInspector from '@ckeditor/ckeditor5-inspector'; CKEditorInspector.attach( editor );` - **Change the model, not the DOM.** Wrap all model mutations in `editor.model.change( writer => … )` (one block = one undo step). Use `editor.editing.view.change()` only for view-only state (e.g. focus class) that the model does not represent. - **Lint & test per package** with pnpm workspace filters: `pnpm --filter @triliumnext/ckeditor5-<feature> test` (also `lint`, `stylelint`, `test:debug`). - **Verify** with `editor.getData()` / `editor.setData()` and by exercising selection edge cases (collapsed vs. ranged, inside objects/limits). - **A changed plugin won't apply to an already-open editor via HMR.** A plugin's `init()` runs only when the editor is *built*, so do a **full page reload** (or close/reopen the note) to get a fresh editor instance that picks up your change — otherwise you're testing the old code. ## Reference map Load the focused reference for the task at hand: | File | Use it for | |------|-----------| | `references/architecture.md` | Model, view, schema, positions/ranges/selections, markers, the event/observable system, binding. The conceptual foundation. | | `references/conversion.md` | Upcast/downcast pipelines, conversion helpers, custom (callback) converters, attribute/element/marker conversion, position mapping. | | `references/commands.md` | `Command` patterns: `refresh()`/`execute()`, state (`value`/`isEnabled`), `forceDisabled()`, `affectsData`, command events. | | `references/ui-and-localization.md` | Views & templates, component catalog (buttons, inputs, dropdowns, dialogs/modals, balloons, toolbars), icons, `componentFactory`, focus/keystroke management, and `t()` localization. | | `references/widgets.md` | Block & inline widgets: `toWidget`/`toWidgetEditable`, nested editables, `insertObject`, widget toolbars, view↔model position mapping, custom properties, and external/async-rendered widgets (UI-element render callbacks, re-render on change, stale-render guard, lazy-load). |
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen