leepi 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/core/active-marks.d.ts +15 -0
  2. package/dist/core/active-marks.js +57 -0
  3. package/dist/core/commands.d.ts +39 -0
  4. package/dist/core/commands.js +415 -0
  5. package/dist/core/editor.d.ts +25 -0
  6. package/dist/core/editor.js +102 -0
  7. package/dist/core/field-notifier.d.ts +21 -0
  8. package/dist/core/field-notifier.js +56 -0
  9. package/dist/core/highlight-style.js +60 -0
  10. package/dist/core/highlight.d.ts +8 -0
  11. package/dist/core/highlight.js +34 -0
  12. package/dist/core/plugins/blockquote.d.ts +11 -0
  13. package/dist/core/plugins/blockquote.js +78 -0
  14. package/dist/core/plugins/bracket.d.ts +6 -0
  15. package/dist/core/plugins/bracket.js +38 -0
  16. package/dist/core/plugins/code-block.d.ts +27 -0
  17. package/dist/core/plugins/code-block.js +207 -0
  18. package/dist/core/plugins/heading.d.ts +13 -0
  19. package/dist/core/plugins/heading.js +111 -0
  20. package/dist/core/plugins/inline.d.ts +14 -0
  21. package/dist/core/plugins/inline.js +103 -0
  22. package/dist/core/plugins/link.d.ts +25 -0
  23. package/dist/core/plugins/link.js +104 -0
  24. package/dist/core/plugins/list.d.ts +14 -0
  25. package/dist/core/plugins/list.js +91 -0
  26. package/dist/core/plugins/table.d.ts +12 -0
  27. package/dist/core/plugins/table.js +161 -0
  28. package/dist/core/plugins.d.ts +9 -0
  29. package/dist/core/plugins.js +9 -0
  30. package/dist/core/popover.d.ts +9 -0
  31. package/dist/core/popover.js +16 -0
  32. package/dist/core/registry.d.ts +10 -0
  33. package/dist/core/registry.js +8 -0
  34. package/dist/core/types.d.ts +25 -0
  35. package/dist/core/types.js +0 -0
  36. package/dist/core/utils.d.ts +13 -0
  37. package/dist/core/utils.js +32 -0
  38. package/dist/leepi.css +461 -0
  39. package/dist/react/code-block-popover.d.ts +76 -0
  40. package/dist/react/code-block-popover.js +223 -0
  41. package/dist/react/context.d.ts +42 -0
  42. package/dist/react/context.js +88 -0
  43. package/dist/react/editor.d.ts +30 -0
  44. package/dist/react/editor.js +60 -0
  45. package/dist/react/floating-toolbar.d.ts +30 -0
  46. package/dist/react/floating-toolbar.js +87 -0
  47. package/dist/react/link-popover.d.ts +70 -0
  48. package/dist/react/link-popover.js +222 -0
  49. package/dist/react/preview.d.ts +13 -0
  50. package/dist/react/preview.js +56 -0
  51. package/dist/react/toolbar.d.ts +51 -0
  52. package/dist/react/toolbar.js +161 -0
  53. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { markRegistry, shortcutRegistry } from "../registry.js";
