Skip to content

feat(text-editor): own ProseMirror schema assembly#3988

Draft
john-traas wants to merge 11 commits intomainfrom
feat/custom-prosemirror-schema
Draft

feat(text-editor): own ProseMirror schema assembly#3988
john-traas wants to merge 11 commits intomainfrom
feat/custom-prosemirror-schema

Conversation

@john-traas
Copy link
Copy Markdown
Contributor

@john-traas john-traas commented Mar 30, 2026

Summary

  • Replace prosemirror-example-setup with explicit, owned schema and plugin assembly
  • Create schema/ module (nodes.ts, marks.ts, index.ts) that cherry-picks specs from prosemirror-schema-basic instead of importing the pre-built schema object
  • Create plugins/input-rules.ts and plugins/base-plugins.ts to replace the opaque exampleSetup() call with direct imports from stable ProseMirror packages
  • Fix image node missing draggable: true and title attribute (existing oversight discovered during audit)
  • Fix key casing in MenuCommandFactory.buildKeymap()Mod-B (uppercase) only matched Cmd+Shift+B, not plain Cmd+B

Why

prosemirror-example-setup is explicitly intended for demos, not production use. We were passing menuBar: false but still getting everything else as a black box. Our custom link and image specs were silently replacing the basic ones via OrderedMap.append() — not obvious from the code. This change makes every node, mark, and plugin in the editor explicit and auditable.

Dependencies

  • Removed: prosemirror-example-setup
  • Added (were transitive, now direct): prosemirror-inputrules, prosemirror-dropcursor, prosemirror-gapcursor, prosemirror-history, prosemirror-commands, prosemirror-keymap, prosemirror-state, prosemirror-view, prosemirror-schema-list

