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.
- 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 +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 };
|