mardora 1.2.0
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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/chunk-3OCUX4OO.js +7690 -0
- package/dist/chunk-3OCUX4OO.js.map +1 -0
- package/dist/chunk-3ZOCCFDL.cjs +74 -0
- package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
- package/dist/chunk-7JOEPNEV.cjs +7740 -0
- package/dist/chunk-7JOEPNEV.cjs.map +1 -0
- package/dist/chunk-BIKZQZ6W.js +33 -0
- package/dist/chunk-BIKZQZ6W.js.map +1 -0
- package/dist/chunk-EQJESPP2.js +234 -0
- package/dist/chunk-EQJESPP2.js.map +1 -0
- package/dist/chunk-G4SE26YY.js +70 -0
- package/dist/chunk-G4SE26YY.js.map +1 -0
- package/dist/chunk-KNDWF2DP.cjs +35 -0
- package/dist/chunk-KNDWF2DP.cjs.map +1 -0
- package/dist/chunk-MLBEBFHB.cjs +2971 -0
- package/dist/chunk-MLBEBFHB.cjs.map +1 -0
- package/dist/chunk-P7JFCYU3.js +905 -0
- package/dist/chunk-P7JFCYU3.js.map +1 -0
- package/dist/chunk-SWFUKJDO.cjs +243 -0
- package/dist/chunk-SWFUKJDO.cjs.map +1 -0
- package/dist/chunk-WFVCG4LD.cjs +926 -0
- package/dist/chunk-WFVCG4LD.cjs.map +1 -0
- package/dist/chunk-XL6WFGJT.js +2901 -0
- package/dist/chunk-XL6WFGJT.js.map +1 -0
- package/dist/editor/index.cjs +277 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +186 -0
- package/dist/editor/index.d.ts +186 -0
- package/dist/editor/index.js +4 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.cjs +405 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +12 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +16 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/mardora-DCwjomil.d.cts +640 -0
- package/dist/mardora-DCwjomil.d.ts +640 -0
- package/dist/plugins/index.cjs +104 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +740 -0
- package/dist/plugins/index.d.ts +740 -0
- package/dist/plugins/index.js +7 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/preview/index.cjs +38 -0
- package/dist/preview/index.cjs.map +1 -0
- package/dist/preview/index.d.cts +101 -0
- package/dist/preview/index.d.ts +101 -0
- package/dist/preview/index.js +5 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/types-NBsaxl4d.d.cts +71 -0
- package/dist/types-Pw2SWWAR.d.ts +71 -0
- package/package.json +92 -0
- package/src/editor/attachments/extension.ts +181 -0
- package/src/editor/attachments/format.ts +63 -0
- package/src/editor/attachments/index.ts +3 -0
- package/src/editor/attachments/types.ts +37 -0
- package/src/editor/heading-fold/config.ts +25 -0
- package/src/editor/heading-fold/extension.ts +268 -0
- package/src/editor/heading-fold/extract.ts +88 -0
- package/src/editor/heading-fold/index.ts +5 -0
- package/src/editor/heading-fold/theme.ts +85 -0
- package/src/editor/heading-fold/types.ts +24 -0
- package/src/editor/i18n.ts +13 -0
- package/src/editor/icons/index.ts +367 -0
- package/src/editor/index.ts +16 -0
- package/src/editor/mardora.ts +257 -0
- package/src/editor/media-lightbox-theme.ts +146 -0
- package/src/editor/media-lightbox.ts +125 -0
- package/src/editor/plugin.ts +294 -0
- package/src/editor/selection-toolbar/activation.ts +123 -0
- package/src/editor/selection-toolbar/commands.ts +279 -0
- package/src/editor/selection-toolbar/extension.ts +564 -0
- package/src/editor/selection-toolbar/i18n.ts +164 -0
- package/src/editor/selection-toolbar/index.ts +7 -0
- package/src/editor/selection-toolbar/menu.ts +252 -0
- package/src/editor/selection-toolbar/position.ts +43 -0
- package/src/editor/selection-toolbar/theme.ts +195 -0
- package/src/editor/selection-toolbar/types.ts +155 -0
- package/src/editor/slash/default-commands.ts +190 -0
- package/src/editor/slash/extension.ts +319 -0
- package/src/editor/slash/index.ts +7 -0
- package/src/editor/slash/insertions.ts +26 -0
- package/src/editor/slash/menu.ts +123 -0
- package/src/editor/slash/position.ts +61 -0
- package/src/editor/slash/query.ts +33 -0
- package/src/editor/slash/theme.ts +113 -0
- package/src/editor/slash/types.ts +40 -0
- package/src/editor/table-of-contents/extension.ts +202 -0
- package/src/editor/table-of-contents/extract.ts +53 -0
- package/src/editor/table-of-contents/index.ts +7 -0
- package/src/editor/table-of-contents/panel.ts +83 -0
- package/src/editor/table-of-contents/slug.ts +50 -0
- package/src/editor/table-of-contents/storage.ts +35 -0
- package/src/editor/table-of-contents/theme.ts +153 -0
- package/src/editor/table-of-contents/types.ts +44 -0
- package/src/editor/theme.ts +72 -0
- package/src/editor/utils.ts +176 -0
- package/src/editor/view-plugin.ts +189 -0
- package/src/index.ts +5 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/input-handler.ts +47 -0
- package/src/plugins/code-plugin.theme.ts +545 -0
- package/src/plugins/code-plugin.ts +1892 -0
- package/src/plugins/emoji-plugin.ts +140 -0
- package/src/plugins/heading-plugin.ts +194 -0
- package/src/plugins/hr-plugin.ts +102 -0
- package/src/plugins/html-plugin.ts +353 -0
- package/src/plugins/image-plugin.ts +806 -0
- package/src/plugins/index.ts +71 -0
- package/src/plugins/inline-plugin.ts +311 -0
- package/src/plugins/link-plugin.ts +509 -0
- package/src/plugins/list-plugin.ts +492 -0
- package/src/plugins/math-plugin.ts +526 -0
- package/src/plugins/mermaid-plugin.ts +513 -0
- package/src/plugins/paragraph-plugin.ts +38 -0
- package/src/plugins/quote-plugin.ts +733 -0
- package/src/plugins/table-controls-theme.ts +126 -0
- package/src/plugins/table-controls.ts +423 -0
- package/src/plugins/table-model.ts +661 -0
- package/src/plugins/table-plugin.ts +2111 -0
- package/src/preview/context.ts +45 -0
- package/src/preview/css-generator.ts +64 -0
- package/src/preview/default-renderers.ts +29 -0
- package/src/preview/index.ts +29 -0
- package/src/preview/preview.ts +41 -0
- package/src/preview/renderer.ts +184 -0
- package/src/preview/syntax-theme.ts +112 -0
- package/src/preview/toc.ts +23 -0
- package/src/preview/types.ts +89 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChangeSpec } from "@codemirror/state";
|
|
2
|
+
import type { MardoraSlashQuery } from "./types";
|
|
3
|
+
|
|
4
|
+
export type MardoraSlashReplacementTemplate = {
|
|
5
|
+
marker: string;
|
|
6
|
+
cursorOffset: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type MardoraSlashReplacement = {
|
|
10
|
+
changes: ChangeSpec;
|
|
11
|
+
selectionAnchor: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function buildSlashReplacement(
|
|
15
|
+
template: MardoraSlashReplacementTemplate,
|
|
16
|
+
query: MardoraSlashQuery
|
|
17
|
+
): MardoraSlashReplacement {
|
|
18
|
+
return {
|
|
19
|
+
changes: {
|
|
20
|
+
from: query.from,
|
|
21
|
+
to: query.to,
|
|
22
|
+
insert: template.marker,
|
|
23
|
+
},
|
|
24
|
+
selectionAnchor: query.from + template.cursorOffset,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { MardoraSlashCommand } from "./types";
|
|
2
|
+
import { createMardoraIcon } from "../icons";
|
|
3
|
+
import type { MardoraLocale } from "../i18n";
|
|
4
|
+
import type { MardoraSlashMessages } from "./types";
|
|
5
|
+
|
|
6
|
+
export type MardoraSlashMenuState = {
|
|
7
|
+
commands: MardoraSlashCommand[];
|
|
8
|
+
activeIndex: number;
|
|
9
|
+
messages: MardoraSlashMessages;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MardoraSlashMenuCallbacks = {
|
|
13
|
+
onHover: (index: number) => void;
|
|
14
|
+
onSelect: (index: number) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const slashMessages: Record<MardoraLocale, MardoraSlashMessages> = {
|
|
18
|
+
"zh-CN": {
|
|
19
|
+
groups: {
|
|
20
|
+
basic: "基本区块",
|
|
21
|
+
media: "媒体",
|
|
22
|
+
},
|
|
23
|
+
empty: "没有匹配的命令",
|
|
24
|
+
close: "关闭菜单",
|
|
25
|
+
closeHint: "esc",
|
|
26
|
+
},
|
|
27
|
+
"en-US": {
|
|
28
|
+
groups: {
|
|
29
|
+
basic: "Basic blocks",
|
|
30
|
+
media: "Media",
|
|
31
|
+
},
|
|
32
|
+
empty: "No matching commands",
|
|
33
|
+
close: "Close menu",
|
|
34
|
+
closeHint: "esc",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function getSlashMessages(locale: MardoraLocale): MardoraSlashMessages {
|
|
39
|
+
return slashMessages[locale];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createSlashMenuElement(
|
|
43
|
+
state: MardoraSlashMenuState,
|
|
44
|
+
callbacks: MardoraSlashMenuCallbacks
|
|
45
|
+
): HTMLElement {
|
|
46
|
+
const { messages } = state;
|
|
47
|
+
const root = document.createElement("div");
|
|
48
|
+
root.className = "cm-mardora-slash-menu";
|
|
49
|
+
root.setAttribute("role", "listbox");
|
|
50
|
+
|
|
51
|
+
const list = document.createElement("div");
|
|
52
|
+
list.className = "cm-mardora-slash-list";
|
|
53
|
+
root.addEventListener(
|
|
54
|
+
"wheel",
|
|
55
|
+
(event) => {
|
|
56
|
+
list.scrollTop += event.deltaY;
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
},
|
|
60
|
+
{ capture: true, passive: false }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (state.commands.length === 0) {
|
|
64
|
+
const empty = document.createElement("div");
|
|
65
|
+
empty.className = "cm-mardora-slash-empty";
|
|
66
|
+
empty.textContent = messages.empty;
|
|
67
|
+
list.appendChild(empty);
|
|
68
|
+
root.appendChild(list);
|
|
69
|
+
return root;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let currentGroup: MardoraSlashCommand["group"] | null = null;
|
|
73
|
+
|
|
74
|
+
for (const [index, command] of state.commands.entries()) {
|
|
75
|
+
if (command.group !== currentGroup) {
|
|
76
|
+
currentGroup = command.group;
|
|
77
|
+
const label = document.createElement("div");
|
|
78
|
+
label.className = "cm-mardora-slash-group";
|
|
79
|
+
label.textContent = messages.groups[currentGroup];
|
|
80
|
+
list.appendChild(label);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const item = document.createElement("button");
|
|
84
|
+
item.type = "button";
|
|
85
|
+
item.className =
|
|
86
|
+
index === state.activeIndex ? "cm-mardora-slash-item cm-mardora-slash-item-active" : "cm-mardora-slash-item";
|
|
87
|
+
item.dataset.mardoraSlashIndex = String(index);
|
|
88
|
+
item.setAttribute("role", "option");
|
|
89
|
+
item.setAttribute("aria-selected", String(index === state.activeIndex));
|
|
90
|
+
item.addEventListener("mouseenter", () => callbacks.onHover(index));
|
|
91
|
+
item.addEventListener("mousedown", (event) => {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
callbacks.onSelect(index);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const icon = document.createElement("span");
|
|
97
|
+
icon.className = "cm-mardora-slash-icon";
|
|
98
|
+
const svgIcon = createMardoraIcon(command.icon);
|
|
99
|
+
if (svgIcon) {
|
|
100
|
+
icon.appendChild(svgIcon);
|
|
101
|
+
} else {
|
|
102
|
+
icon.textContent = command.icon;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const title = document.createElement("span");
|
|
106
|
+
title.className = "cm-mardora-slash-title";
|
|
107
|
+
title.textContent = command.title;
|
|
108
|
+
|
|
109
|
+
const hint = document.createElement("span");
|
|
110
|
+
hint.className = "cm-mardora-slash-hint";
|
|
111
|
+
hint.textContent = command.hint;
|
|
112
|
+
|
|
113
|
+
item.append(icon, title, hint);
|
|
114
|
+
list.appendChild(item);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const footer = document.createElement("div");
|
|
118
|
+
footer.className = "cm-mardora-slash-footer";
|
|
119
|
+
footer.innerHTML = `<span>${messages.close}</span><span>${messages.closeHint}</span>`;
|
|
120
|
+
root.append(list, footer);
|
|
121
|
+
|
|
122
|
+
return root;
|
|
123
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type SlashMenuAnchorRect = {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
bottom: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type SlashMenuViewport = {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SlashMenuLayoutInput = {
|
|
13
|
+
anchor: SlashMenuAnchorRect;
|
|
14
|
+
viewport: SlashMenuViewport;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SlashMenuPlacement = "top" | "bottom";
|
|
18
|
+
|
|
19
|
+
export type SlashMenuLayout = {
|
|
20
|
+
placement: SlashMenuPlacement;
|
|
21
|
+
left: number;
|
|
22
|
+
top: number | null;
|
|
23
|
+
bottom: number | null;
|
|
24
|
+
maxHeight: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const menuWidth = 328;
|
|
28
|
+
const menuMaxHeight = 420;
|
|
29
|
+
const viewportPadding = 8;
|
|
30
|
+
const anchorGap = 6;
|
|
31
|
+
|
|
32
|
+
function clamp(value: number, min: number, max: number): number {
|
|
33
|
+
return Math.min(Math.max(value, min), max);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function computeSlashMenuLayout(input: SlashMenuLayoutInput): SlashMenuLayout {
|
|
37
|
+
const maxLeft = Math.max(viewportPadding, input.viewport.width - menuWidth - viewportPadding);
|
|
38
|
+
const left = clamp(input.anchor.left, viewportPadding, maxLeft);
|
|
39
|
+
const availableBelow = Math.max(1, input.viewport.height - input.anchor.bottom - anchorGap - viewportPadding);
|
|
40
|
+
const availableAbove = Math.max(1, input.anchor.top - anchorGap - viewportPadding);
|
|
41
|
+
const placement: SlashMenuPlacement =
|
|
42
|
+
availableBelow >= menuMaxHeight || availableBelow >= availableAbove ? "bottom" : "top";
|
|
43
|
+
|
|
44
|
+
if (placement === "bottom") {
|
|
45
|
+
return {
|
|
46
|
+
placement,
|
|
47
|
+
left,
|
|
48
|
+
top: input.anchor.bottom + anchorGap,
|
|
49
|
+
bottom: null,
|
|
50
|
+
maxHeight: Math.min(menuMaxHeight, availableBelow),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
placement,
|
|
56
|
+
left,
|
|
57
|
+
top: null,
|
|
58
|
+
bottom: input.viewport.height - input.anchor.top + anchorGap,
|
|
59
|
+
maxHeight: Math.min(menuMaxHeight, availableAbove),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MardoraSlashCommand, MardoraSlashQuery } from "./types";
|
|
2
|
+
|
|
3
|
+
const slashLinePattern = /^\/([^\s]*)$/;
|
|
4
|
+
|
|
5
|
+
export function detectSlashQuery(documentText: string, cursorPosition: number): MardoraSlashQuery | null {
|
|
6
|
+
const safeCursor = Math.max(0, Math.min(cursorPosition, documentText.length));
|
|
7
|
+
const lineStart = documentText.lastIndexOf("\n", safeCursor - 1) + 1;
|
|
8
|
+
const lineTextBeforeCursor = documentText.slice(lineStart, safeCursor);
|
|
9
|
+
const match = lineTextBeforeCursor.match(slashLinePattern);
|
|
10
|
+
|
|
11
|
+
if (!match) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
from: lineStart,
|
|
17
|
+
to: safeCursor,
|
|
18
|
+
query: match[1] ?? "",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function filterSlashCommands(commands: readonly MardoraSlashCommand[], query: string): MardoraSlashCommand[] {
|
|
23
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
24
|
+
|
|
25
|
+
if (!normalizedQuery) {
|
|
26
|
+
return [...commands];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return commands.filter((command) => {
|
|
30
|
+
const searchable = [command.title, command.id, command.hint, ...command.aliases].join(" ").toLocaleLowerCase();
|
|
31
|
+
return searchable.includes(normalizedQuery);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
|
|
3
|
+
export const slashMenuTheme = EditorView.baseTheme({
|
|
4
|
+
".cm-mardora-slash-menu": {
|
|
5
|
+
position: "fixed",
|
|
6
|
+
display: "flex",
|
|
7
|
+
flexDirection: "column",
|
|
8
|
+
zIndex: "1000",
|
|
9
|
+
width: "328px",
|
|
10
|
+
maxHeight: "420px",
|
|
11
|
+
overflow: "hidden",
|
|
12
|
+
border: "1px solid rgba(120, 113, 108, 0.22)",
|
|
13
|
+
borderRadius: "12px",
|
|
14
|
+
background: "var(--mardora-slash-bg, #ffffff)",
|
|
15
|
+
boxShadow: "0 18px 48px rgba(15, 23, 42, 0.16)",
|
|
16
|
+
padding: "8px 0 0",
|
|
17
|
+
caretColor: "transparent",
|
|
18
|
+
cursor: "default",
|
|
19
|
+
fontFamily: "var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif)",
|
|
20
|
+
userSelect: "none",
|
|
21
|
+
},
|
|
22
|
+
".cm-mardora-slash-open .cm-cursor": {
|
|
23
|
+
visibility: "hidden",
|
|
24
|
+
},
|
|
25
|
+
".cm-mardora-slash-open .cm-content": {
|
|
26
|
+
caretColor: "transparent",
|
|
27
|
+
},
|
|
28
|
+
".cm-mardora-slash-list": {
|
|
29
|
+
flex: "1 1 auto",
|
|
30
|
+
minHeight: "0",
|
|
31
|
+
overflowY: "auto",
|
|
32
|
+
},
|
|
33
|
+
".cm-mardora-slash-group": {
|
|
34
|
+
padding: "8px 14px 6px",
|
|
35
|
+
color: "var(--mardora-slash-muted, #a8a29e)",
|
|
36
|
+
fontSize: "12px",
|
|
37
|
+
fontWeight: "500",
|
|
38
|
+
},
|
|
39
|
+
".cm-mardora-slash-item": {
|
|
40
|
+
display: "grid",
|
|
41
|
+
gridTemplateColumns: "34px 1fr auto",
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
width: "100%",
|
|
44
|
+
minHeight: "34px",
|
|
45
|
+
border: "0",
|
|
46
|
+
padding: "0 14px",
|
|
47
|
+
background: "transparent",
|
|
48
|
+
color: "var(--mardora-slash-fg, #27272a)",
|
|
49
|
+
textAlign: "left",
|
|
50
|
+
cursor: "default",
|
|
51
|
+
},
|
|
52
|
+
".cm-mardora-slash-item-active": {
|
|
53
|
+
background: "var(--mardora-slash-active, #f4f4f5)",
|
|
54
|
+
},
|
|
55
|
+
".cm-mardora-slash-icon": {
|
|
56
|
+
display: "inline-flex",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
color: "var(--mardora-slash-icon, #3f3f46)",
|
|
60
|
+
fontSize: "13px",
|
|
61
|
+
fontWeight: "400",
|
|
62
|
+
textAlign: "center",
|
|
63
|
+
},
|
|
64
|
+
".cm-mardora-slash-icon svg": {
|
|
65
|
+
display: "block",
|
|
66
|
+
width: "17px",
|
|
67
|
+
height: "17px",
|
|
68
|
+
strokeWidth: "1.9",
|
|
69
|
+
},
|
|
70
|
+
".cm-mardora-slash-title": {
|
|
71
|
+
overflow: "hidden",
|
|
72
|
+
color: "inherit",
|
|
73
|
+
fontSize: "14px",
|
|
74
|
+
fontWeight: "400",
|
|
75
|
+
textOverflow: "ellipsis",
|
|
76
|
+
whiteSpace: "nowrap",
|
|
77
|
+
},
|
|
78
|
+
".cm-mardora-slash-hint": {
|
|
79
|
+
marginLeft: "12px",
|
|
80
|
+
color: "var(--mardora-slash-muted, #a8a29e)",
|
|
81
|
+
fontSize: "12px",
|
|
82
|
+
fontWeight: "400",
|
|
83
|
+
},
|
|
84
|
+
".cm-mardora-slash-footer": {
|
|
85
|
+
display: "flex",
|
|
86
|
+
flex: "0 0 auto",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "space-between",
|
|
89
|
+
minHeight: "40px",
|
|
90
|
+
borderTop: "1px solid rgba(120, 113, 108, 0.18)",
|
|
91
|
+
background: "var(--mardora-slash-bg, #ffffff)",
|
|
92
|
+
padding: "0 14px",
|
|
93
|
+
color: "var(--mardora-slash-fg, #27272a)",
|
|
94
|
+
fontSize: "14px",
|
|
95
|
+
},
|
|
96
|
+
".cm-mardora-slash-footer span:last-child": {
|
|
97
|
+
color: "var(--mardora-slash-muted, #a8a29e)",
|
|
98
|
+
fontSize: "12px",
|
|
99
|
+
fontWeight: "400",
|
|
100
|
+
},
|
|
101
|
+
".cm-mardora-slash-empty": {
|
|
102
|
+
padding: "14px",
|
|
103
|
+
color: "var(--mardora-slash-muted, #a8a29e)",
|
|
104
|
+
fontSize: "13px",
|
|
105
|
+
},
|
|
106
|
+
"&dark .cm-mardora-slash-menu": {
|
|
107
|
+
"--mardora-slash-bg": "#18181b",
|
|
108
|
+
"--mardora-slash-fg": "#f4f4f5",
|
|
109
|
+
"--mardora-slash-muted": "#a1a1aa",
|
|
110
|
+
"--mardora-slash-active": "#27272a",
|
|
111
|
+
"--mardora-slash-icon": "#e4e4e7",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EditorView } from "@codemirror/view";
|
|
2
|
+
import type { MardoraAttachmentKind } from "../attachments";
|
|
3
|
+
import type { MardoraLocale } from "../i18n";
|
|
4
|
+
|
|
5
|
+
export type MardoraSlashCommandGroup = "basic" | "media";
|
|
6
|
+
|
|
7
|
+
export type MardoraSlashMessages = {
|
|
8
|
+
groups: Record<MardoraSlashCommandGroup, string>;
|
|
9
|
+
empty: string;
|
|
10
|
+
close: string;
|
|
11
|
+
closeHint: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MardoraSlashCommandContext = {
|
|
15
|
+
view: EditorView;
|
|
16
|
+
queryRange: { from: number; to: number };
|
|
17
|
+
requestAttachment?: (kind: MardoraAttachmentKind, context: MardoraSlashCommandContext) => boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MardoraSlashCommand = {
|
|
21
|
+
id: string;
|
|
22
|
+
group: MardoraSlashCommandGroup;
|
|
23
|
+
title: string;
|
|
24
|
+
aliases: string[];
|
|
25
|
+
icon: string;
|
|
26
|
+
hint: string;
|
|
27
|
+
run: (context: MardoraSlashCommandContext) => boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type MardoraSlashQuery = {
|
|
31
|
+
from: number;
|
|
32
|
+
to: number;
|
|
33
|
+
query: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type MardoraSlashCommandsConfig = {
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
locale?: MardoraLocale;
|
|
39
|
+
commands?: MardoraSlashCommand[];
|
|
40
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { EditorSelection, Extension, Prec } from "@codemirror/state";
|
|
2
|
+
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
3
|
+
import { extractTocItemsFromState } from "./extract";
|
|
4
|
+
import { createTocPanelElement } from "./panel";
|
|
5
|
+
import { clampTocWidth, resolveTocConfig } from "./slug";
|
|
6
|
+
import { readTocPanelState, writeTocPanelState } from "./storage";
|
|
7
|
+
import { tocTheme } from "./theme";
|
|
8
|
+
import type { MardoraTocConfig, MardoraTocItem, ResolvedMardoraTocConfig, TocPanelState } from "./types";
|
|
9
|
+
|
|
10
|
+
function sameItems(a: MardoraTocItem[], b: MardoraTocItem[]): boolean {
|
|
11
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class TocViewPlugin {
|
|
15
|
+
private panel: HTMLElement | null = null;
|
|
16
|
+
private config: ResolvedMardoraTocConfig;
|
|
17
|
+
private panelState: TocPanelState;
|
|
18
|
+
private items: MardoraTocItem[] = [];
|
|
19
|
+
private renderFrame: number | null = null;
|
|
20
|
+
private readonly measureKey = {};
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly view: EditorView,
|
|
24
|
+
rawConfig: MardoraTocConfig
|
|
25
|
+
) {
|
|
26
|
+
this.config = resolveTocConfig(rawConfig);
|
|
27
|
+
const stored = readTocPanelState(this.config.storageKey);
|
|
28
|
+
this.panelState = {
|
|
29
|
+
expanded: stored?.expanded ?? this.config.defaultExpanded,
|
|
30
|
+
width: clampTocWidth(stored?.width ?? this.config.defaultWidth, this.config),
|
|
31
|
+
};
|
|
32
|
+
this.view.scrollDOM.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
33
|
+
this.recompute();
|
|
34
|
+
this.scheduleRender();
|
|
35
|
+
this.scheduleActiveMeasure();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
update(update: ViewUpdate): void {
|
|
39
|
+
if (update.docChanged) {
|
|
40
|
+
this.recompute();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (update.viewportChanged || update.geometryChanged) this.scheduleActiveMeasure();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
destroy(): void {
|
|
47
|
+
this.view.scrollDOM.removeEventListener("scroll", this.handleScroll);
|
|
48
|
+
if (this.renderFrame !== null) {
|
|
49
|
+
this.view.dom.ownerDocument.defaultView?.cancelAnimationFrame(this.renderFrame);
|
|
50
|
+
this.renderFrame = null;
|
|
51
|
+
}
|
|
52
|
+
this.clearLayoutWidth();
|
|
53
|
+
this.panel?.remove();
|
|
54
|
+
this.panel = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private readonly handleScroll = (): void => {
|
|
58
|
+
this.scheduleActiveMeasure();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
private recompute(): void {
|
|
62
|
+
const next = extractTocItemsFromState(this.view.state, this.config);
|
|
63
|
+
this.items = this.withPreservedActive(next);
|
|
64
|
+
this.config.onTocChange?.(this.items);
|
|
65
|
+
this.render();
|
|
66
|
+
this.scheduleActiveMeasure();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private withPreservedActive(items: MardoraTocItem[]): MardoraTocItem[] {
|
|
70
|
+
if (items.length === 0) return [];
|
|
71
|
+
const previousActive = this.items.find((item) => item.active)?.id;
|
|
72
|
+
const fallbackActiveId = items[0]?.id;
|
|
73
|
+
const activeId =
|
|
74
|
+
previousActive && items.some((item) => item.id === previousActive) ? previousActive : fallbackActiveId;
|
|
75
|
+
return items.map((item) => ({ ...item, active: item.id === activeId }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private scheduleActiveMeasure(): void {
|
|
79
|
+
if (this.items.length === 0 || !this.view.dom.isConnected) return;
|
|
80
|
+
this.view.requestMeasure({
|
|
81
|
+
key: this.measureKey,
|
|
82
|
+
read: (view) => {
|
|
83
|
+
const viewportTop = view.scrollDOM.getBoundingClientRect().top + 24;
|
|
84
|
+
let activeId = this.items[0]?.id ?? null;
|
|
85
|
+
for (const item of this.items) {
|
|
86
|
+
if (typeof item.from !== "number") continue;
|
|
87
|
+
const coords = view.coordsAtPos(item.from);
|
|
88
|
+
if (coords && coords.top <= viewportTop) activeId = item.id;
|
|
89
|
+
}
|
|
90
|
+
return activeId;
|
|
91
|
+
},
|
|
92
|
+
write: (activeId) => {
|
|
93
|
+
if (!activeId) return;
|
|
94
|
+
this.updateActiveItem(activeId);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private updateActiveItem(activeId: string): void {
|
|
100
|
+
const next = this.items.map((item) => ({ ...item, active: item.id === activeId }));
|
|
101
|
+
if (sameItems(next, this.items)) return;
|
|
102
|
+
this.items = next;
|
|
103
|
+
this.config.onTocChange?.(this.items);
|
|
104
|
+
this.render();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private scheduleRender(): void {
|
|
108
|
+
const win = this.view.dom.ownerDocument.defaultView;
|
|
109
|
+
if (!win) {
|
|
110
|
+
this.render();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (this.renderFrame !== null) return;
|
|
114
|
+
this.renderFrame = win.requestAnimationFrame(() => {
|
|
115
|
+
this.renderFrame = null;
|
|
116
|
+
this.render();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private render(): void {
|
|
121
|
+
if (!this.config.enabled) {
|
|
122
|
+
this.clearLayoutWidth();
|
|
123
|
+
this.panel?.remove();
|
|
124
|
+
this.panel = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.syncLayoutWidth();
|
|
128
|
+
const nextPanel = createTocPanelElement(
|
|
129
|
+
{ expanded: this.panelState.expanded, width: this.panelState.width, items: this.items },
|
|
130
|
+
{
|
|
131
|
+
onSelect: (item) => this.selectItem(item),
|
|
132
|
+
onToggle: () => this.toggle(),
|
|
133
|
+
onResizeStart: (event) => this.startResize(event),
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
if (this.panel?.isConnected) {
|
|
137
|
+
this.panel.replaceWith(nextPanel);
|
|
138
|
+
} else {
|
|
139
|
+
this.view.dom.appendChild(nextPanel);
|
|
140
|
+
}
|
|
141
|
+
this.panel = nextPanel;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private selectItem(item: MardoraTocItem): void {
|
|
145
|
+
if (typeof item.from !== "number") return;
|
|
146
|
+
this.view.dispatch({
|
|
147
|
+
selection: EditorSelection.cursor(item.from),
|
|
148
|
+
effects: EditorView.scrollIntoView(item.from, { y: "start" }),
|
|
149
|
+
});
|
|
150
|
+
this.view.focus();
|
|
151
|
+
this.updateActiveItem(item.id);
|
|
152
|
+
this.scheduleActiveMeasure();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private persistPanelState(): void {
|
|
156
|
+
writeTocPanelState(this.config.storageKey, this.panelState);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private toggle(): void {
|
|
160
|
+
this.panelState = { ...this.panelState, expanded: !this.panelState.expanded };
|
|
161
|
+
this.persistPanelState();
|
|
162
|
+
this.render();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private syncLayoutWidth(): void {
|
|
166
|
+
const layoutWidth = this.panelState.expanded ? this.panelState.width : 42;
|
|
167
|
+
this.view.dom.style.setProperty("--mardora-toc-layout-width", `${layoutWidth}px`);
|
|
168
|
+
this.view.dom.style.setProperty("--mardora-toc-scrollbar-gutter", "14px");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private clearLayoutWidth(): void {
|
|
172
|
+
this.view.dom.style.removeProperty("--mardora-toc-layout-width");
|
|
173
|
+
this.view.dom.style.removeProperty("--mardora-toc-scrollbar-gutter");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private startResize(event: MouseEvent): void {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
const startX = event.clientX;
|
|
179
|
+
const startWidth = this.panelState.width;
|
|
180
|
+
const doc = this.view.dom.ownerDocument;
|
|
181
|
+
doc.body.classList.add("cm-mardora-toc-resizing");
|
|
182
|
+
|
|
183
|
+
const move = (moveEvent: MouseEvent) => {
|
|
184
|
+
const nextWidth = clampTocWidth(startWidth - (moveEvent.clientX - startX), this.config);
|
|
185
|
+
this.panelState = { ...this.panelState, width: nextWidth };
|
|
186
|
+
this.render();
|
|
187
|
+
};
|
|
188
|
+
const up = () => {
|
|
189
|
+
doc.body.classList.remove("cm-mardora-toc-resizing");
|
|
190
|
+
doc.removeEventListener("mousemove", move);
|
|
191
|
+
doc.removeEventListener("mouseup", up);
|
|
192
|
+
this.persistPanelState();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
doc.addEventListener("mousemove", move);
|
|
196
|
+
doc.addEventListener("mouseup", up);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function tableOfContents(config: MardoraTocConfig = {}): Extension[] {
|
|
201
|
+
return [tocTheme, Prec.low(ViewPlugin.define((view) => new TocViewPlugin(view, config)))];
|
|
202
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { EditorState } from "@codemirror/state";
|
|
2
|
+
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
|
|
3
|
+
import type { SyntaxNodeRef } from "@lezer/common";
|
|
4
|
+
import type { MardoraTocConfig, MardoraTocItem, MardoraTocLevel } from "./types";
|
|
5
|
+
import { createTocSlugger, resolveTocConfig } from "./slug";
|
|
6
|
+
|
|
7
|
+
const headingPattern = /^ATXHeading([1-6])$/;
|
|
8
|
+
const tocParseTimeout = 100;
|
|
9
|
+
|
|
10
|
+
function headingLevel(node: SyntaxNodeRef): MardoraTocLevel | null {
|
|
11
|
+
const match = headingPattern.exec(node.name);
|
|
12
|
+
if (!match) return null;
|
|
13
|
+
const level = Number(match[1]);
|
|
14
|
+
return level >= 2 && level <= 6 ? (level as MardoraTocLevel) : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function stripMarkdownHeadingText(text: string): string {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/^#{1,6}\s*/, "")
|
|
20
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
21
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
22
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
23
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
24
|
+
.replace(/_([^_]+)_/g, "$1")
|
|
25
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function extractTocItemsFromState(state: EditorState, config: MardoraTocConfig = {}): MardoraTocItem[] {
|
|
30
|
+
const resolved = resolveTocConfig(config);
|
|
31
|
+
const slug = createTocSlugger();
|
|
32
|
+
const items: MardoraTocItem[] = [];
|
|
33
|
+
const tree = ensureSyntaxTree(state, state.doc.length, tocParseTimeout) ?? syntaxTree(state);
|
|
34
|
+
|
|
35
|
+
tree.iterate({
|
|
36
|
+
enter: (node) => {
|
|
37
|
+
const level = headingLevel(node);
|
|
38
|
+
if (!level || level < resolved.minLevel || level > resolved.maxLevel) return;
|
|
39
|
+
const text = stripMarkdownHeadingText(state.sliceDoc(node.from, node.to));
|
|
40
|
+
if (!text) return;
|
|
41
|
+
items.push({
|
|
42
|
+
id: slug(text),
|
|
43
|
+
level,
|
|
44
|
+
text,
|
|
45
|
+
from: node.from,
|
|
46
|
+
to: node.to,
|
|
47
|
+
active: false,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return items;
|
|
53
|
+
}
|