Skip to content

feat(backend): add the editor.markdown plugin extension point (WI-1787) - #444

Merged
stefan-ernst merged 1 commit into
mainfrom
wi-1787-editor-extension-point
Nov 3, 2026
Merged

stefan-ernst merged 1 commit into
mainfrom
wi-1787-editor-extension-point

Conversation

@stefan-ernst

Copy link
Copy Markdown
Contributor

Why

GH #349 asks for a plain-Markdown editing mode. Decision (see the item): don't add a native source mode to Milkdown — its serializer is lossy and would become the source of truth. Instead expose a plugin extension point so an installed plugin can render its own editor (e.g. a textarea), with the host remaining the only writer.

The editor plugin itself ships from a separate repository. This PR is only the core wiring: the manifest point, validation, the extensions endpoint, the bridge protocol, and the host component.

Backend

  • Extension gains mode (toggle default / replace) and entityTypes.
  • LoadPlugin validates editor.markdown manifests — component required, known entity types (item/page/comment), known mode — before compilation and again for the resolved manifest+metadata set.
  • GetExtensions now applies the same license gate as GetCapabilities, so an unlicensed plugin cannot advertise an editor provider.
  • GET /api/plugins/extensions accepts ?point= so an editor surface only receives the point it needs.

Frontend

  • Extensions store: ensureExtensionsLoaded() shares one in-flight request (the host mounts many times per page); getEditorProviders() filters by entity type and sorts by order.
  • PluginBridge: rejects messages whose event.source is not the bridge's own iframe; adds the editor protocol (host:editorInit/Update/SaveRequest/CancelRequest/Dispose and plugin:editorReady/Change/Save/Cancel/Error). Every plugin → host editor message must echo the sessionId, so two editors on one page cannot cross-talk. Legacy messages still route.
  • PluginEditorHost.svelte: resolves a provider, shows an opt-in toggle, mounts the plugin iframe, sends the current Markdown on ready, persists plugin:editorSave through the parent, confirms before discarding unsaved edits, refuses a save over 256 KB, and mirrors MilkdownEditor's imperative surface (insertImage/insertMarkdown no-op with a warning in plugin mode).
  • ItemDetailDescription opts in for the item description.

Acceptance covered

  • A manifest can declare editor.markdown and it appears in GET /api/plugins/extensions only when the plugin is enabled and licensed.
  • The host mounts the provider with entity/workspace context, persists the raw Markdown via the existing API, confirms before discarding unsaved edits, and leaves the native editor unchanged when no provider exists.

Not in this PR (separate repo / follow-ups)

  • The actual plaintext plugin (separate repository).
  • Page-editor integration (item description is the first surface).
  • insertImage/insertMarkdown delegation, mention/page-link pickers, persisted per-user preference, and the Playwright fixture plugin.

Verification

  • go build ./internal/..., go vet, golangci-lint (plugins/handlers) — 0 issues
  • internal/plugins/... — pass
  • internal/handlers/... — pass except the two pre-existing TestNeutralInferenceEndpoint... failures (fail on main)
  • bun run check, svelte-check — pass
  • Full ../core-tests frontend suite — 439 files, 3283 tests pass
  • New tests: core-tests#37 (Go validation/license gating; Vitest store + bridge)

Pairs with Windshiftapp/core-tests-internal#37.

Expose a plugin extension point so an installed plugin can render its own
Markdown editor (e.g. a plain textarea). The host stays the only writer: it
holds the content, validates it, and persists through the normal item/page
APIs. The editor plugin itself lives in a separate repository.

Backend:
- Extension gains mode (toggle/replace, default toggle) and entityTypes.
- LoadPlugin validates editor.markdown manifests (component required, known
  entity types, known mode) before compilation and again for the resolved
  manifest+metadata set.
- GetExtensions now applies the same license gate as GetCapabilities, so an
  unlicensed plugin cannot advertise an editor provider.
- GET /api/plugins/extensions accepts ?point= so an editor surface receives
  only the point it needs.

Frontend:
- extensions store: ensureExtensionsLoaded() shares one in-flight request;
  getEditorProviders() filters by entity type and sorts by order.
- PluginBridge: reject messages whose event.source is not the bridge's
  iframe; add the editor protocol (init/update/save/cancel/dispose and the
  plugin -> host counterparts), with every plugin message required to echo
  the sessionId so two editors on one page cannot cross-talk.
- PluginEditorHost.svelte: resolves a provider, shows an opt-in toggle,
  mounts the plugin iframe, forwards save/cancel to the parent, and mirrors
  MilkdownEditor's imperative surface. Unsaved edits are confirmed before
  switching, and a save over 256 KB is refused.
- ItemDetailDescription enables providers for the item description.

The react-demo manifest drops its unparsed displayMode field.
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Size Change: 0 B 🆕

Total Size: 0 B

compressed-size-action

@stefan-ernst
stefan-ernst merged commit 8aff824 into main Nov 3, 2026
21 checks passed
@stefan-ernst
stefan-ernst deleted the wi-1787-editor-extension-point branch November 3, 2026 12:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant