leepi 0.0.0 → 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.
- package/dist/core/active-marks.d.ts +15 -0
- package/dist/core/active-marks.js +57 -0
- package/dist/core/commands.d.ts +39 -0
- package/dist/core/commands.js +415 -0
- package/dist/core/editor.d.ts +25 -0
- package/dist/core/editor.js +102 -0
- package/dist/core/field-notifier.d.ts +21 -0
- package/dist/core/field-notifier.js +56 -0
- package/dist/core/highlight-style.js +60 -0
- package/dist/core/highlight.d.ts +8 -0
- package/dist/core/highlight.js +34 -0
- package/dist/core/plugins/blockquote.d.ts +11 -0
- package/dist/core/plugins/blockquote.js +78 -0
- package/dist/core/plugins/bracket.d.ts +6 -0
- package/dist/core/plugins/bracket.js +38 -0
- package/dist/core/plugins/code-block.d.ts +27 -0
- package/dist/core/plugins/code-block.js +207 -0
- package/dist/core/plugins/heading.d.ts +13 -0
- package/dist/core/plugins/heading.js +111 -0
- package/dist/core/plugins/inline.d.ts +14 -0
- package/dist/core/plugins/inline.js +103 -0
- package/dist/core/plugins/link.d.ts +25 -0
- package/dist/core/plugins/link.js +104 -0
- package/dist/core/plugins/list.d.ts +14 -0
- package/dist/core/plugins/list.js +91 -0
- package/dist/core/plugins/table.d.ts +12 -0
- package/dist/core/plugins/table.js +161 -0
- package/dist/core/plugins.d.ts +9 -0
- package/dist/core/plugins.js +9 -0
- package/dist/core/popover.d.ts +9 -0
- package/dist/core/popover.js +16 -0
- package/dist/core/registry.d.ts +10 -0
- package/dist/core/registry.js +8 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +0 -0
- package/dist/core/utils.d.ts +13 -0
- package/dist/core/utils.js +32 -0
- package/dist/leepi.css +461 -0
- package/dist/react/code-block-popover.d.ts +76 -0
- package/dist/react/code-block-popover.js +223 -0
- package/dist/react/context.d.ts +42 -0
- package/dist/react/context.js +88 -0
- package/dist/react/editor.d.ts +30 -0
- package/dist/react/editor.js +60 -0
- package/dist/react/floating-toolbar.d.ts +30 -0
- package/dist/react/floating-toolbar.js +87 -0
- package/dist/react/link-popover.d.ts +70 -0
- package/dist/react/link-popover.js +222 -0
- package/dist/react/preview.d.ts +13 -0
- package/dist/react/preview.js +56 -0
- package/dist/react/toolbar.d.ts +51 -0
- package/dist/react/toolbar.js +161 -0
- package/package.json +90 -1
- package/src/core/active-marks.ts +89 -0
- package/src/core/commands.ts +461 -0
- package/src/core/editor.ts +139 -0
- package/src/core/field-notifier.ts +71 -0
- package/src/core/highlight-style.ts +66 -0
- package/src/core/highlight.ts +50 -0
- package/src/core/plugins/blockquote.ts +108 -0
- package/src/core/plugins/bracket.ts +34 -0
- package/src/core/plugins/code-block.ts +195 -0
- package/src/core/plugins/heading.ts +95 -0
- package/src/core/plugins/index.ts +16 -0
- package/src/core/plugins/inline.ts +62 -0
- package/src/core/plugins/link.ts +124 -0
- package/src/core/plugins/list.ts +68 -0
- package/src/core/plugins/table.ts +217 -0
- package/src/core/popover.ts +17 -0
- package/src/core/registry.ts +18 -0
- package/src/core/types.ts +25 -0
- package/src/core/utils.ts +38 -0
- package/src/react/code-block-popover.tsx +387 -0
- package/src/react/context.tsx +153 -0
- package/src/react/editor.tsx +106 -0
- package/src/react/floating-toolbar.tsx +161 -0
- package/src/react/link-popover.tsx +354 -0
- package/src/react/preview.tsx +80 -0
- package/src/react/toolbar.tsx +294 -0
- package/src/styles/floating-toolbar.css +52 -0
- package/src/styles/leepi.css +2 -0
- package/src/styles/popover.css +93 -0
- package/src/styles/preview.css +191 -0
- package/src/styles/theme.css +99 -0
- package/src/styles/tokens.css +63 -0
- package/src/styles/toolbar.css +55 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type Extension, EditorSelection } from "@codemirror/state";
|
|
2
|
+
import { keymap, EditorView } from "@codemirror/view";
|
|
3
|
+
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
|
4
|
+
import { tags } from "@lezer/highlight";
|
|
5
|
+
import { shortcutRegistry, markRegistry } from "../registry";
|
|
6
|
+
import { findLinkAtCursor, insertLink } from "../commands";
|
|
7
|
+
import { isInsideCodeBlock } from "../utils";
|
|
8
|
+
import { openPopover } from "../popover";
|
|
9
|
+
import type { PopoverRequest } from "../types";
|
|
10
|
+
|
|
11
|
+
export interface LinkData {
|
|
12
|
+
/** Selected text or existing link label */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Pre-filled URL when editing an existing link */
|
|
15
|
+
url: string;
|
|
16
|
+
/** Document range to replace when the link is inserted/removed */
|
|
17
|
+
from: number;
|
|
18
|
+
to: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LinkRequest = PopoverRequest<"link", LinkData>;
|
|
22
|
+
|
|
23
|
+
export interface LinkPluginOptions {
|
|
24
|
+
shortcuts?: {
|
|
25
|
+
insertLink?: string;
|
|
26
|
+
removeLink?: string;
|
|
27
|
+
};
|
|
28
|
+
urlRegex?: RegExp;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultShortcuts = {
|
|
32
|
+
insertLink: "Mod-k",
|
|
33
|
+
removeLink: "Mod-Shift-k",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const URL_RE = /^(?:https?:\/\/|mailto:|tel:)\S+$/;
|
|
37
|
+
|
|
38
|
+
const linkHighlight = HighlightStyle.define([
|
|
39
|
+
{ tag: tags.link, color: "var(--lp-color-accent)" },
|
|
40
|
+
{ tag: tags.url, color: "var(--lp-color-accent)" },
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export function linkPlugin(options?: LinkPluginOptions): Extension {
|
|
44
|
+
const keys = { ...defaultShortcuts, ...options?.shortcuts };
|
|
45
|
+
const urlRegex = options?.urlRegex ?? URL_RE;
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
syntaxHighlighting(linkHighlight),
|
|
49
|
+
EditorView.domEventHandlers({
|
|
50
|
+
paste(event, view) {
|
|
51
|
+
const range = view.state.selection.main;
|
|
52
|
+
if (range.empty) return false;
|
|
53
|
+
if (isInsideCodeBlock(view)) return false;
|
|
54
|
+
|
|
55
|
+
const text = event.clipboardData?.getData("text/plain")?.trim();
|
|
56
|
+
if (!text || !urlRegex.test(text)) return false;
|
|
57
|
+
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
const label = view.state.sliceDoc(range.from, range.to);
|
|
60
|
+
insertLink(view, range.from, range.to, label, text);
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
shortcutRegistry.of([
|
|
65
|
+
{ key: keys.insertLink, action: "insertLink", plugin: "link" },
|
|
66
|
+
{ key: keys.removeLink, action: "removeLink", plugin: "link" },
|
|
67
|
+
]),
|
|
68
|
+
markRegistry.of([{ mark: "link", detect: (nodeName) => nodeName === "Link" }]),
|
|
69
|
+
keymap.of([
|
|
70
|
+
{
|
|
71
|
+
key: keys.removeLink,
|
|
72
|
+
run(view) {
|
|
73
|
+
if (isInsideCodeBlock(view)) return false;
|
|
74
|
+
const { state } = view;
|
|
75
|
+
const range = state.selection.main;
|
|
76
|
+
const existing = findLinkAtCursor(state, range.from);
|
|
77
|
+
if (!existing) return false;
|
|
78
|
+
|
|
79
|
+
view.dispatch({
|
|
80
|
+
changes: { from: existing.from, to: existing.to, insert: existing.label },
|
|
81
|
+
selection: EditorSelection.cursor(existing.from + existing.label.length),
|
|
82
|
+
});
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: keys.insertLink,
|
|
88
|
+
run(view) {
|
|
89
|
+
if (isInsideCodeBlock(view)) return false;
|
|
90
|
+
const { state } = view;
|
|
91
|
+
const range = state.selection.main;
|
|
92
|
+
const existing = findLinkAtCursor(state, range.from);
|
|
93
|
+
const coords = view.coordsAtPos(range.from);
|
|
94
|
+
if (!coords) return false;
|
|
95
|
+
|
|
96
|
+
const req: LinkRequest = {
|
|
97
|
+
type: "link",
|
|
98
|
+
x: coords.left,
|
|
99
|
+
y: coords.bottom,
|
|
100
|
+
data: existing
|
|
101
|
+
? {
|
|
102
|
+
text: existing.label,
|
|
103
|
+
url: existing.url,
|
|
104
|
+
from: existing.from,
|
|
105
|
+
to: existing.to,
|
|
106
|
+
}
|
|
107
|
+
: {
|
|
108
|
+
text: state.sliceDoc(range.from, range.to),
|
|
109
|
+
url: "",
|
|
110
|
+
from: range.from,
|
|
111
|
+
to: range.to,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
view.dispatch({ effects: openPopover.of(req) });
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
]),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Re-export for convenience
|
|
124
|
+
export { insertLink, findLinkAtCursor };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Extension, type EditorState } from "@codemirror/state";
|
|
2
|
+
import { keymap } from "@codemirror/view";
|
|
3
|
+
import { syntaxTree } from "@codemirror/language";
|
|
4
|
+
import { shortcutRegistry, markRegistry } from "../registry";
|
|
5
|
+
import { toggleListKind, toggleTaskCheck } from "../commands";
|
|
6
|
+
|
|
7
|
+
export interface ListPluginOptions {
|
|
8
|
+
shortcuts?: {
|
|
9
|
+
unorderedList?: string;
|
|
10
|
+
orderedList?: string;
|
|
11
|
+
taskList?: string;
|
|
12
|
+
toggleTask?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const defaultShortcuts = {
|
|
17
|
+
orderedList: "Mod-Shift-7",
|
|
18
|
+
unorderedList: "Mod-Shift-8",
|
|
19
|
+
taskList: "Mod-Shift-9",
|
|
20
|
+
toggleTask: "Mod-Enter",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Walk up from pos to find a ListItem node containing a Task child */
|
|
24
|
+
function hasTaskAtLine(state: EditorState, pos: number): boolean {
|
|
25
|
+
let node = syntaxTree(state).resolveInner(pos, 1);
|
|
26
|
+
while (node) {
|
|
27
|
+
if (node.name === "ListItem") return !!node.getChild("Task");
|
|
28
|
+
if (!node.parent) break;
|
|
29
|
+
node = node.parent;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function listPlugin(options?: ListPluginOptions): Extension {
|
|
35
|
+
const keys = { ...defaultShortcuts, ...options?.shortcuts };
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
shortcutRegistry.of([
|
|
39
|
+
{ key: keys.unorderedList, action: "unorderedList", plugin: "list" },
|
|
40
|
+
{ key: keys.orderedList, action: "orderedList", plugin: "list" },
|
|
41
|
+
{ key: keys.taskList, action: "taskList", plugin: "list" },
|
|
42
|
+
{ key: keys.toggleTask, action: "toggleTask", plugin: "list" },
|
|
43
|
+
]),
|
|
44
|
+
markRegistry.of([
|
|
45
|
+
{
|
|
46
|
+
mark: "ul",
|
|
47
|
+
detect: (nodeName, state, pos) => {
|
|
48
|
+
if (nodeName !== "BulletList") return false;
|
|
49
|
+
return !hasTaskAtLine(state, pos);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{ mark: "ol", detect: (nodeName) => nodeName === "OrderedList" },
|
|
53
|
+
{
|
|
54
|
+
mark: "task",
|
|
55
|
+
detect: (nodeName, state, pos) => {
|
|
56
|
+
if (nodeName !== "__line__") return false;
|
|
57
|
+
return hasTaskAtLine(state, pos);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
keymap.of([
|
|
62
|
+
{ key: keys.unorderedList, run: toggleListKind("ul") },
|
|
63
|
+
{ key: keys.orderedList, run: toggleListKind("ol") },
|
|
64
|
+
{ key: keys.taskList, run: toggleListKind("task") },
|
|
65
|
+
{ key: keys.toggleTask, run: toggleTaskCheck },
|
|
66
|
+
]),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { Extension } from "@codemirror/state";
|
|
2
|
+
import { EditorSelection, RangeSetBuilder } from "@codemirror/state";
|
|
3
|
+
import type { EditorState } from "@codemirror/state";
|
|
4
|
+
import {
|
|
5
|
+
Decoration,
|
|
6
|
+
type DecorationSet,
|
|
7
|
+
EditorView,
|
|
8
|
+
ViewPlugin,
|
|
9
|
+
type ViewUpdate,
|
|
10
|
+
keymap,
|
|
11
|
+
} from "@codemirror/view";
|
|
12
|
+
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
|
|
13
|
+
import { shortcutRegistry } from "../registry";
|
|
14
|
+
|
|
15
|
+
export interface TablePluginOptions {
|
|
16
|
+
shortcuts?: {
|
|
17
|
+
formatTable?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultShortcuts = {
|
|
22
|
+
formatTable: "Mod-Alt-f",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Find the Table node containing the cursor position.
|
|
27
|
+
* Returns the node's from/to range or null if not in a table.
|
|
28
|
+
*/
|
|
29
|
+
function findTableAtCursor(view: EditorView): { from: number; to: number } | null {
|
|
30
|
+
const { state } = view;
|
|
31
|
+
const pos = state.selection.main.from;
|
|
32
|
+
// Ensure the tree is fully parsed up to the cursor position
|
|
33
|
+
const tree = ensureSyntaxTree(state, pos) ?? syntaxTree(state);
|
|
34
|
+
|
|
35
|
+
// Try both biases — at line start (on a `|`), bias -1 may land outside the Table
|
|
36
|
+
for (const bias of [-1, 1] as const) {
|
|
37
|
+
let node = tree.resolveInner(pos, bias);
|
|
38
|
+
while (node) {
|
|
39
|
+
if (node.name === "Table") return { from: node.from, to: node.to };
|
|
40
|
+
if (!node.parent) break;
|
|
41
|
+
node = node.parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a markdown table string into a 2D array of cell strings.
|
|
49
|
+
* Returns { rows, delimiterIndex } where delimiterIndex is the index
|
|
50
|
+
* of the separator row (e.g. |---|---|).
|
|
51
|
+
*/
|
|
52
|
+
function parseTable(text: string): {
|
|
53
|
+
rows: string[][];
|
|
54
|
+
delimiterIndex: number;
|
|
55
|
+
} | null {
|
|
56
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
57
|
+
if (lines.length < 2) return null;
|
|
58
|
+
|
|
59
|
+
const rows: string[][] = [];
|
|
60
|
+
let delimiterIndex = -1;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i].trim();
|
|
64
|
+
// Strip leading/trailing pipes and split
|
|
65
|
+
const stripped = line.replace(/^\|/, "").replace(/\|$/, "");
|
|
66
|
+
const cells = stripped.split("|").map((c) => c.trim());
|
|
67
|
+
|
|
68
|
+
// Detect delimiter row: all cells match ---+ or :---: patterns
|
|
69
|
+
if (delimiterIndex === -1 && cells.every((c) => /^:?-{1,}:?$/.test(c))) {
|
|
70
|
+
delimiterIndex = i;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rows.push(cells);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (delimiterIndex === -1) return null;
|
|
77
|
+
return { rows, delimiterIndex };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format a parsed table into an aligned markdown string.
|
|
82
|
+
*/
|
|
83
|
+
function formatTable(rows: string[][], delimiterIndex: number): string {
|
|
84
|
+
// Calculate column count from header row
|
|
85
|
+
const colCount = rows[0].length;
|
|
86
|
+
|
|
87
|
+
// Parse alignment from delimiter row
|
|
88
|
+
const alignments: ("left" | "center" | "right" | "none")[] = [];
|
|
89
|
+
const delimRow = rows[delimiterIndex];
|
|
90
|
+
for (let col = 0; col < colCount; col++) {
|
|
91
|
+
const cell = col < delimRow.length ? delimRow[col] : "---";
|
|
92
|
+
const left = cell.startsWith(":");
|
|
93
|
+
const right = cell.endsWith(":");
|
|
94
|
+
if (left && right) alignments.push("center");
|
|
95
|
+
else if (right) alignments.push("right");
|
|
96
|
+
else if (left) alignments.push("left");
|
|
97
|
+
else alignments.push("none");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Calculate max width per column (excluding delimiter row)
|
|
101
|
+
const colWidths: number[] = Array.from({ length: colCount }, () => 3); // minimum 3 for "---"
|
|
102
|
+
for (let i = 0; i < rows.length; i++) {
|
|
103
|
+
if (i === delimiterIndex) continue;
|
|
104
|
+
for (let col = 0; col < colCount; col++) {
|
|
105
|
+
const cell = col < rows[i].length ? rows[i][col] : "";
|
|
106
|
+
colWidths[col] = Math.max(colWidths[col], cell.length);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Build formatted lines
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
for (let i = 0; i < rows.length; i++) {
|
|
113
|
+
if (i === delimiterIndex) {
|
|
114
|
+
// Build delimiter row
|
|
115
|
+
const cells = colWidths.map((w, col) => {
|
|
116
|
+
const align = alignments[col];
|
|
117
|
+
const dashes = "-".repeat(w);
|
|
118
|
+
if (align === "center") return `:${dashes}:`;
|
|
119
|
+
if (align === "right") return `${dashes}:`;
|
|
120
|
+
if (align === "left") return `:${dashes}`;
|
|
121
|
+
return dashes;
|
|
122
|
+
});
|
|
123
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
124
|
+
} else {
|
|
125
|
+
const cells = colWidths.map((w, col) => {
|
|
126
|
+
const cell = col < rows[i].length ? rows[i][col] : "";
|
|
127
|
+
return cell.padEnd(w);
|
|
128
|
+
});
|
|
129
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function formatTableCommand(view: EditorView): boolean {
|
|
137
|
+
const tableRange = findTableAtCursor(view);
|
|
138
|
+
if (!tableRange) return false;
|
|
139
|
+
|
|
140
|
+
const tableText = view.state.sliceDoc(tableRange.from, tableRange.to);
|
|
141
|
+
const parsed = parseTable(tableText);
|
|
142
|
+
if (!parsed) return false;
|
|
143
|
+
|
|
144
|
+
const formatted = formatTable(parsed.rows, parsed.delimiterIndex);
|
|
145
|
+
if (formatted === tableText) return true;
|
|
146
|
+
|
|
147
|
+
// Calculate cursor offset relative to table start to preserve position
|
|
148
|
+
const cursorOffset = view.state.selection.main.from - tableRange.from;
|
|
149
|
+
const newCursorPos = Math.min(tableRange.from + cursorOffset, tableRange.from + formatted.length);
|
|
150
|
+
|
|
151
|
+
view.dispatch({
|
|
152
|
+
changes: { from: tableRange.from, to: tableRange.to, insert: formatted },
|
|
153
|
+
selection: EditorSelection.cursor(newCursorPos),
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Table line decorations (monospace font) ---
|
|
159
|
+
|
|
160
|
+
const tableLineDecoration = Decoration.line({ class: "cm-table-line" });
|
|
161
|
+
|
|
162
|
+
function buildTableDecorations(state: EditorState): DecorationSet {
|
|
163
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
164
|
+
const tree = syntaxTree(state);
|
|
165
|
+
|
|
166
|
+
tree.iterate({
|
|
167
|
+
enter(node) {
|
|
168
|
+
if (node.name === "Table") {
|
|
169
|
+
const from = state.doc.lineAt(node.from).number;
|
|
170
|
+
const to = state.doc.lineAt(node.to).number;
|
|
171
|
+
for (let i = from; i <= to; i++) {
|
|
172
|
+
const line = state.doc.line(i);
|
|
173
|
+
builder.add(line.from, line.from, tableLineDecoration);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return builder.finish();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const tableLineDecorations = ViewPlugin.fromClass(
|
|
183
|
+
class {
|
|
184
|
+
decorations: DecorationSet;
|
|
185
|
+
constructor(view: EditorView) {
|
|
186
|
+
this.decorations = buildTableDecorations(view.state);
|
|
187
|
+
}
|
|
188
|
+
update(update: ViewUpdate) {
|
|
189
|
+
if (
|
|
190
|
+
update.docChanged ||
|
|
191
|
+
update.viewportChanged ||
|
|
192
|
+
syntaxTree(update.state) !== syntaxTree(update.startState)
|
|
193
|
+
) {
|
|
194
|
+
this.decorations = buildTableDecorations(update.state);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{ decorations: (v) => v.decorations },
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const tableTheme = EditorView.theme({
|
|
202
|
+
".cm-table-line": {
|
|
203
|
+
fontFamily: "var(--lp-font-mono, ui-monospace, monospace)",
|
|
204
|
+
fontSize: "0.875em",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export function tablePlugin(options?: TablePluginOptions): Extension {
|
|
209
|
+
const keys = { ...defaultShortcuts, ...options?.shortcuts };
|
|
210
|
+
|
|
211
|
+
return [
|
|
212
|
+
shortcutRegistry.of([{ key: keys.formatTable, action: "formatTable", plugin: "table" }]),
|
|
213
|
+
keymap.of([{ key: keys.formatTable, run: formatTableCommand, preventDefault: true }]),
|
|
214
|
+
tableLineDecorations,
|
|
215
|
+
tableTheme,
|
|
216
|
+
];
|
|
217
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StateEffect, type StateEffectType, StateField } from "@codemirror/state";
|
|
2
|
+
import type { PopoverRequest } from "./types";
|
|
3
|
+
|
|
4
|
+
export const openPopover: StateEffectType<PopoverRequest> = StateEffect.define<PopoverRequest>();
|
|
5
|
+
export const closePopover: StateEffectType<void> = StateEffect.define<void>();
|
|
6
|
+
|
|
7
|
+
export const popoverField: StateField<PopoverRequest | null> =
|
|
8
|
+
StateField.define<PopoverRequest | null>({
|
|
9
|
+
create: () => null,
|
|
10
|
+
update(value, tr) {
|
|
11
|
+
for (const e of tr.effects) {
|
|
12
|
+
if (e.is(openPopover)) return e.value;
|
|
13
|
+
if (e.is(closePopover)) return null;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Facet } from "@codemirror/state";
|
|
2
|
+
import type { ShortcutEntry, MarkDetector } from "./types";
|
|
3
|
+
|
|
4
|
+
/** Plugins register their keybindings here so the React layer can display them. */
|
|
5
|
+
export const shortcutRegistry: Facet<ShortcutEntry[], ShortcutEntry[]> = Facet.define<
|
|
6
|
+
ShortcutEntry[],
|
|
7
|
+
ShortcutEntry[]
|
|
8
|
+
>({
|
|
9
|
+
combine: (values) => values.flat(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/** Plugins register node-to-mark detectors here for active mark detection. */
|
|
13
|
+
export const markRegistry: Facet<MarkDetector[], MarkDetector[]> = Facet.define<
|
|
14
|
+
MarkDetector[],
|
|
15
|
+
MarkDetector[]
|
|
16
|
+
>({
|
|
17
|
+
combine: (values) => values.flat(),
|
|
18
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EditorState } from "@codemirror/state";
|
|
2
|
+
|
|
3
|
+
export interface ShortcutDef {
|
|
4
|
+
key: string;
|
|
5
|
+
action: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export 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
|
+
|
|
16
|
+
export interface ShortcutEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
action: string;
|
|
19
|
+
plugin: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MarkDetector {
|
|
23
|
+
mark: string;
|
|
24
|
+
detect: (nodeName: string, state: EditorState, pos: number) => boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EditorView } from "@codemirror/view";
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
|
|
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
|
+
export function isInsideCodeBlock(view: EditorView): boolean {
|
|
10
|
+
const pos = view.state.selection.main.from;
|
|
11
|
+
let node = syntaxTree(view.state).resolveInner(pos, -1);
|
|
12
|
+
while (node) {
|
|
13
|
+
if (node.name === "FencedCode") return true;
|
|
14
|
+
if (!node.parent) break;
|
|
15
|
+
node = node.parent;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
21
|
+
|
|
22
|
+
/** Parse a CodeMirror key string into individual key tokens for display */
|
|
23
|
+
export function formatShortcutKeys(key: string): string[] {
|
|
24
|
+
return key.split("-").map((part) => {
|
|
25
|
+
switch (part) {
|
|
26
|
+
case "Mod":
|
|
27
|
+
return isMac ? "\u2318" : "Ctrl";
|
|
28
|
+
case "Shift":
|
|
29
|
+
return isMac ? "\u21E7" : "Shift";
|
|
30
|
+
case "Alt":
|
|
31
|
+
return isMac ? "\u2325" : "Alt";
|
|
32
|
+
case "Enter":
|
|
33
|
+
return "\u23CE";
|
|
34
|
+
default:
|
|
35
|
+
return part;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|