Skip to content

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

Install

Terminal window
pnpm add @citolab/prose-extensions

Usage

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 reusing prosemirror-schema-basic’s strong/em (so document JSON stays compatible with that schema), plus a toggleStrong / toggleEm command and Mod-b / Mod-i keymap binding.

  • defineList(options?: ListOptions)bullet_list / ordered_list / list_item node specs reusing prosemirror-schema-list’s parse/toDOM, a toggleBulletList / toggleOrderedList command pair, and a keymap (Enter splits the current item; Backspace is 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/table nodes, defineList (real nested ul/ol/li instead of ProseKit’s flat list node), defineEm/defineStrong (marks named em/strong instead of ProseKit’s bold/italic), plus the base keymap, base commands, history, and gap cursor. paragraph and table also join a richtext group so they’re usable inside qtiRubricBlock’s content: 'richtext+'. It exists because ProseKit’s own defineBasicExtension() (from prosekit/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 a paragraph, 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 own basic-extension.ts is 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.