Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 71 additions & 162 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
"parse-css-color": "^0.2.1",
"postal-mime": "^2.7.3",
"prettier": "^3.5.0",
"prosemirror-example-setup": "^1.2.3",
"prosemirror-markdown": "^1.13.2",
"prosemirror-model": ">=1.22.1",
"prosemirror-schema-basic": "^1.2.4",
Expand Down Expand Up @@ -126,6 +125,15 @@
"packageManager": "npm@>=10.9",
"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"
Comment on lines 126 to 137
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.

}
}
156 changes: 150 additions & 6 deletions src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { toggleMark, setBlockType, wrapIn, lift } from 'prosemirror-commands';
import {
chainCommands,
exitCode,
joinDown,
joinUp,
lift,
selectParentNode,
setBlockType,
toggleMark,
wrapIn,
} from 'prosemirror-commands';
import { undo, redo } from 'prosemirror-history';
import { undoInputRule } from 'prosemirror-inputrules';
import { Schema, MarkType, NodeType, Attrs } from 'prosemirror-model';
import { findWrapping, liftTarget } from 'prosemirror-transform';
import {
splitListItem,
liftListItem,
sinkListItem,
} from 'prosemirror-schema-list';
import { Command, EditorState, TextSelection } from 'prosemirror-state';
import { findWrapping, liftTarget } from 'prosemirror-transform';
import { EditorMenuTypes, EditorTextLink, LevelMapping } from './types';
import { getLinkAttributes } from '../plugins/link/utils';

Expand Down Expand Up @@ -295,6 +312,61 @@ const createListCommand = (
return command;
};

const createSplitListItemCommand = (schema: Schema): CommandWithActive => {
const type = schema.nodes.list_item;

return splitListItem(type);
};

const createLiftListItemCommand = (schema: Schema): CommandWithActive => {
const type = schema.nodes.list_item;

return liftListItem(type);
};

const createSinkListItemCommand = (schema: Schema): CommandWithActive => {
const type = schema.nodes.list_item;

return sinkListItem(type);
};

const createHardBreakCommand = (schema: Schema): CommandWithActive => {
const br = schema.nodes.hard_break;

return chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
dispatch(
state.tr.replaceSelectionWith(br.create()).scrollIntoView()
);
}

return true;
});
};

const createHorizontalRuleCommand = (schema: Schema): CommandWithActive => {
const hr = schema.nodes.horizontal_rule;

return (state, dispatch) => {
if (dispatch) {
dispatch(
state.tr.replaceSelectionWith(hr.create()).scrollIntoView()
);
}

return true;
};
};

const createWrapInNodeCommand = (
schema: Schema,
nodeType: string
): CommandWithActive => {
const type = schema.nodes[nodeType];

return wrapIn(type);
};

const commandMapping: CommandMapping = {
strong: createToggleMarkCommand,
em: createToggleMarkCommand,
Expand All @@ -320,15 +392,44 @@ const commandMapping: CommandMapping = {
LevelMapping.Heading,
LevelMapping.three
),
headerlevel4: (schema) =>
createSetNodeTypeCommand(
schema,
LevelMapping.Heading,
LevelMapping.four
),
headerlevel5: (schema) =>
createSetNodeTypeCommand(
schema,
LevelMapping.Heading,
LevelMapping.five
),
headerlevel6: (schema) =>
createSetNodeTypeCommand(
schema,
LevelMapping.Heading,
LevelMapping.six
),
blockquote: (schema) =>
createWrapInCommand(schema, EditorMenuTypes.Blockquote),

code_block: (schema) =>
createSetNodeTypeCommand(schema, EditorMenuTypes.CodeBlock),
paragraph: (schema) =>
createSetNodeTypeCommand(schema, EditorMenuTypes.Paragraph),
ordered_list: (schema) =>
createListCommand(schema, EditorMenuTypes.OrderedList),
bullet_list: (schema) =>
createListCommand(schema, EditorMenuTypes.BulletList),
split_list_item: (schema) => createSplitListItemCommand(schema),
lift_list_item: (schema) => createLiftListItemCommand(schema),
sink_list_item: (schema) => createSinkListItemCommand(schema),
hard_break: (schema) => createHardBreakCommand(schema),
horizontal_rule: (schema) => createHorizontalRuleCommand(schema),
wrap_bullet_list: (schema) =>
createWrapInNodeCommand(schema, 'bullet_list'),
wrap_ordered_list: (schema) =>
createWrapInNodeCommand(schema, 'ordered_list'),
wrap_blockquote: (schema) => createWrapInNodeCommand(schema, 'blockquote'),
};