2
+ import { toggleMarker } from "../commands.js";
3
+ import { keymap } from "@codemirror/view";
4
+ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
5
+ import { tags } from "@lezer/highlight";
6
+ //#region src/core/plugins/inline.ts
7
+ const defaultShortcuts = {
8
+ bold: "Mod-b",
9
+ italic: "Mod-i",
10
+ strikethrough: "Mod-Shift-x",
11
+ code: "Mod-e"
12
+ };
13
+ const inlineHighlight = HighlightStyle.define([
14
+ {
15
+ tag: tags.strong,
16
+ fontWeight: "var(--lp-font-weight-bold, 700)"
17
+ },
18
+ {
19
+ tag: tags.emphasis,
20
+ fontStyle: "italic"
21
+ },
22
+ {
23
+ tag: tags.strikethrough,
24
+ textDecoration: "line-through"
25
+ },
26
+ {
27
+ tag: tags.monospace,
28
+ fontFamily: "var(--lp-font-mono)",
29
+ fontSize: "0.875em",
30
+ backgroundColor: "var(--lp-color-surface)",
31
+ borderRadius: "var(--lp-radius, 6px)",
32
+ padding: "0.1em 0.3em"
33
+ }
34
+ ]);
35
+ function inlinePlugin(options) {
36
+ const keys = {
37
+ ...defaultShortcuts,
38
+ ...options?.shortcuts
39
+ };
40
+ return [
41
+ syntaxHighlighting(inlineHighlight),
42
+ shortcutRegistry.of([
43
+ {
44
+ key: keys.bold,
45
+ action: "bold",
46
+ plugin: "inline"
47
+ },
48
+ {
49
+ key: keys.italic,
50
+ action: "italic",
51
+ plugin: "inline"
52
+ },
53
+ {
54
+ key: keys.strikethrough,
55
+ action: "strikethrough",
56
+ plugin: "inline"
57
+ },
58
+ {
59
+ key: keys.code,
60
+ action: "code",
61
+ plugin: "inline"
62
+ }
63
+ ]),
64
+ markRegistry.of([
65
+ {
66
+ mark: "bold",
67
+ detect: (nodeName) => nodeName === "StrongEmphasis"
68
+ },
69
+ {
70
+ mark: "italic",
71
+ detect: (nodeName) => nodeName === "Emphasis"
72
+ },
73
+ {
74
+ mark: "strikethrough",
75
+ detect: (nodeName) => nodeName === "Strikethrough"
76
+ },
77
+ {
78
+ mark: "code",
79
+ detect: (nodeName) => nodeName === "InlineCode"
80
+ }
81
+ ]),
82
+ keymap.of([
83
+ {
84
+ key: keys.bold,
85
+ run: toggleMarker("**")
86
+ },
87
+ {
88
+ key: keys.italic,
89
+ run: toggleMarker("_")
90
+ },
91
+ {
92
+ key: keys.strikethrough,
93
+ run: toggleMarker("~~")
94
+ },
95
+ {
96
+ key: keys.code,
97
+ run: toggleMarker("`")
98
+ }
99
+ ])
100
+ ];
101
+ }
102
+ //#endregion
103
+ export { inlinePlugin };
@@ -0,0 +1,25 @@
1
+ import { PopoverRequest } from "../types.js";
2
+ import { findLinkAtCursor, insertLink } from "../commands.js";
3
+ import { Extension } from "@codemirror/state";
4
+
5
+ //#region src/core/plugins/link.d.ts
6
+ interface LinkData {
7
+ /** Selected text or existing link label */
8
+ text: string;
9
+ /** Pre-filled URL when editing an existing link */
10
+ url: string;
11
+ /** Document range to replace when the link is inserted/removed */
12
+ from: number;
13
+ to: number;
14
+ }
15
+ type LinkRequest = PopoverRequest<"link", LinkData>;
16
+ interface LinkPluginOptions {
17
+ shortcuts?: {
18
+ insertLink?: string;
19
+ removeLink?: string;
20
+ };
21
+ urlRegex?: RegExp;
22
+ }
23
+ declare function linkPlugin(options?: LinkPluginOptions): Extension;
24
+ //#endregion
25
+ export { LinkData, LinkPluginOptions, LinkRequest, linkPlugin };
@@ -0,0 +1,104 @@
1
+ import { markRegistry, shortcutRegistry } from "../registry.js";
2
+ import { isInsideCodeBlock } from "../utils.js";
3
+ import { findLinkAtCursor, insertLink } from "../commands.js";
4
+ import { openPopover } from "../popover.js";
5
+ import { EditorView, keymap } from "@codemirror/view";
6
+ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
7
+ import { EditorSelection } from "@codemirror/state";
8
+ import { tags } from "@lezer/highlight";
9
+ //#region src/core/plugins/link.ts
10
+ const defaultShortcuts = {
11
+ insertLink: "Mod-k",
12
+ removeLink: "Mod-Shift-k"
13
+ };
14
+ const URL_RE = /^(?:https?:\/\/|mailto:|tel:)\S+$/;
15
+ const linkHighlight = HighlightStyle.define([{
16
+ tag: tags.link,
17
+ color: "var(--lp-color-accent)"
18
+ }, {
19
+ tag: tags.url,
20
+ color: "var(--lp-color-accent)"
21
+ }]);
22
+ function linkPlugin(options) {
23
+ const keys = {
24
+ ...defaultShortcuts,
25
+ ...options?.shortcuts
26
+ };
27
+ const urlRegex = options?.urlRegex ?? URL_RE;
28
+ return [
29
+ syntaxHighlighting(linkHighlight),
30
+ EditorView.domEventHandlers({ paste(event, view) {
31
+ const range = view.state.selection.main;
32
+ if (range.empty) return false;
33
+ if (isInsideCodeBlock(view)) return false;
34
+ const text = event.clipboardData?.getData("text/plain")?.trim();
35
+ if (!text || !urlRegex.test(text)) return false;
36
+ event.preventDefault();
37
+ const label = view.state.sliceDoc(range.from, range.to);
38
+ insertLink(view, range.from, range.to, label, text);
39
+ return true;
40
+ } }),
41
+ shortcutRegistry.of([{
42
+ key: keys.insertLink,
43
+ action: "insertLink",
44
+ plugin: "link"
45
+ }, {
46
+ key: keys.removeLink,
47
+ action: "removeLink",
48
+ plugin: "link"
49
+ }]),
50
+ markRegistry.of([{
51
+ mark: "link",
52
+ detect: (nodeName) => nodeName === "Link"
53
+ }]),
54
+ keymap.of([{
55
+ key: keys.removeLink,
56
+ run(view) {
57
+ if (isInsideCodeBlock(view)) return false;
58
+ const { state } = view;
59
+ const range = state.selection.main;
60
+ const existing = findLinkAtCursor(state, range.from);
61
+ if (!existing) return false;
62
+ view.dispatch({
63
+ changes: {
64
+ from: existing.from,
65
+ to: existing.to,
66
+ insert: existing.label
67
+ },
68
+ selection: EditorSelection.cursor(existing.from + existing.label.length)
69
+ });
70
+ return true;
71
+ }
72
+ }, {
73
+ key: keys.insertLink,
74
+ run(view) {
75
+ if (isInsideCodeBlock(view)) return false;
76
+ const { state } = view;
77
+ const range = state.selection.main;
78
+ const existing = findLinkAtCursor(state, range.from);
79
+ const coords = view.coordsAtPos(range.from);
80
+ if (!coords) return false;
81
+ const req = {
82
+ type: "link",
83
+ x: coords.left,
84
+ y: coords.bottom,
85
+ data: existing ? {
86
+ text: existing.label,
87
+ url: existing.url,
88
+ from: existing.from,
89
+ to: existing.to
90
+ } : {
91
+ text: state.sliceDoc(range.from, range.to),
92
+ url: "",
93
+ from: range.from,
94
+ to: range.to
95
+ }
96
+ };
97
+ view.dispatch({ effects: openPopover.of(req) });
98
+ return true;
99
+ }
100
+ }])
101
+ ];
102
+ }
103
+ //#endregion
104
+ export { linkPlugin };
@@ -0,0 +1,14 @@
1
+ import { Extension } from "@codemirror/state";
2
+
3
+ //#region src/core/plugins/list.d.ts
4
+ interface ListPluginOptions {
5
+ shortcuts?: {
6
+ unorderedList?: string;
7
+ orderedList?: string;
8
+ taskList?: string;
9
+ toggleTask?: string;
10
+ };
11
+ }
12
+ declare function listPlugin(options?: ListPluginOptions): Extension;
13
+ //#endregion
14
+ export { ListPluginOptions, listPlugin };
@@ -0,0 +1,91 @@
1
+ import { markRegistry, shortcutRegistry } from "../registry.js";
2
+ import { toggleListKind, toggleTaskCheck } from "../commands.js";
3
+ import { keymap } from "@codemirror/view";
4
+ import { syntaxTree } from "@codemirror/language";
5
+ //#region src/core/plugins/list.ts
6
+ const defaultShortcuts = {
7
+ orderedList: "Mod-Shift-7",
8
+ unorderedList: "Mod-Shift-8",
9
+ taskList: "Mod-Shift-9",
10
+ toggleTask: "Mod-Enter"
11
+ };
12
+ /** Walk up from pos to find a ListItem node containing a Task child */
13
+ function hasTaskAtLine(state, pos) {
14
+ let node = syntaxTree(state).resolveInner(pos, 1);
15
+ while (node) {
16
+ if (node.name === "ListItem") return !!node.getChild("Task");
17
+ if (!node.parent) break;
18
+ node = node.parent;
19
+ }
20
+ return false;
21
+ }
22
+ function listPlugin(options) {
23
+ const keys = {
24
+ ...defaultShortcuts,
25
+ ...options?.shortcuts
26
+ };
27
+ return [
28
+ shortcutRegistry.of([
29
+ {
30
+ key: keys.unorderedList,
31
+ action: "unorderedList",
32
+ plugin: "list"
33
+ },
34
+ {
35
+ key: keys.orderedList,
36
+ action: "orderedList",
37
+ plugin: "list"
38
+ },
39
+ {
40
+ key: keys.taskList,
41
+ action: "taskList",
42
+ plugin: "list"
43
+ },
44
+ {
45
+ key: keys.toggleTask,
46
+ action: "toggleTask",
47
+ plugin: "list"
48
+ }
49
+ ]),
50
+ markRegistry.of([
51
+ {
52
+ mark: "ul",
53
+ detect: (nodeName, state, pos) => {
54
+ if (nodeName !== "BulletList") return false;
55
+ return !hasTaskAtLine(state, pos);
56
+ }
57
+ },
58
+ {
59
+ mark: "ol",
60
+ detect: (nodeName) => nodeName === "OrderedList"
61
+ },
62
+ {
63
+ mark: "task",
64
+ detect: (nodeName, state, pos) => {
65
+ if (nodeName !== "__line__") return false;
66
+ return hasTaskAtLine(state, pos);
67
+ }
68
+ }
69
+ ]),
70
+ keymap.of([
71
+ {
72
+ key: keys.unorderedList,
73
+ run: toggleListKind("ul")
74
+ },
75
+ {
76
+ key: keys.orderedList,
77
+ run: toggleListKind("ol")
78
+ },
79
+ {
80
+ key: keys.taskList,
81
+ run: toggleListKind("task")
82
+ },
83
+ {
84
+ key: keys.toggleTask,
85
+ run: toggleTaskCheck
86
+ }
87
+ ])
88
+ ];
89
+ }
90
+ //#endregion
91
+ export { listPlugin };
@@ -0,0 +1,12 @@
1
+ import { EditorView } from "@codemirror/view";
2
+ import { Extension } from "@codemirror/state";
3
+
4
+ //#region src/core/plugins/table.d.ts
5
+ interface TablePluginOptions {
6
+ shortcuts?: {
7
+ formatTable?: string;
8
+ };
9
+ }
10
+ declare function tablePlugin(options?: TablePluginOptions): Extension;
11
+ //#endregion
12
+ export { TablePluginOptions, tablePlugin };
@@ -0,0 +1,161 @@
1
+ import { shortcutRegistry } from "../registry.js";
2
+ import { Decoration, EditorView, ViewPlugin, keymap } from "@codemirror/view";
3
+ import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
4
+ import { EditorSelection, RangeSetBuilder } from "@codemirror/state";
5
+ //#region src/core/plugins/table.ts
6
+ const defaultShortcuts = { formatTable: "Mod-Alt-f" };
7
+ /**
8
+ * Find the Table node containing the cursor position.
9
+ * Returns the node's from/to range or null if not in a table.
10
+ */
11
+ function findTableAtCursor(view) {
12
+ const { state } = view;
13
+ const pos = state.selection.main.from;
14
+ const tree = ensureSyntaxTree(state, pos) ?? syntaxTree(state);
15
+ for (const bias of [-1, 1]) {
16
+ let node = tree.resolveInner(pos, bias);
17
+ while (node) {
18
+ if (node.name === "Table") return {
19
+ from: node.from,
20
+ to: node.to
21
+ };
22
+ if (!node.parent) break;
23
+ node = node.parent;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * Parse a markdown table string into a 2D array of cell strings.
30
+ * Returns { rows, delimiterIndex } where delimiterIndex is the index
31
+ * of the separator row (e.g. |---|---|).
32
+ */
33
+ function parseTable(text) {
34
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
35
+ if (lines.length < 2) return null;
36
+ const rows = [];
37
+ let delimiterIndex = -1;
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const cells = lines[i].trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
40
+ if (delimiterIndex === -1 && cells.every((c) => /^:?-{1,}:?$/.test(c))) delimiterIndex = i;
41
+ rows.push(cells);
42
+ }
43
+ if (delimiterIndex === -1) return null;
44
+ return {
45
+ rows,
46
+ delimiterIndex
47
+ };
48
+ }
49
+ /**
50
+ * Format a parsed table into an aligned markdown string.
51
+ */
52
+ function formatTable(rows, delimiterIndex) {
53
+ const colCount = rows[0].length;
54
+ const alignments = [];
55
+ const delimRow = rows[delimiterIndex];
56
+ for (let col = 0; col < colCount; col++) {
57
+ const cell = col < delimRow.length ? delimRow[col] : "---";
58
+ const left = cell.startsWith(":");
59
+ const right = cell.endsWith(":");
60
+ if (left && right) alignments.push("center");
61
+ else if (right) alignments.push("right");
62
+ else if (left) alignments.push("left");
63
+ else alignments.push("none");
64
+ }
65
+ const colWidths = Array.from({ length: colCount }, () => 3);
66
+ for (let i = 0; i < rows.length; i++) {
67
+ if (i === delimiterIndex) continue;
68
+ for (let col = 0; col < colCount; col++) {
69
+ const cell = col < rows[i].length ? rows[i][col] : "";
70
+ colWidths[col] = Math.max(colWidths[col], cell.length);
71
+ }
72
+ }
73
+ const lines = [];
74
+ for (let i = 0; i < rows.length; i++) if (i === delimiterIndex) {
75
+ const cells = colWidths.map((w, col) => {
76
+ const align = alignments[col];
77
+ const dashes = "-".repeat(w);
78
+ if (align === "center") return `:${dashes}:`;
79
+ if (align === "right") return `${dashes}:`;
80
+ if (align === "left") return `:${dashes}`;
81
+ return dashes;
82
+ });
83
+ lines.push(`| ${cells.join(" | ")} |`);
84
+ } else {
85
+ const cells = colWidths.map((w, col) => {
86
+ return (col < rows[i].length ? rows[i][col] : "").padEnd(w);
87
+ });
88
+ lines.push(`| ${cells.join(" | ")} |`);
89
+ }
90
+ return lines.join("\n");
91
+ }
92
+ function formatTableCommand(view) {
93
+ const tableRange = findTableAtCursor(view);
94
+ if (!tableRange) return false;
95
+ const tableText = view.state.sliceDoc(tableRange.from, tableRange.to);
96
+ const parsed = parseTable(tableText);
97
+ if (!parsed) return false;
98
+ const formatted = formatTable(parsed.rows, parsed.delimiterIndex);
99
+ if (formatted === tableText) return true;
100
+ const cursorOffset = view.state.selection.main.from - tableRange.from;
101
+ const newCursorPos = Math.min(tableRange.from + cursorOffset, tableRange.from + formatted.length);
102
+ view.dispatch({
103
+ changes: {
104
+ from: tableRange.from,
105
+ to: tableRange.to,
106
+ insert: formatted
107
+ },
108
+ selection: EditorSelection.cursor(newCursorPos)
109
+ });
110
+ return true;
111
+ }
112
+ const tableLineDecoration = Decoration.line({ class: "cm-table-line" });
113
+ function buildTableDecorations(state) {
114
+ const builder = new RangeSetBuilder();
115
+ syntaxTree(state).iterate({ enter(node) {
116
+ if (node.name === "Table") {
117
+ const from = state.doc.lineAt(node.from).number;
118
+ const to = state.doc.lineAt(node.to).number;
119
+ for (let i = from; i <= to; i++) {
120
+ const line = state.doc.line(i);
121
+ builder.add(line.from, line.from, tableLineDecoration);
122
+ }
123
+ }
124
+ } });
125
+ return builder.finish();
126
+ }
127
+ const tableLineDecorations = ViewPlugin.fromClass(class {
128
+ decorations;
129
+ constructor(view) {
130
+ this.decorations = buildTableDecorations(view.state);
131
+ }
132
+ update(update) {
133
+ if (update.docChanged || update.viewportChanged || syntaxTree(update.state) !== syntaxTree(update.startState)) this.decorations = buildTableDecorations(update.state);
134
+ }
135
+ }, { decorations: (v) => v.decorations });
136
+ const tableTheme = EditorView.theme({ ".cm-table-line": {
137
+ fontFamily: "var(--lp-font-mono, ui-monospace, monospace)",
138
+ fontSize: "0.875em"
139
+ } });
140
+ function tablePlugin(options) {
141
+ const keys = {
142
+ ...defaultShortcuts,
143
+ ...options?.shortcuts
144
+ };
145
+ return [
146
+ shortcutRegistry.of([{
147
+ key: keys.formatTable,
148
+ action: "formatTable",
149
+ plugin: "table"
150
+ }]),
151
+ keymap.of([{
152
+ key: keys.formatTable,
153
+ run: formatTableCommand,
154
+ preventDefault: true
155
+ }]),
156
+ tableLineDecorations,
157
+ tableTheme
158
+ ];
159
+ }
160
+ //#endregion
161
+ export { tablePlugin };
@@ -0,0 +1,9 @@
1
+ import { CodeBlockData, CodeBlockPluginOptions, CodeBlockRequest, codeBlockPlugin } from "./plugins/code-block.js";
2
+ import { InlinePluginOptions, inlinePlugin } from "./plugins/inline.js";
3
+ import { HeadingPluginOptions, headingPlugin } from "./plugins/heading.js";
4
+ import { ListPluginOptions, listPlugin } from "./plugins/list.js";
5
+ import { BlockquotePluginOptions, blockquotePlugin } from "./plugins/blockquote.js";
6
+ import { LinkData, LinkPluginOptions, LinkRequest, linkPlugin } from "./plugins/link.js";
7
+ import { TablePluginOptions, tablePlugin } from "./plugins/table.js";
8
+ import { bracketPlugin } from "./plugins/bracket.js";
9
+ export { type BlockquotePluginOptions, type CodeBlockData, type CodeBlockPluginOptions, type CodeBlockRequest, type HeadingPluginOptions, type InlinePluginOptions, type LinkData, type LinkPluginOptions, type LinkRequest, type ListPluginOptions, type TablePluginOptions, blockquotePlugin, bracketPlugin, codeBlockPlugin, headingPlugin, inlinePlugin, linkPlugin, listPlugin, tablePlugin };
@@ -0,0 +1,9 @@
1
+ import { inlinePlugin } from "./plugins/inline.js";
2
+ import { headingPlugin } from "./plugins/heading.js";
3
+ import { listPlugin } from "./plugins/list.js";
4
+ import { blockquotePlugin } from "./plugins/blockquote.js";
5
+ import { linkPlugin } from "./plugins/link.js";
6
+ import { codeBlockPlugin } from "./plugins/code-block.js";
7
+ import { tablePlugin } from "./plugins/table.js";
8
+ import { bracketPlugin } from "./plugins/bracket.js";
9
+ export { blockquotePlugin, bracketPlugin, codeBlockPlugin, headingPlugin, inlinePlugin, linkPlugin, listPlugin, tablePlugin };
@@ -0,0 +1,9 @@
1
+ import { PopoverRequest } from "./types.js";
2
+ import { StateEffectType, StateField } from "@codemirror/state";
3
+
4
+ //#region src/core/popover.d.ts
5
+ declare const openPopover: StateEffectType<PopoverRequest>;
6
+ declare const closePopover: StateEffectType<void>;
7
+ declare const popoverField: StateField<PopoverRequest | null>;
8
+ //#endregion
9
+ export { closePopover, openPopover, popoverField };
@@ -0,0 +1,16 @@
1
+ import { StateEffect, StateField } from "@codemirror/state";
2
+ //#region src/core/popover.ts
3
+ const openPopover = StateEffect.define();
4
+ const closePopover = StateEffect.define();
5
+ const popoverField = StateField.define({
6
+ create: () => null,
7
+ update(value, tr) {
8
+ for (const e of tr.effects) {
9
+ if (e.is(openPopover)) return e.value;
10
+ if (e.is(closePopover)) return null;
11
+ }
12
+ return value;
13
+ }
14
+ });
15
+ //#endregion
16
+ export { closePopover, openPopover, popoverField };
@@ -0,0 +1,10 @@
1
+ import { MarkDetector, ShortcutEntry } from "./types.js";
2
+ import { Facet } from "@codemirror/state";
3
+
4
+ //#region src/core/registry.d.ts
5
+ /** Plugins register their keybindings here so the React layer can display them. */
6
+ declare const shortcutRegistry: Facet<ShortcutEntry[], ShortcutEntry[]>;
7
+ /** Plugins register node-to-mark detectors here for active mark detection. */
8
+ declare const markRegistry: Facet<MarkDetector[], MarkDetector[]>;
9
+ //#endregion
10
+ export { markRegistry, shortcutRegistry };
@@ -0,0 +1,8 @@
1
+ import { Facet } from "@codemirror/state";
2
+ //#region src/core/registry.ts
3
+ /** Plugins register their keybindings here so the React layer can display them. */
4
+ const shortcutRegistry = Facet.define({ combine: (values) => values.flat() });
5
+ /** Plugins register node-to-mark detectors here for active mark detection. */
6
+ const markRegistry = Facet.define({ combine: (values) => values.flat() });
7
+ //#endregion
8
+ export { markRegistry, shortcutRegistry };
@@ -0,0 +1,25 @@
1
+ import { EditorState } from "@codemirror/state";
2
+
3
+ //#region src/core/types.d.ts
4
+ interface ShortcutDef {
5
+ key: string;
6
+ action: string;
7
+ }
8
+ interface PopoverRequest<Type extends string = string, Data = unknown> {
9
+ type: Type;
10
+ /** Screen coordinates for anchoring the popover */
11
+ x: number;
12
+ y: number;
13
+ data: Data;
14
+ }
15
+ interface ShortcutEntry {
16
+ key: string;
17
+ action: string;
18
+ plugin: string;
19
+ }
20
+ interface MarkDetector {
21
+ mark: string;
22
+ detect: (nodeName: string, state: EditorState, pos: number) => boolean;
23
+ }
24
+ //#endregion
25
+ export { MarkDetector, PopoverRequest, ShortcutDef, ShortcutEntry };
File without changes
@@ -0,0 +1,13 @@
1
+ import { EditorView } from "@codemirror/view";
2
+
3
+ //#region src/core/utils.d.ts
4
+ /**
5
+ * Returns true when the primary cursor sits inside a fenced code block.
6
+ * Used to disable inline/block formatting commands that would produce
7
+ * broken markdown if applied inside code.
8
+ */
9
+ declare function isInsideCodeBlock(view: EditorView): boolean;
10
+ /** Parse a CodeMirror key string into individual key tokens for display */
11
+ declare function formatShortcutKeys(key: string): string[];
12
+ //#endregion
13
+ export { formatShortcutKeys, isInsideCodeBlock };
@@ -0,0 +1,32 @@
1
+ import { syntaxTree } from "@codemirror/language";
2
+ //#region src/core/utils.ts
3
+ /**
4
+ * Returns true when the primary cursor sits inside a fenced code block.
5
+ * Used to disable inline/block formatting commands that would produce
6
+ * broken markdown if applied inside code.
7
+ */
8
+ function isInsideCodeBlock(view) {
9
+ const pos = view.state.selection.main.from;
10
+ let node = syntaxTree(view.state).resolveInner(pos, -1);
11
+ while (node) {
12
+ if (node.name === "FencedCode") return true;
13
+ if (!node.parent) break;
14
+ node = node.parent;
15
+ }
16
+ return false;
17
+ }
18
+ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
19
+ /** Parse a CodeMirror key string into individual key tokens for display */
20
+ function formatShortcutKeys(key) {
21
+ return key.split("-").map((part) => {
22
+ switch (part) {
23
+ case "Mod": return isMac ? "⌘" : "Ctrl";
24
+ case "Shift": return isMac ? "⇧" : "Shift";
25
+ case "Alt": return isMac ? "⌥" : "Alt";
26
+ case "Enter": return "⏎";
27
+ default: return part;
28
+ }
29
+ });
30
+ }
31
+ //#endregion
32
+ export { formatShortcutKeys, isInsideCodeBlock };