Reviewing CKEditor 5 plugins in Trilium
A workflow and defect catalog for reviewing CKEditor 5 plugin code in the Trilium monorepo — a
distinct task from writing it. This skill orchestrates the review and hunts both CKEditor-specific
and Trilium-integration bugs; it delegates the per-dimension "is this idiomatic?" checklists to
the companion skills rather than duplicating them:
ckeditor5-plugin-development — its references/review-checklist.md (architecture, schema,
conversion, commands, UI, a11y) and references/conventions.md (naming, imports, JSDoc, TS).
ckeditor5-testing — its review checklist and patterns for the test side (Vitest, browser-mode
vs. @vitest/browser-playwright browser mode, real ClassicEditor.create).
Use those for "does this follow the conventions"; use this skill for how to drive the review
and what subtle things tend to be wrong.
Scope & sources
This skill reviews CKEditor 5 plugin code in the Trilium monorepo (scope @triliumnext/). The
CKEditor 5 library itself is the external ckeditor5 dependency, 48 or later — its docs
(ckeditor.com/docs) and source
(github.com/ckeditor/ckeditor5) are external references,
not the code under review. Nearly all Trilium plugins live inside packages/ckeditor5, under
src/plugins/<name>/ — admonition, collapsible, footnotes, keyboard_marker, mermaid and the
in-tree plugins alongside them. There are no separate CKEditor plugin packages left.
The aggregate assembles them all and is wired into the editor UI from apps/client.
On versions: these skills name major versions only ("48 or later"). Trilium tracks CKEditor
5 closely, so an exact pin written here would be stale within weeks — read the current one from
packages/ckeditor5/package.json.
When to use
Reviewing a PR or diff that touches a Trilium CKEditor 5 plugin (packages/ckeditor5/src/plugins/
), the aggregate itself, or the toolbar config in apps/client;
auditing an existing
plugin; sanity-checking your own feature before opening a PR. For writing the feature, use
ckeditor5-plugin-development; for writing tests, use ckeditor5-testing.
Review workflow
- Scope the diff against Trilium's structure. What surface changed — editing
(schema/conversion/command), UI (componentFactory/buttons/dropdowns/balloons), a widget, config?
Which plugin (
packages/ckeditor5/src/plugins/<name>/)? Does it
also touch the aggregate's wiring (packages/ckeditor5/src/plugins.ts, src/index.ts) or the toolbar
(apps/client/src/widgets/type_widgets/text/toolbar.ts)? This tells you which checklists, defect
groups, and integration checks apply.
- Read model-first. The model is the source of truth; trace the feature in order
schema → conversion → command → UI. Confirm each layer is present and consistent (e.g. a
new model element has a schema registration and upcast and downcast and an insertion path).
- Check registration & wiring (Trilium-specific). A correct plugin that nobody loads is still
broken. Confirm:
- the plugin is exported and added to the right array in
packages/ckeditor5/src/plugins.ts
(CORE_PLUGINS / TRILIUM_PLUGINS / EXTERNAL_PLUGINS → builtinPlugins) so it actually
loads into the editor classes in packages/ckeditor5/src/index.ts (AttributeEditor[Balloon],
ClassicEditor[Decoupled], PopupEditor[Balloon]);
- any new toolbar component is added to
apps/client/src/widgets/type_widgets/text/toolbar.ts,
otherwise the button never appears even though the plugin loaded.
- Check conventions (Trilium-specific). Imports come from the
ckeditor5 aggregate (or a
relative path inside packages/ckeditor5) with explicit file extensions; augmentation uses
declare module 'ckeditor5' (never @ckeditor/ckeditor5-core), normally at the bottom of the
plugin's glue file; any license header matches the sibling files in that plugin folder and the
provenance recorded in its README.md. These are enforced by eslint-config-ckeditor5
(require-file-extensions-in-imports, allow-imports-only-from-main-package-entry-point,
no-legacy-imports) and stylelint-config-ckeditor5 — a diff that breaks them fails lint.
- Run the tests for the affected package. Use Vitest via
pnpm --filter @triliumnext/ckeditor5 test (or ...-math); the two run sequentially because
each spins up headless Chromium. Both use @vitest/browser-playwright browser mode and
gate src/** at 100% coverage. Coverage ≠ correctness: confirm
the change itself is tested, not just that lines are hit. A bug fix with no new/changed test is
a red flag even when coverage stays green.
- Observe behavior. Attach the CKEditor Inspector (model / view / schema / commands), then:
round-trip
editor.getData() → setData() (does content survive a save?); exercise selection
edge cases (collapsed vs. ranged, inside objects/limits, at attribute boundaries); toggle
read-only.
- Apply the dimension checklists (delegate to the two companion skills).
- Hunt defects with
references/defect-patterns.md — the high-value, easy-to-miss failure
modes (CKEditor-general + Trilium integration).
- Verify each finding before reporting it (see below). Reproduce it, or cite the exact line
and the rule it breaks. Discard the ones that don't hold up.
- Report findings: severity-tagged,
file:line, with the concrete fix or question.
Triage / severity
- Blocker — data loss (content dropped on
getData()), crashes/console errors, undo
corruption, security (unsanitized HTML/XSS), schema corruption; plugin not registered in
plugins.ts (feature silently never loads).
- Major — incorrect behavior in real selections, accessibility gaps (no keyboard path, missing
labels/
addKeystrokeInfos), memory leaks, command enabled where the schema disallows it; button
missing from toolbar.ts; lint-failing imports (missing file extension / wrong package /
legacy @ckeditor/*) that block the build.
- Minor — convention/naming issues, missing
t(), redundant converters, non-idiomatic but
working code.
- Nit — style preferences with no behavioral impact; mark them as optional.
Lead with blockers/majors; don't bury them under nits.
Verify before reporting (avoid false positives)
A noisy review erodes trust. Confirm a finding is real before raising it. Common false
positives to not raise:
- "Missing downcast" when the code uses a two-way helper (
conversion.attributeToElement(...)
registers both directions) — count two-way helpers, not just for('dataDowncast').
- "
getAttribute('data-x') should be .dataset" inside a converter callback — that's a view
element, not DOM; .dataset would silently drop the value (see the plugin-dev conversion.md
view-element pitfall).
- "Boolean attribute should be set to
false" — the idiom is to remove it (yields undefined).
- "Button state not updated" when it's
bind()-ed to the command (reactive, not imperative).
- "Listener never removed" when added via
this.listenTo() (auto-cleaned in destroy()).
- "Plugin not registered" when it's pulled in transitively via another plugin's
static get requires() rather than listed directly in plugins.ts — check the dependency chain
before flagging.
- "Import should be from
@ckeditor/ckeditor5-*" — Trilium deliberately imports from the ckeditor5
aggregate (with file extensions); the deep @ckeditor/* path is the lint failure, not the fix.
When unsure, phrase it as a question ("Is <mark> intended to round-trip through getData()?
I don't see a dataDowncast.") rather than a false assertion.
Reference map
| File |
Use it for |
references/defect-patterns.md |
The bug catalog — CKEditor-general groups (conversion, schema, commands, UI, lifecycle, localization, undo, tests) plus a Trilium integration group. For each: the symptom, how to spot it in a diff, why it's wrong, and the fix. The core of a correctness review. |
Provenance & source references
The CKEditor-general defect patterns are distilled from the companion skills and the CKEditor 5
library at 48 or later; docs/… / packages/… paths in those patterns point into the
external CKEditor 5 repository (github.com/ckeditor/ckeditor5),
not the Trilium code under review. The Trilium-specific paths
(packages/ckeditor5/src/plugins/<name>/,
packages/ckeditor5/src/{plugins,index}.ts, apps/client/src/widgets/type_widgets/text/toolbar.ts)
point into the Trilium monorepo. To refresh, re-check those paths against the
current Trilium tree and bump the CKEditor version if the ckeditor5 pin changes.
1---2name: ckeditor5-reviewing3description: Review or audit CKEditor 5 plugin code in the Trilium (TriliumNext Notes) monorepo, or a PR/diff touching packages/ckeditor5 (including its in-tree plugins under src/plugins/). Use when checking a Trilium CKEditor 5 plugin for correctness and idiom: schema / conversion / command / UI / widget code, CKEditor-specific defects (asymmetric upcast/downcast, unconsumed upcast elements, missing inline-widget position mapping, command refresh/isEnabled bugs, memory leaks, t() gaps, editing/UI split violations), and Trilium integration defects (plugin not registered in plugins.ts, button missing from toolbar.ts, import/file-extension lint failures, wrong augmentation module, wrong DOM assumptions in tests). Pairs with the ckeditor5-plugin-development and ckeditor5-testing skills and delegates their checklists.4---56# Reviewing CKEditor 5 plugins in Trilium78A workflow and defect catalog for **reviewing** CKEditor 5 plugin code in the Trilium monorepo — a9distinct task from writing it. This skill orchestrates the review and hunts both CKEditor-specific10and Trilium-integration bugs; it **delegates the per-dimension "is this idiomatic?" checklists** to11the companion skills rather than duplicating them:1213- **`ckeditor5-plugin-development`** — its `references/review-checklist.md` (architecture, schema,14 conversion, commands, UI, a11y) and `references/conventions.md` (naming, imports, JSDoc, TS).15- **`ckeditor5-testing`** — its review checklist and patterns for the test side (Vitest, browser-mode16 vs. `@vitest/browser-playwright` browser mode, real `ClassicEditor.create`).1718Use those for "does this follow the conventions"; use this skill for **how to drive the review**19and **what subtle things tend to be wrong**.2021## Scope & sources2223This skill reviews **CKEditor 5 plugin code in the Trilium monorepo** (scope `@triliumnext/`). The24CKEditor 5 library itself is the **external `ckeditor5` dependency, 48 or later** — its docs25([ckeditor.com/docs](https://ckeditor.com/docs)) and source26([github.com/ckeditor/ckeditor5](https://github.com/ckeditor/ckeditor5)) are *external references*,27not the code under review. Nearly all Trilium plugins live **inside** `packages/ckeditor5`, under28`src/plugins/<name>/` — admonition, collapsible, footnotes, keyboard_marker, mermaid and the29in-tree plugins alongside them. There are no separate CKEditor plugin packages left.30The aggregate assembles them all and is wired into the editor UI from `apps/client`.3132**On versions:** these skills name **major versions only** ("48 or later"). Trilium tracks CKEditor335 closely, so an exact pin written here would be stale within weeks — read the current one from34`packages/ckeditor5/package.json`.3536## When to use3738Reviewing a PR or diff that touches a Trilium CKEditor 5 plugin (`packages/ckeditor5/src/plugins/`39), the aggregate itself, or the toolbar config in `apps/client`;40auditing an existing41plugin; sanity-checking your own feature before opening a PR. For *writing* the feature, use42`ckeditor5-plugin-development`; for *writing tests*, use `ckeditor5-testing`.4344## Review workflow45461. **Scope the diff against Trilium's structure.** What surface changed — editing47 (schema/conversion/command), UI (componentFactory/buttons/dropdowns/balloons), a widget, config?48 Which plugin (`packages/ckeditor5/src/plugins/<name>/`)? Does it49 also touch the aggregate's wiring (`packages/ckeditor5/src/plugins.ts`, `src/index.ts`) or the toolbar50 (`apps/client/src/widgets/type_widgets/text/toolbar.ts`)? This tells you which checklists, defect51 groups, and integration checks apply.522. **Read model-first.** The model is the source of truth; trace the feature in order53 **schema → conversion → command → UI**. Confirm each layer is present and consistent (e.g. a54 new model element has a schema registration *and* upcast *and* downcast *and* an insertion path).553. **Check registration & wiring (Trilium-specific).** A correct plugin that nobody loads is still56 broken. Confirm:57 - the plugin is exported and added to the right array in `packages/ckeditor5/src/plugins.ts`58 (`CORE_PLUGINS` / `TRILIUM_PLUGINS` / `EXTERNAL_PLUGINS` → `builtinPlugins`) so it actually59 loads into the editor classes in `packages/ckeditor5/src/index.ts` (AttributeEditor[Balloon],60 ClassicEditor[Decoupled], PopupEditor[Balloon]);61 - any new toolbar component is added to `apps/client/src/widgets/type_widgets/text/toolbar.ts`,62 otherwise the button never appears even though the plugin loaded.634. **Check conventions (Trilium-specific).** Imports come from the `ckeditor5` aggregate (or a64 relative path inside `packages/ckeditor5`) **with explicit file extensions**; augmentation uses65 `declare module 'ckeditor5'` (never `@ckeditor/ckeditor5-core`), normally at the bottom of the66 plugin's glue file; any license header matches the sibling files in that plugin folder and the67 provenance recorded in its `README.md`. These are enforced by `eslint-config-ckeditor5`68 (`require-file-extensions-in-imports`, `allow-imports-only-from-main-package-entry-point`,69 `no-legacy-imports`) and `stylelint-config-ckeditor5` — a diff that breaks them fails lint.705. **Run the tests for the affected package.** Use Vitest via71 `pnpm --filter @triliumnext/ckeditor5 test` (or `...-math`); the two run sequentially because72 each spins up headless Chromium. Both use **`@vitest/browser-playwright` browser mode** and73 gate `src/**` at **100% coverage**. Coverage ≠ correctness: confirm74 the *change itself* is tested, not just that lines are hit. A bug fix with no new/changed test is75 a red flag even when coverage stays green.766. **Observe behavior.** Attach the CKEditor Inspector (model / view / schema / commands), then:77 round-trip `editor.getData()` → `setData()` (does content survive a save?); exercise selection78 edge cases (collapsed vs. ranged, inside objects/limits, at attribute boundaries); toggle79 read-only.807. **Apply the dimension checklists** (delegate to the two companion skills).818. **Hunt defects** with `references/defect-patterns.md` — the high-value, easy-to-miss failure82 modes (CKEditor-general + Trilium integration).839. **Verify each finding before reporting it** (see below). Reproduce it, or cite the exact line84 and the rule it breaks. Discard the ones that don't hold up.8510. **Report** findings: severity-tagged, `file:line`, with the concrete fix or question.8687## Triage / severity8889- **Blocker** — data loss (content dropped on `getData()`), crashes/console errors, undo90 corruption, security (unsanitized HTML/XSS), schema corruption; **plugin not registered in91 `plugins.ts`** (feature silently never loads).92- **Major** — incorrect behavior in real selections, accessibility gaps (no keyboard path, missing93 labels/`addKeystrokeInfos`), memory leaks, command enabled where the schema disallows it; **button94 missing from `toolbar.ts`**; **lint-failing imports** (missing file extension / wrong package /95 legacy `@ckeditor/*`) that block the build.96- **Minor** — convention/naming issues, missing `t()`, redundant converters, non-idiomatic but97 working code.98- **Nit** — style preferences with no behavioral impact; mark them as optional.99100Lead with blockers/majors; don't bury them under nits.101102## Verify before reporting (avoid false positives)103104A noisy review erodes trust. Confirm a finding is real before raising it. Common **false105positives** to *not* raise:106107- "Missing downcast" when the code uses a **two-way** helper (`conversion.attributeToElement(...)`108 registers both directions) — count two-way helpers, not just `for('dataDowncast')`.109- "`getAttribute('data-x')` should be `.dataset`" inside a **converter callback** — that's a view110 element, not DOM; `.dataset` would silently drop the value (see the plugin-dev `conversion.md`111 view-element pitfall).112- "Boolean attribute should be set to `false`" — the idiom is to **remove** it (yields `undefined`).113- "Button state not updated" when it's `bind()`-ed to the command (reactive, not imperative).114- "Listener never removed" when added via `this.listenTo()` (auto-cleaned in `destroy()`).115- "Plugin not registered" when it's pulled in transitively via another plugin's116 `static get requires()` rather than listed directly in `plugins.ts` — check the dependency chain117 before flagging.118- "Import should be from `@ckeditor/ckeditor5-*`" — Trilium deliberately imports from the `ckeditor5`119 aggregate (with file extensions); the deep `@ckeditor/*` path is the *lint failure*, not the fix.120121When unsure, phrase it as a **question** ("Is `<mark>` intended to round-trip through `getData()`?122I don't see a `dataDowncast`.") rather than a false assertion.123124## Reference map125126| File | Use it for |127|------|-----------|128| `references/defect-patterns.md` | The bug catalog — CKEditor-general groups (conversion, schema, commands, UI, lifecycle, localization, undo, tests) plus a **Trilium integration** group. For each: the symptom, how to spot it in a diff, why it's wrong, and the fix. The core of a correctness review. |129130## Provenance & source references131132The CKEditor-general defect patterns are distilled from the companion skills and the CKEditor 5133library at **48 or later**; `docs/…` / `packages/…` paths in those patterns point into the134**external CKEditor 5 repository** ([github.com/ckeditor/ckeditor5](https://github.com/ckeditor/ckeditor5)),135not the Trilium code under review. The Trilium-specific paths136(`packages/ckeditor5/src/plugins/<name>/`,137`packages/ckeditor5/src/{plugins,index}.ts`, `apps/client/src/widgets/type_widgets/text/toolbar.ts`)138point into the **Trilium monorepo**. To refresh, re-check those paths against the139current Trilium tree and bump the CKEditor version if the `ckeditor5` pin changes.