export class MenuCommandFactory {
Expand All @@ -347,16 +448,59 @@ export class MenuCommandFactory {
return commandFunc(this.schema, mark, link);
}

buildKeymap() {
buildKeymap(): { [key: string]: Command } {
return {
// History
'Mod-z': undo,
'Shift-Mod-z': redo,
Backspace: undoInputRule,

// Navigation
'Alt-ArrowUp': joinUp,
'Alt-ArrowDown': joinDown,
'Mod-BracketLeft': lift,
Escape: selectParentNode,

// Mark toggles
'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-`': this.getCommand(EditorMenuTypes.Code),
'Mod-Shift-x': this.getCommand(EditorMenuTypes.Strikethrough),
'Mod-Shift-X': this.getCommand(EditorMenuTypes.Strikethrough),

// Block types (Mod-Shift)
'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-`': this.getCommand(EditorMenuTypes.Code),
'Mod-Shift-c': this.getCommand(EditorMenuTypes.CodeBlock),
'Mod-Shift-C': this.getCommand(EditorMenuTypes.CodeBlock),
Comment on lines +465 to 478
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.


// Block types (Shift-Ctrl)
'Shift-Ctrl-0': this.getCommand(EditorMenuTypes.Paragraph),
'Shift-Ctrl-1': this.getCommand(EditorMenuTypes.HeaderLevel1),
'Shift-Ctrl-2': this.getCommand(EditorMenuTypes.HeaderLevel2),
'Shift-Ctrl-3': this.getCommand(EditorMenuTypes.HeaderLevel3),
'Shift-Ctrl-4': this.getCommand(EditorMenuTypes.HeaderLevel4),
'Shift-Ctrl-5': this.getCommand(EditorMenuTypes.HeaderLevel5),
'Shift-Ctrl-6': this.getCommand(EditorMenuTypes.HeaderLevel6),
'Shift-Ctrl-\\': this.getCommand(EditorMenuTypes.CodeBlock),

// List operations
Enter: this.getCommand(EditorMenuTypes.SplitListItem),
'Mod-[': this.getCommand(EditorMenuTypes.LiftListItem),
'Mod-]': this.getCommand(EditorMenuTypes.SinkListItem),
'Shift-Ctrl-8': this.getCommand(EditorMenuTypes.WrapInBulletList),
'Shift-Ctrl-9': this.getCommand(EditorMenuTypes.WrapInOrderedList),

// Wrapping
'Ctrl->': this.getCommand(EditorMenuTypes.WrapInBlockquote),

// Insertions
'Mod-Enter': this.getCommand(EditorMenuTypes.HardBreak),
'Shift-Enter': this.getCommand(EditorMenuTypes.HardBreak),
'Mod-_': this.getCommand(EditorMenuTypes.HorizontalRule),
};
}
}
15 changes: 15 additions & 0 deletions src/components/text-editor/prosemirror-adapter/menu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ export const EditorMenuTypes = {
HeaderLevel1: 'headerlevel1',
HeaderLevel2: 'headerlevel2',
HeaderLevel3: 'headerlevel3',
HeaderLevel4: 'headerlevel4',
HeaderLevel5: 'headerlevel5',
HeaderLevel6: 'headerlevel6',
Link: 'link',
OrderedList: 'ordered_list',
BulletList: 'bullet_list',
Strikethrough: 'strikethrough',
Code: 'code',
CodeBlock: 'code_block',
Paragraph: 'paragraph',
HardBreak: 'hard_break',
HorizontalRule: 'horizontal_rule',
SplitListItem: 'split_list_item',
LiftListItem: 'lift_list_item',
SinkListItem: 'sink_list_item',
WrapInBulletList: 'wrap_bullet_list',
WrapInOrderedList: 'wrap_ordered_list',
WrapInBlockquote: 'wrap_blockquote',
};

/**
Expand Down Expand Up @@ -45,6 +57,9 @@ export const LevelMapping = {
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: 6,
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Plugin } from 'prosemirror-state';
import { Schema } from 'prosemirror-model';
import { keymap } from 'prosemirror-keymap';
import { baseKeymap } from 'prosemirror-commands';
import { history } from 'prosemirror-history';
import { dropCursor } from 'prosemirror-dropcursor';
import { gapCursor } from 'prosemirror-gapcursor';
import { buildInputRules } from './input-rules';

/**
* Assembles the base ProseMirror plugins for the text editor.
*
* Provides infrastructure plugins only: input rules, base keymap
* (Enter, Backspace, Delete), drop cursor, gap cursor, and history.
* Schema-aware keybindings are handled by MenuCommandFactory.buildKeymap().
* @param schema - The ProseMirror schema to build plugins for
*/
export function buildBasePlugins(schema: Schema): Plugin[] {
return [
buildInputRules(schema),
keymap(baseKeymap),
dropCursor(),
gapCursor(),
history(),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,15 @@ function getImageHTML(attrs: ImageNodeAttrs): string {
}

const styleAttribute = style.length > 0 ? ` style="${style.join('')}"` : '';
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} />`;
Comment on lines +123 to +125
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.

}

export interface ImageNodeAttrs {
src: string;
alt: string;
title?: string | null;
state: EditorImageState;
fileInfoId: string | number;
height?: string;
Expand All @@ -140,9 +142,11 @@ function createImageNodeSpec(language: Languages): NodeSpec {
return {
group: 'inline',
inline: true,
draggable: true,
attrs: {
src: { default: '' },
alt: { default: '' },
title: { default: null },
fileInfoId: { default: '' },
height: { default: '' },
width: { default: '' },
Expand All @@ -169,6 +173,7 @@ function createImageNodeSpec(language: Languages): NodeSpec {
return {
src: dom.getAttribute('src') || '',
alt: dom.getAttribute('alt') || 'file',
title: dom.getAttribute('title') || null,
width: dom.style.width || '',
height: dom.style.height || '',
maxWidth: '100%',
Expand Down Expand Up @@ -230,6 +235,7 @@ function updateImageElement(
node: Node
): HTMLImageElement {
img.alt = node.attrs.alt;
img.title = node.attrs.title || '';
applyImageStyles(img, node);

return img;
Expand All @@ -239,6 +245,10 @@ function createImageElement(node: Node): HTMLImageElement {
const img = document.createElement('img');
img.src = node.attrs.src;
img.alt = node.attrs.alt;
if (node.attrs.title) {
img.title = node.attrs.title;
}

applyImageStyles(img, node);

return img;
Expand Down
Loading
Loading