ProseMirror Plugins
@citolab/prose-extensions provides generic editor behavior that sits alongside @citolab/prose-qti. Use it when you need block selection, node attribute synchronization, semantic paste cleanup, or schema-gap detection that does not depend on QTI semantics.
Available plugins
- Block Select - block node selection and drag handles
- Node Attrs Sync - keeps node attributes in sync with the DOM
- Semantic Paste - normalizes pasted HTML into semantic structure
- Schema Gaps - finds and preserves markup a schema cannot represent
Install
pnpm add @citolab/prose-extensionsUsage
Each feature lives under its own subpath export. These subpaths only ever export plain ProseMirror plugins, so @citolab/prose-extensions works without ProseKit installed:
import { blockSelectPlugin } from '@citolab/prose-extensions/block-select';import { nodeAttrsSyncPlugin } from '@citolab/prose-extensions/node-attrs-sync';import { createSemanticPastePlugin } from '@citolab/prose-extensions/paste-semantic-html';If your app is built on ProseKit, import the extension wrappers from @citolab/prose-extensions/prosekit-extensions instead. Importing from this subpath requires the prosekit peer dependency to be installed — it is optional everywhere else in the package:
import { blockSelectExtension, nodeAttrsSyncExtension, defineSemanticPasteExtension,} from '@citolab/prose-extensions/prosekit-extensions';See each plugin’s page for the exact shape of its plugin factory and, where available, its ProseKit extension.
The schema-version compatibility/migration pipeline and local-storage doc persistence used to be documented here, but they now live inside the editor application (its own repository, not published) since only that app persists raw ProseMirror JSON. The virtual cursor plugin was removed — it had no consumers.
ProseKit marks and lists
@citolab/prose-extensions/prosekit publishes standalone ProseKit wrappers for basic rich-text marks and lists, for apps that don’t already define them. Importing this subpath requires the prosekit peer dependency:
import { defineStrong, defineEm, defineList, defineBasicExtension } from '@citolab/prose-extensions/prosekit';-
defineStrong()/defineEm()— mark specs reusingprosemirror-schema-basic’sstrong/em(so document JSON stays compatible with that schema), plus atoggleStrong/toggleEmcommand andMod-b/Mod-ikeymap binding. -
defineList(options?: ListOptions)—bullet_list/ordered_list/list_itemnode specs reusingprosemirror-schema-list’s parse/toDOM, atoggleBulletList/toggleOrderedListcommand pair, and a keymap (Entersplits the current item;Backspaceis restored to ProseMirror’s standard join/lift chain). Pass{ inputRules: true }to enable markdown-style-/1.input rules. -
defineBasicExtension(options?: BasicExtensionOptions)— the shared QTI-shaped ProseKit base every ProseKit editor in this repo builds on:doc/text/paragraph/heading/image/tablenodes,defineList(real nestedul/ol/liinstead of ProseKit’s flat list node),defineEm/defineStrong(marks namedem/stronginstead of ProseKit’sbold/italic), plus the base keymap, base commands, history, and gap cursor.paragraphandtablealso join arichtextgroup so they’re usable insideqtiRubricBlock’scontent: 'richtext+'. It exists because ProseKit’s owndefineBasicExtension()(fromprosekit/basic) is unusable for QTI on those two counts — flat list markup and mismatched mark names — and is not a superset you can patch after the fact. Typing at a gap cursor also reliably opens aparagraph, not whichever textblock happens to be registered first in the schema.import { createEditor, union } from 'prosekit/core';import { defineBasicExtension } from '@citolab/prose-extensions/prosekit';const editor = createEditor({extension: union(defineBasicExtension(), /* ...QTI interactions, app-specific extras */),});Apps layer their own additions on top with
union()— hard break, virtual selection, mod-click prevention, AI extensions, etc. — rather than forking this base. The editor application’s ownbasic-extension.tsis the reference for that pattern.
If you assemble a plain ProseMirror schema by hand instead of through ProseKit, use qtiBasicNodes/qtiBasicMarks from QTI Base Schema instead — same underlying image-dimension fix, no ProseKit dependency.
When to use this
Use these helpers when you need editor behavior that is independent of QTI semantics. QTI-specific integration surfaces live in @citolab/prose-qti.