Test plan

  • Build passes (npm run build)
  • Lint passes (npm run lint)
  • Schema assembly tests pass
  • Manual: typing, bold/italic/strikethrough, headings, lists, blockquote, code block
  • Manual: auto-formatting shortcuts (> , 1. , # , ```)
  • Manual: undo/redo (Cmd+Z / Cmd+Shift+Z)
  • Manual: link insertion, image paste
  • Manual: Shift+Enter for hard break
  • Manual: HTML mode includes tables

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Image nodes now support dragging and optional title attributes
    • Added lowercase keyboard shortcuts (Mod+b, Mod+i, Mod+Shift+x, Mod+Shift+c) as alternatives to uppercase formatting variants

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f981b712-9122-4057-bb55-4e1719109790

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors ProseMirror schema and plugin setup by replacing prosemirror-example-setup with explicit, schema-aware modules for schema construction, input rules, and base plugins. Promotes ProseMirror packages to direct dependencies. Enhances the image node with title attribute and draggable support.

Changes

Cohort / File(s) Summary
Schema construction
src/components/text-editor/prosemirror-adapter/schema/nodes.ts, schema/marks.ts, schema/index.ts, schema/schema.spec.ts
Introduces new schema modules that explicitly assemble node and mark specs from ProseMirror packages with custom editor-specific pieces; conditionally adds list and table nodes; comprehensive test suite validates expected nodes, marks, and conditional behavior.
Plugin assembly
src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts, plugins/input-rules.ts
Adds schema-aware plugin modules that conditionally register input rules and keybindings based on node/mark presence in the provided schema; replaces exampleSetup() usage with explicit plugin composition.
Adapter refactor
src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx
Delegates schema creation to new createSchema() and plugin assembly to buildBasePlugins(), removing direct mutation of prosemirror-schema-basic and exampleSetup() call.
Image node enhancements
src/components/text-editor/prosemirror-adapter/plugins/image/node.ts
Adds optional title attribute with DOM parsing and serialization support; marks image node as draggable: true.
Keybinding updates
src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts
Adds lowercase keyboard shortcut variants (Mod-b, Mod-i, Mod-Shift-x, Mod-Shift-c) alongside existing uppercase equivalents.
Package dependencies
package.json
Removes prosemirror-example-setup from devDependencies; promotes nine ProseMirror packages to direct runtime dependencies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jgroth
  • adrianschmidt
  • omaralweli
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: taking ownership of ProseMirror schema assembly by removing the external setup dependency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/custom-prosemirror-schema

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-3988/

@john-traas john-traas marked this pull request as draft March 30, 2026 20:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/plans/2026-03-30-custom-prosemirror-schema.md`:
- Line 34: Update the table summary row that currently reads "`package.json` |
Remove `prosemirror-example-setup`, add 4 direct deps`" to match Task 8 by
changing "4" to "8" (or alternatively adjust Task 8 to only install 4 packages);
ensure the phrase exactly reflects the action in Task 8 so the docs and the task
remain consistent.
- Line 822: The inline code spans in the sentence containing examples like `> `,
`1. `, and `# ` include trailing spaces which trigger markdownlint MD038; update
that line (the sentence "Auto-formatting shortcuts work (type `> ` for
blockquote, `1. ` for ordered list, `# ` for heading)") to remove spaces inside
the backticks so the spans read `>`, `1.`, and `#` respectively, preserving the
text and punctuation around them.

In `@package.json`:
- Around line 126-137: The project relies on the external module "orderedmap"
(imported in nodes.ts and marks.ts) but it is not declared in package.json,
causing brittle transitive-dependency behavior; add "orderedmap" to the
"dependencies" block in package.json (e.g., "orderedmap": "^1.0.0") and run
install to ensure the module is explicitly installed so imports in
src/components/text-editor/prosemirror-adapter/schema/nodes.ts and
src/components/text-editor/prosemirror-adapter/schema/marks.ts resolve reliably.

In `@src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts`:
- Around line 352-363: The menu shortcuts (e.g.,
'Mod-b','Mod-B','Mod-i','Mod-I','Mod-Shift-1','Mod-Shift-2','Mod-Shift-3','Mod-Shift-x','Mod-Shift-X','Mod-`','Mod-Shift-c','Mod-Shift-C')
are shadowed by the base keymap built in buildEditorKeymap(...), so
MenuCommandFactory handlers never run; fix by making precedence explicit: either
move the plugin registration of keymap(this.menuCommandFactory.buildKeymap()) to
run before the base keymap plugin (so MenuCommandFactory's buildKeymap takes
priority), or remove those overlapping entries from
menuCommandFactory.buildKeymap() so they are only present in
buildEditorKeymap(); update the code paths that construct/install the plugins
(where keymap(...) and buildEditorKeymap(...) are invoked) and adjust tests if
any.

In `@src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts`:
- Around line 38-40: The redo mapping lacks the common Windows/Linux shortcut;
in the keys mapping inside base-plugins.ts add a mapping for 'Mod-y' to the
existing redo handler so Ctrl+Y (or Cmd+Y on mac where appropriate) triggers
redo; specifically update the keys object (where keys['Shift-Mod-z'] = redo and
keys['Backspace'] = undoInputRule are defined) to include keys['Mod-y'] = redo
to provide parity across platforms.

In `@src/components/text-editor/prosemirror-adapter/plugins/image/node.ts`:
- Around line 123-125: The image HTML builder currently interpolates raw attrs
(attrs.title, attrs.src, attrs.alt) into the returned string (where
titleAttribute and styleAttribute are composed), which allows attribute
injection; fix it by running all serialized attribute values through an
HTML-attribute-escaping helper (e.g., escapeHtmlAttribute or similar) before
composing titleAttribute/styleAttribute and the final `<img ... />` string so
quotes, ampersands and angle brackets are encoded and the attributes remain
safe; update uses in the template-returning function to reference the escaped
values instead of raw attrs.

In `@src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx`:
- Around line 403-405: The custom keymap from menuCommandFactory.buildKeymap is
being shadowed by the base keymap created in buildBasePlugins (which registers
buildEditorKeymap) because plugin order matters; move the
keymap(keymap(this.menuCommandFactory.buildKeymap())) to appear before
...buildBasePlugins(this.schema) so the custom bindings (Mod-b, Mod-i, Mod-`)
are checked first, ensuring createTriggerPlugin and other plugins remain in the
same relative order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7c30d255-abbf-4e7a-9f4e-ea3f52e0c907

📥 Commits

Reviewing files that changed from the base of the PR and between 7c5cc57 and 18004b2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • docs/superpowers/plans/2026-03-30-custom-prosemirror-schema.md
  • package.json
  • src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts
  • src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts
  • src/components/text-editor/prosemirror-adapter/plugins/image/node.ts
  • src/components/text-editor/prosemirror-adapter/plugins/input-rules.ts
  • src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx
  • src/components/text-editor/prosemirror-adapter/schema/index.ts
  • src/components/text-editor/prosemirror-adapter/schema/marks.ts
  • src/components/text-editor/prosemirror-adapter/schema/nodes.ts
  • src/components/text-editor/prosemirror-adapter/schema/schema.spec.ts

|------|--------|
| `src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx` | Replace imports, simplify `initializeSchema()` and `createEditorState()` |
| `src/components/text-editor/prosemirror-adapter/plugins/image/node.ts` | Add `draggable: true` and `title` attribute |
| `package.json` | Remove `prosemirror-example-setup`, add 4 direct deps |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Dependency count in file map is inconsistent with Task 8.

Line 34 says “add 4 direct deps,” but Task 8 installs 8 direct prosemirror-* packages. Please align the summary row to avoid plan ambiguity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-03-30-custom-prosemirror-schema.md` at line 34,
Update the table summary row that currently reads "`package.json` | Remove
`prosemirror-example-setup`, add 4 direct deps`" to match Task 8 by changing "4"
to "8" (or alternatively adjust Task 8 to only install 4 packages); ensure the
phrase exactly reflects the action in Task 8 so the docs and the task remain
consistent.

- Bold, italic, strikethrough formatting works
- Lists (bullet and ordered) work
- Blockquote, code block, heading work
- Auto-formatting shortcuts work (type `> ` for blockquote, `1. ` for ordered list, `# ` for heading)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix markdownlint MD038 by removing spaces inside inline code spans.

The inline examples include trailing spaces inside backticks, which triggers lint warnings.

Suggested fix
-- Auto-formatting shortcuts work (type `> ` for blockquote, `1. ` for ordered list, `# ` for heading)
+- Auto-formatting shortcuts work (type `>` + space for blockquote, `1.` + space for ordered list, `#` + space for heading)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Auto-formatting shortcuts work (type `> ` for blockquote, `1. ` for ordered list, `# ` for heading)
- Auto-formatting shortcuts work (type `>` + space for blockquote, `1.` + space for ordered list, `#` + space for heading)
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 822-822: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 822-822: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 822-822: Spaces inside code span elements

(MD038, no-space-in-code)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-03-30-custom-prosemirror-schema.md` at line 822,
The inline code spans in the sentence containing examples like `> `, `1. `, and
`# ` include trailing spaces which trigger markdownlint MD038; update that line
(the sentence "Auto-formatting shortcuts work (type `> ` for blockquote, `1. `
for ordered list, `# ` for heading)") to remove spaces inside the backticks so
the spans read `>`, `1.`, and `#` respectively, preserving the text and
punctuation around them.

Comment on lines 126 to 137
"dependencies": {
"diff": "^8.0.3",
"prosemirror-commands": "^1.7.1",
"prosemirror-dropcursor": "^1.8.2",
"prosemirror-gapcursor": "^1.4.1",
"prosemirror-history": "^1.5.0",
"prosemirror-inputrules": "^1.5.1",
"prosemirror-keymap": "^1.2.3",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.4",
"prosemirror-view": "^1.41.7",
"sortablejs": "^1.15.6"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Direct imports of orderedmap:"
rg -n "from 'orderedmap'|from \"orderedmap\"" src/components/text-editor/prosemirror-adapter

echo
node - <<'NODE'
const pkg = require('./package.json');
console.log('dependencies.orderedmap =', pkg.dependencies?.orderedmap ?? null);
console.log('devDependencies.orderedmap =', pkg.devDependencies?.orderedmap ?? null);
NODE

Repository: Lundalogik/lime-elements

Length of output: 367


🌐 Web query:

orderedmap npm package latest version current

💡 Result:

The latest version of the orderedmap npm package is 2.1.1, published on May 17, 2023. It is a persistent ordered mapping from strings to values, maintained by Marijn Haverbeke, with the repository at https://github.com/marijnh/orderedmap. No newer versions have been released as of 2026-03-30.

Citations:


Declare orderedmap explicitly before importing it directly.

orderedmap is imported directly in src/components/text-editor/prosemirror-adapter/schema/nodes.ts and src/components/text-editor/prosemirror-adapter/schema/marks.ts but not declared in package.json. This currently relies on transitive dependency hoisting and will break in stricter installs.

Suggested fix
   "dependencies": {
     "diff": "^8.0.3",
+    "orderedmap": "^2.1.1",
     "prosemirror-commands": "^1.7.1",
     "prosemirror-dropcursor": "^1.8.2",
     "prosemirror-gapcursor": "^1.4.1",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dependencies": {
"diff": "^8.0.3",
"prosemirror-commands": "^1.7.1",
"prosemirror-dropcursor": "^1.8.2",
"prosemirror-gapcursor": "^1.4.1",
"prosemirror-history": "^1.5.0",
"prosemirror-inputrules": "^1.5.1",
"prosemirror-keymap": "^1.2.3",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.4",
"prosemirror-view": "^1.41.7",
"sortablejs": "^1.15.6"
"dependencies": {
"diff": "^8.0.3",
"orderedmap": "^2.1.1",
"prosemirror-commands": "^1.7.1",
"prosemirror-dropcursor": "^1.8.2",
"prosemirror-gapcursor": "^1.4.1",
"prosemirror-history": "^1.5.0",
"prosemirror-inputrules": "^1.5.1",
"prosemirror-keymap": "^1.2.3",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.4",
"prosemirror-view": "^1.41.7",
"sortablejs": "^1.15.6"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 126 - 137, The project relies on the external
module "orderedmap" (imported in nodes.ts and marks.ts) but it is not declared
in package.json, causing brittle transitive-dependency behavior; add
"orderedmap" to the "dependencies" block in package.json (e.g., "orderedmap":
"^1.0.0") and run install to ensure the module is explicitly installed so
imports in src/components/text-editor/prosemirror-adapter/schema/nodes.ts and
src/components/text-editor/prosemirror-adapter/schema/marks.ts resolve reliably.

Comment on lines +352 to 363
'Mod-b': this.getCommand(EditorMenuTypes.Bold),
'Mod-B': this.getCommand(EditorMenuTypes.Bold),
'Mod-i': this.getCommand(EditorMenuTypes.Italic),
'Mod-I': this.getCommand(EditorMenuTypes.Italic),
'Mod-Shift-1': this.getCommand(EditorMenuTypes.HeaderLevel1),
'Mod-Shift-2': this.getCommand(EditorMenuTypes.HeaderLevel2),
'Mod-Shift-3': this.getCommand(EditorMenuTypes.HeaderLevel3),
'Mod-Shift-x': this.getCommand(EditorMenuTypes.Strikethrough),
'Mod-Shift-X': this.getCommand(EditorMenuTypes.Strikethrough),
'Mod-`': this.getCommand(EditorMenuTypes.Code),
'Mod-Shift-c': this.getCommand(EditorMenuTypes.CodeBlock),
'Mod-Shift-C': this.getCommand(EditorMenuTypes.CodeBlock),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

These new shortcuts are shadowed by the base keymap and won’t hit MenuCommandFactory.

Line 352, Line 354, Line 359, and Line 362 overlap with bindings in buildEditorKeymap(...). Because base plugins are registered first, these menu handlers are effectively unreachable for those chords.

Suggested direction

Use a single source of truth for overlapping shortcuts:

  • either move keymap(this.menuCommandFactory.buildKeymap()) before the base keymap plugin registration, or
  • remove overlapping entries from one map so precedence is explicit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts` around
lines 352 - 363, The menu shortcuts (e.g.,
'Mod-b','Mod-B','Mod-i','Mod-I','Mod-Shift-1','Mod-Shift-2','Mod-Shift-3','Mod-Shift-x','Mod-Shift-X','Mod-`','Mod-Shift-c','Mod-Shift-C')
are shadowed by the base keymap built in buildEditorKeymap(...), so
MenuCommandFactory handlers never run; fix by making precedence explicit: either
move the plugin registration of keymap(this.menuCommandFactory.buildKeymap()) to
run before the base keymap plugin (so MenuCommandFactory's buildKeymap takes
priority), or remove those overlapping entries from
menuCommandFactory.buildKeymap() so they are only present in
buildEditorKeymap(); update the code paths that construct/install the plugins
(where keymap(...) and buildEditorKeymap(...) are invoked) and adjust tests if
any.

Comment on lines +38 to +40
keys['Mod-z'] = undo;
keys['Shift-Mod-z'] = redo;
keys['Backspace'] = undoInputRule;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add Mod-y for redo shortcut parity on Windows/Linux.

Line 39 defines Shift-Mod-z redo, but Ctrl+Y/Mod-y is missing and users typically expect it in editors.

Suggested fix
     keys['Mod-z'] = undo;
     keys['Shift-Mod-z'] = redo;
+    keys['Mod-y'] = redo;
     keys['Backspace'] = undoInputRule;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
keys['Mod-z'] = undo;
keys['Shift-Mod-z'] = redo;
keys['Backspace'] = undoInputRule;
keys['Mod-z'] = undo;
keys['Shift-Mod-z'] = redo;
keys['Mod-y'] = redo;
keys['Backspace'] = undoInputRule;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts`
around lines 38 - 40, The redo mapping lacks the common Windows/Linux shortcut;
in the keys mapping inside base-plugins.ts add a mapping for 'Mod-y' to the
existing redo handler so Ctrl+Y (or Cmd+Y on mac where appropriate) triggers
redo; specifically update the keys object (where keys['Shift-Mod-z'] = redo and
keys['Backspace'] = undoInputRule are defined) to include keys['Mod-y'] = redo
to provide parity across platforms.

Comment on lines +123 to +125
const titleAttribute = attrs.title ? ` title="${attrs.title}"` : '';

return `<img src="${attrs.src}" alt="${attrs.alt}"${styleAttribute} />`;
return `<img src="${attrs.src}" alt="${attrs.alt}"${titleAttribute}${styleAttribute} />`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Escape serialized attributes before emitting image HTML.

Line 123-Line 125 interpolates attrs.title directly into HTML. A crafted title can break attribute context and inject markup/script when this HTML is rendered.

Suggested fix
+function escapeAttr(value: string): string {
+    return value
+        .replace(/&/g, '&amp;')
+        .replace(/"/g, '&quot;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;');
+}
+
 function getImageHTML(attrs: ImageNodeAttrs): string {
     const style = [];
@@
-    const titleAttribute = attrs.title ? ` title="${attrs.title}"` : '';
-
-    return `<img src="${attrs.src}" alt="${attrs.alt}"${titleAttribute}${styleAttribute} />`;
+    const titleAttribute = attrs.title
+        ? ` title="${escapeAttr(attrs.title)}"`
+        : '';
+
+    return `<img src="${escapeAttr(attrs.src)}" alt="${escapeAttr(
+        attrs.alt
+    )}"${titleAttribute}${styleAttribute} />`;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const titleAttribute = attrs.title ? ` title="${attrs.title}"` : '';
return `<img src="${attrs.src}" alt="${attrs.alt}"${styleAttribute} />`;
return `<img src="${attrs.src}" alt="${attrs.alt}"${titleAttribute}${styleAttribute} />`;
function escapeAttr(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function getImageHTML(attrs: ImageNodeAttrs): string {
const style = [];
const styleAttribute = style.length > 0 ? ` style="${style.join(';')}"` : '';
const titleAttribute = attrs.title
? ` title="${escapeAttr(attrs.title)}"`
: '';
return `<img src="${escapeAttr(attrs.src)}" alt="${escapeAttr(
attrs.alt
)}"${titleAttribute}${styleAttribute} />`;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/text-editor/prosemirror-adapter/plugins/image/node.ts` around
lines 123 - 125, The image HTML builder currently interpolates raw attrs
(attrs.title, attrs.src, attrs.alt) into the returned string (where
titleAttribute and styleAttribute are composed), which allows attribute
injection; fix it by running all serialized attribute values through an
HTML-attribute-escaping helper (e.g., escapeHtmlAttribute or similar) before
composing titleAttribute/styleAttribute and the final `<img ... />` string so
quotes, ampersands and angle brackets are encoded and the attributes remain
safe; update uses in the template-returning function to reference the escaped
values instead of raw attrs.

Comment on lines +403 to 405
...buildBasePlugins(this.schema),
keymap(this.menuCommandFactory.buildKeymap()),
createTriggerPlugin(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Plugin order in adapter =="
rg -n -A20 -B5 'plugins:\s*\[' src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx

echo
echo "== Base keymap registration in buildBasePlugins =="
rg -n -A8 -B3 'keymap\(baseKeymap\)|buildBasePlugins' src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts

echo
echo "== Potentially overlapping custom key bindings =="
rg -nP 'Mod-b|Mod-B|Shift-Enter|Mod-z|Mod-y|Enter' src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts

Repository: Lundalogik/lime-elements

Length of output: 1800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find baseKeymap definition =="
rg -n 'baseKeymap' src/components/text-editor/prosemirror-adapter/ --type ts --type tsx

echo
echo "== Check imports in base-plugins.ts =="
head -30 src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts

Repository: Lundalogik/lime-elements

Length of output: 145


🌐 Web query:

ProseMirror plugin execution order precedence keymaps

💡 Result:

In ProseMirror, the execution order and precedence of plugins, particularly for keymaps, is determined by the order in which plugins are provided in the EditorState's plugins array. Plugins are executed in the order they appear in the array (index 0 first). Earlier plugins have higher precedence. For keymaps specifically: - Keymap plugins (created via prosemirror-keymap's keymap function) define key bindings that are checked during keyboard input handling. - When a key is pressed, ProseMirror's EditorView checks plugins providing a 'handleKeyDown' prop (which keymap plugins do). These are called in plugin order until one returns true (handled). - Within a single keymap plugin, commands for matching keys are chained—tried in the order of the bindings object until one returns true. - Multiple keymap plugins: the first (earliest in array) whose binding matches and returns true takes precedence. Official docs state: "You can add multiple keymap plugins to an editor. The order in which they appear determines their precedence (the ones early in the array get to dispatch first)." Example from ProseMirror forum: Custom keymaps placed before baseKeymap will override its bindings for the same keys. To control precedence, order plugins explicitly when creating EditorState: plugins: [ customKeymap1, // Highest precedence customKeymap2, keymap(baseKeymap), // Lower history, ] This applies generally to plugin props like handleDOMEvents, filterTransaction, etc., which are resolved by iterating plugins in order.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check imports in base-plugins.ts =="
head -40 src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts

echo
echo "== Find where baseKeymap is imported from =="
rg -n 'import.*baseKeymap' src/components/text-editor/

echo
echo "== Check all custom keybindings in menu-commands.ts =="
rg -n "'\w+(-\w+)*':" src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts | head -30

Repository: Lundalogik/lime-elements

Length of output: 1401


🌐 Web query:

ProseMirror baseKeymap contents prosemirror-commands Mod-b

💡 Result:

In the prosemirror-commands package, baseKeymap is an object exported from src/commands.ts that provides a standard set of keyboard shortcuts for common editing actions in ProseMirror editors. It maps key combinations (using the keymap notation where "Mod" means Cmd on Mac or Ctrl on other platforms) to commands from the package. The full contents are not explicitly listed in the official documentation or reference manual, but the README lists all exported commands used in baseKeymap, including: chainCommands, deleteSelection, joinBackward, selectNodeBackward, joinTextblockBackward, joinForward, selectNodeForward, joinTextblockForward, joinUp, joinDown, lift, newlineInCode, exitCode, createParagraphNear, liftEmptyBlock, splitBlock, splitBlockAs, splitBlockKeepMarks, selectParentNode, selectAll, selectTextblockStart, selectTextblockEnd, wrapIn, setBlockType, toggleMark, autoJoin, baseKeymap, pcBaseKeymap, macBaseKeymap. Common bindings include: - Mod-b: toggleMark(schema.marks.strong) for bold text - Mod-i: toggleMark(schema.marks.em) for italic - Mod-: toggleMark(schema.marks.code) or similar for code - Enter: splitBlock or chainCommands(newlineInCode, createParagraphNear) - Backspace: deleteSelection or chainCommands(joinBackward, deleteSelection) "Mod-b" specifically binds to the toggleMark command for strong/bold formatting, allowing users to toggle bold on the current selection with Cmd+B (Mac) or Ctrl+B (other platforms). To see the exact current implementation, view the source at https://github.com/ProseMirror/prosemirror-commands/blob/master/src/commands.ts (32KB file containing baseKeymap definition). There are platform-specific variants pcBaseKeymap and macBaseKeymap. The keymap plugin from prosemirror-keymap is used to activate it: keymap(baseKeymap(schema)).

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full buildEditorKeymap implementation =="
sed -n '30,120p' src/components/text-editor/prosemirror-adapter/plugins/base-plugins.ts

echo
echo "== All custom keybindings in menu-commands.ts =="
rg -A200 'buildKeymap\(\)' src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts | grep -E "^\s+'[^']+': " | head -20

Repository: Lundalogik/lime-elements

Length of output: 3735


Reorder plugins to prevent custom keybindings from being shadowed by base keybindings.

Custom key bindings for bold (Mod-b), italic (Mod-i), and code (Mod-```) are currently unreachable. The buildEditorKeymapinbuildBasePlugins` (line 403) is registered before the custom keymap (line 404). Since ProseMirror checks plugins in array order, the base bindings dispatch first and prevent the custom commands from executing.

Suggested fix
         return EditorState.create({
             doc: initialDoc,
             plugins: [
-                ...buildBasePlugins(this.schema),
                 keymap(this.menuCommandFactory.buildKeymap()),
+                ...buildBasePlugins(this.schema),
                 createTriggerPlugin(
                     this.triggerCharacters,
                     this.contentConverter
                 ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
...buildBasePlugins(this.schema),
keymap(this.menuCommandFactory.buildKeymap()),
createTriggerPlugin(
keymap(this.menuCommandFactory.buildKeymap()),
...buildBasePlugins(this.schema),
createTriggerPlugin(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx`
around lines 403 - 405, The custom keymap from menuCommandFactory.buildKeymap is
being shadowed by the base keymap created in buildBasePlugins (which registers
buildEditorKeymap) because plugin order matters; move the
keymap(keymap(this.menuCommandFactory.buildKeymap())) to appear before
...buildBasePlugins(this.schema) so the custom bindings (Mod-b, Mod-i, Mod-`)
are checked first, ensuring createTriggerPlugin and other plugins remain in the
same relative order.

@john-traas john-traas force-pushed the feat/custom-prosemirror-schema branch from 18004b2 to 6a1d303 Compare March 31, 2026 08:19
john-traas and others added 11 commits March 31, 2026 10:23
The custom image node spec was missing draggable: true (present in
prosemirror-schema-basic's image spec), making images non-draggable.
Also adds title attribute support so <img title='...'> is preserved
during parse/serialize cycles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cherry-picks strong, em, code from prosemirror-schema-basic. Uses our
own link mark (security attrs) and strikethrough. Basic link mark
intentionally excluded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cherry-picks base nodes from prosemirror-schema-basic (doc, paragraph,
blockquote, heading, code_block, horizontal_rule, text, hard_break).
Excludes basic image — uses our own. Adds custom elements, lists,
tables (HTML only), and image node via buildNodes().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single entry point for schema creation. Assembles nodes and marks
from the cherry-picked specs in nodes.ts and marks.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replicates the input rules from prosemirror-example-setup: smart
quotes, ellipsis, em dash, and markdown-style shortcuts for
blockquote, lists, code block, and headings. Schema-aware — only
creates rules for nodes present in the schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explicit plugin assembly replacing exampleSetup(): input rules,
baseKeymap, dropCursor, gapCursor, history. Each from its own
stable ProseMirror package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies createSchema() includes all expected nodes and marks,
conditionally adds tables for HTML mode, includes custom elements,
and confirms image node has draggable and title.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline schema assembly and exampleSetup() with createSchema()
and buildBasePlugins(). The adapter now delegates all schema and base
plugin construction to the schema/ and plugins/ modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove prosemirror-example-setup. Add prosemirror-inputrules,
prosemirror-dropcursor, prosemirror-gapcursor, prosemirror-history,
prosemirror-commands, prosemirror-keymap, prosemirror-state, and
prosemirror-view as direct dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
prosemirror-schema-list was a transitive dep of the now-removed
prosemirror-example-setup. Add it as a direct dependency. Also
applies Prettier formatting fixes to new schema module files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all keybindings into MenuCommandFactory.buildKeymap(). The
factory is now the single source of truth for keyboard shortcuts.
base-plugins.ts returns to infrastructure only (inputRules, baseKeymap,
dropCursor, gapCursor, history).

Fix key casing: ProseMirror treats Mod-B as Cmd+Shift+B and Mod-b as
Cmd+B. Now binds both cases for mark toggles.

Add EditorMenuTypes: Paragraph, HeaderLevel4/5/6, HardBreak,
HorizontalRule, SplitListItem, LiftListItem, SinkListItem,
WrapInBulletList, WrapInOrderedList, WrapInBlockquote. Add
corresponding command factories and mapping entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@john-traas john-traas force-pushed the feat/custom-prosemirror-schema branch from 6a1d303 to 395c0c5 Compare March 31, 2026 08:24
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