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,146 @@
|
|
|
1
|
+
import { createTheme } from "./utils";
|
|
2
|
+
|
|
3
|
+
export const mediaLightboxTheme = createTheme({
|
|
4
|
+
default: {
|
|
5
|
+
".cm-mardora-media-preview": {
|
|
6
|
+
position: "relative",
|
|
7
|
+
},
|
|
8
|
+
".cm-mardora-media-preview-button": {
|
|
9
|
+
position: "absolute",
|
|
10
|
+
top: "0.5rem",
|
|
11
|
+
right: "0.5rem",
|
|
12
|
+
width: "1.75rem",
|
|
13
|
+
height: "1.75rem",
|
|
14
|
+
border: "1px solid rgba(148, 163, 184, 0.45)",
|
|
15
|
+
borderRadius: "0.375rem",
|
|
16
|
+
backgroundColor: "rgba(255, 255, 255, 0.86)",
|
|
17
|
+
color: "#334155",
|
|
18
|
+
display: "inline-flex",
|
|
19
|
+
alignItems: "center",
|
|
20
|
+
justifyContent: "center",
|
|
21
|
+
padding: "0",
|
|
22
|
+
opacity: "0",
|
|
23
|
+
pointerEvents: "auto",
|
|
24
|
+
cursor: "pointer",
|
|
25
|
+
transition: "opacity 120ms ease, background-color 120ms ease, border-color 120ms ease",
|
|
26
|
+
zIndex: "2",
|
|
27
|
+
},
|
|
28
|
+
".cm-mardora-media-preview:hover .cm-mardora-media-preview-button, .cm-mardora-media-preview-button:focus-visible": {
|
|
29
|
+
opacity: "1",
|
|
30
|
+
},
|
|
31
|
+
".cm-mardora-media-preview-button:hover, .cm-mardora-media-preview-button:focus-visible": {
|
|
32
|
+
borderColor: "rgba(37, 99, 235, 0.55)",
|
|
33
|
+
backgroundColor: "rgba(255, 255, 255, 0.96)",
|
|
34
|
+
color: "#2563eb",
|
|
35
|
+
outline: "none",
|
|
36
|
+
},
|
|
37
|
+
".cm-mardora-media-preview-button svg": {
|
|
38
|
+
width: "1rem",
|
|
39
|
+
height: "1rem",
|
|
40
|
+
},
|
|
41
|
+
".cm-mardora-media-lightbox": {
|
|
42
|
+
position: "fixed",
|
|
43
|
+
inset: "0",
|
|
44
|
+
zIndex: "10000",
|
|
45
|
+
display: "flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "center",
|
|
48
|
+
padding: "2rem",
|
|
49
|
+
backgroundColor: "rgba(15, 23, 42, 0.24)",
|
|
50
|
+
},
|
|
51
|
+
".cm-mardora-media-lightbox-panel": {
|
|
52
|
+
position: "relative",
|
|
53
|
+
width: "min(96vw, 1200px)",
|
|
54
|
+
maxWidth: "min(96vw, 1200px)",
|
|
55
|
+
maxHeight: "92vh",
|
|
56
|
+
borderRadius: "0.75rem",
|
|
57
|
+
backgroundColor: "#ffffff",
|
|
58
|
+
color: "#0f172a",
|
|
59
|
+
boxShadow: "0 24px 70px rgba(15, 23, 42, 0.28)",
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
},
|
|
62
|
+
".cm-mardora-media-lightbox-body": {
|
|
63
|
+
maxWidth: "100%",
|
|
64
|
+
maxHeight: "92vh",
|
|
65
|
+
overflow: "auto",
|
|
66
|
+
display: "flex",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
padding: "1rem",
|
|
70
|
+
},
|
|
71
|
+
".cm-mardora-media-lightbox-image": {
|
|
72
|
+
display: "block",
|
|
73
|
+
maxWidth: "100%",
|
|
74
|
+
maxHeight: "calc(92vh - 2rem)",
|
|
75
|
+
width: "auto",
|
|
76
|
+
height: "auto",
|
|
77
|
+
objectFit: "contain",
|
|
78
|
+
},
|
|
79
|
+
".cm-mardora-media-lightbox-html": {
|
|
80
|
+
width: "100%",
|
|
81
|
+
maxWidth: "100%",
|
|
82
|
+
maxHeight: "calc(92vh - 2rem)",
|
|
83
|
+
overflow: "auto",
|
|
84
|
+
display: "flex",
|
|
85
|
+
justifyContent: "center",
|
|
86
|
+
},
|
|
87
|
+
".cm-mardora-media-lightbox-html svg": {
|
|
88
|
+
display: "block",
|
|
89
|
+
maxWidth: "100%",
|
|
90
|
+
maxHeight: "calc(92vh - 2rem)",
|
|
91
|
+
width: "100%",
|
|
92
|
+
height: "auto",
|
|
93
|
+
},
|
|
94
|
+
".cm-mardora-media-lightbox-close": {
|
|
95
|
+
position: "absolute",
|
|
96
|
+
top: "0.5rem",
|
|
97
|
+
right: "0.5rem",
|
|
98
|
+
width: "2rem",
|
|
99
|
+
height: "2rem",
|
|
100
|
+
border: "1px solid rgba(148, 163, 184, 0.45)",
|
|
101
|
+
borderRadius: "0.375rem",
|
|
102
|
+
backgroundColor: "rgba(255, 255, 255, 0.92)",
|
|
103
|
+
color: "#334155",
|
|
104
|
+
display: "inline-flex",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
justifyContent: "center",
|
|
107
|
+
padding: "0",
|
|
108
|
+
cursor: "pointer",
|
|
109
|
+
},
|
|
110
|
+
".cm-mardora-media-lightbox-close:hover, .cm-mardora-media-lightbox-close:focus-visible": {
|
|
111
|
+
borderColor: "rgba(37, 99, 235, 0.55)",
|
|
112
|
+
color: "#2563eb",
|
|
113
|
+
outline: "none",
|
|
114
|
+
},
|
|
115
|
+
".cm-mardora-media-lightbox-close svg": {
|
|
116
|
+
width: "1rem",
|
|
117
|
+
height: "1rem",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
dark: {
|
|
121
|
+
".cm-mardora-media-preview-button": {
|
|
122
|
+
borderColor: "rgba(71, 85, 105, 0.72)",
|
|
123
|
+
backgroundColor: "rgba(15, 23, 42, 0.84)",
|
|
124
|
+
color: "#cbd5e1",
|
|
125
|
+
},
|
|
126
|
+
".cm-mardora-media-preview-button:hover, .cm-mardora-media-preview-button:focus-visible": {
|
|
127
|
+
borderColor: "rgba(96, 165, 250, 0.68)",
|
|
128
|
+
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
|
129
|
+
color: "#93c5fd",
|
|
130
|
+
},
|
|
131
|
+
".cm-mardora-media-lightbox-panel": {
|
|
132
|
+
backgroundColor: "#0f172a",
|
|
133
|
+
color: "#e2e8f0",
|
|
134
|
+
boxShadow: "0 24px 70px rgba(0, 0, 0, 0.45)",
|
|
135
|
+
},
|
|
136
|
+
".cm-mardora-media-lightbox-close": {
|
|
137
|
+
borderColor: "rgba(71, 85, 105, 0.72)",
|
|
138
|
+
backgroundColor: "rgba(15, 23, 42, 0.92)",
|
|
139
|
+
color: "#cbd5e1",
|
|
140
|
+
},
|
|
141
|
+
".cm-mardora-media-lightbox-close:hover, .cm-mardora-media-lightbox-close:focus-visible": {
|
|
142
|
+
borderColor: "rgba(96, 165, 250, 0.68)",
|
|
143
|
+
color: "#93c5fd",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createMardoraIcon } from "./icons";
|
|
2
|
+
|
|
3
|
+
export type MediaLightboxContent =
|
|
4
|
+
| {
|
|
5
|
+
readonly kind: "image";
|
|
6
|
+
readonly src: string;
|
|
7
|
+
readonly alt: string;
|
|
8
|
+
readonly title?: string;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
readonly kind: "html";
|
|
12
|
+
readonly html: string;
|
|
13
|
+
readonly title?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface OpenMediaLightboxOptions {
|
|
17
|
+
readonly content: MediaLightboxContent;
|
|
18
|
+
readonly returnFocus?: HTMLElement | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MediaLightboxHandle {
|
|
22
|
+
readonly element: HTMLElement;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MediaPreviewButtonOptions {
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly content: () => MediaLightboxContent | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function consumeMediaLightboxTrigger(event: Pick<Event, "preventDefault" | "stopPropagation">): void {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createMediaPreviewButton(
|
|
37
|
+
ownerDocument: Document,
|
|
38
|
+
options: MediaPreviewButtonOptions
|
|
39
|
+
): HTMLButtonElement {
|
|
40
|
+
const button = ownerDocument.createElement("button");
|
|
41
|
+
button.type = "button";
|
|
42
|
+
button.className = "cm-mardora-media-preview-button";
|
|
43
|
+
button.setAttribute("aria-label", options.label);
|
|
44
|
+
button.title = options.label;
|
|
45
|
+
const icon = createMardoraIcon("maximize-2");
|
|
46
|
+
if (icon) button.appendChild(icon);
|
|
47
|
+
button.addEventListener("click", (event) => {
|
|
48
|
+
consumeMediaLightboxTrigger(event);
|
|
49
|
+
const content = options.content();
|
|
50
|
+
if (!content) return;
|
|
51
|
+
openMediaLightbox(ownerDocument, { content, returnFocus: button });
|
|
52
|
+
});
|
|
53
|
+
return button;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function openMediaLightbox(ownerDocument: Document, options: OpenMediaLightboxOptions): MediaLightboxHandle {
|
|
57
|
+
ownerDocument.querySelector(".cm-mardora-media-lightbox")?.remove();
|
|
58
|
+
const mountPoint = options.returnFocus?.closest(".cm-editor") ?? ownerDocument.body;
|
|
59
|
+
|
|
60
|
+
const root = ownerDocument.createElement("div");
|
|
61
|
+
root.className = "cm-mardora-media-lightbox";
|
|
62
|
+
root.setAttribute("role", "dialog");
|
|
63
|
+
root.setAttribute("aria-modal", "true");
|
|
64
|
+
if (options.content.title) {
|
|
65
|
+
root.setAttribute("aria-label", options.content.title);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const panel = ownerDocument.createElement("div");
|
|
69
|
+
panel.className = "cm-mardora-media-lightbox-panel";
|
|
70
|
+
panel.addEventListener("click", (event) => event.stopPropagation());
|
|
71
|
+
|
|
72
|
+
const closeButton = ownerDocument.createElement("button");
|
|
73
|
+
closeButton.type = "button";
|
|
74
|
+
closeButton.className = "cm-mardora-media-lightbox-close";
|
|
75
|
+
closeButton.setAttribute("aria-label", "关闭大图查看");
|
|
76
|
+
closeButton.title = "关闭大图查看";
|
|
77
|
+
const closeIcon = createMardoraIcon("x");
|
|
78
|
+
if (closeIcon) closeButton.appendChild(closeIcon);
|
|
79
|
+
|
|
80
|
+
const body = ownerDocument.createElement("div");
|
|
81
|
+
body.className = "cm-mardora-media-lightbox-body";
|
|
82
|
+
body.appendChild(createLightboxContent(ownerDocument, options.content));
|
|
83
|
+
|
|
84
|
+
panel.appendChild(closeButton);
|
|
85
|
+
panel.appendChild(body);
|
|
86
|
+
root.appendChild(panel);
|
|
87
|
+
|
|
88
|
+
const close = (): void => {
|
|
89
|
+
root.remove();
|
|
90
|
+
ownerDocument.removeEventListener("keydown", handleKeydown);
|
|
91
|
+
options.returnFocus?.focus();
|
|
92
|
+
};
|
|
93
|
+
const handleKeydown = (event: KeyboardEvent): void => {
|
|
94
|
+
if (event.key === "Escape") {
|
|
95
|
+
close();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
root.addEventListener("click", close);
|
|
100
|
+
closeButton.addEventListener("click", (event) => {
|
|
101
|
+
consumeMediaLightboxTrigger(event);
|
|
102
|
+
close();
|
|
103
|
+
});
|
|
104
|
+
ownerDocument.addEventListener("keydown", handleKeydown);
|
|
105
|
+
mountPoint.appendChild(root);
|
|
106
|
+
closeButton.focus();
|
|
107
|
+
|
|
108
|
+
return { element: root, close };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createLightboxContent(ownerDocument: Document, content: MediaLightboxContent): HTMLElement {
|
|
112
|
+
if (content.kind === "image") {
|
|
113
|
+
const image = ownerDocument.createElement("img");
|
|
114
|
+
image.className = "cm-mardora-media-lightbox-image";
|
|
115
|
+
image.src = content.src;
|
|
116
|
+
image.alt = content.alt;
|
|
117
|
+
if (content.title) image.title = content.title;
|
|
118
|
+
return image;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const container = ownerDocument.createElement("div");
|
|
122
|
+
container.className = "cm-mardora-media-lightbox-html";
|
|
123
|
+
container.innerHTML = content.html;
|
|
124
|
+
return container;
|
|
125
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Decoration, EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
|
2
|
+
import { Extension, Range } from "@codemirror/state";
|
|
3
|
+
import { MarkdownConfig } from "@lezer/markdown";
|
|
4
|
+
import { SyntaxNode } from "@lezer/common";
|
|
5
|
+
import { MardoraConfig } from "./mardora";
|
|
6
|
+
import { createTheme, ThemeEnum, ThemeStyle } from "./utils";
|
|
7
|
+
import { StyleModule } from "style-mod";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context passed to plugin lifecycle methods
|
|
11
|
+
*/
|
|
12
|
+
export interface PluginContext {
|
|
13
|
+
/** Current configuration */
|
|
14
|
+
readonly config: MardoraConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Plugin configuration schema
|
|
19
|
+
*/
|
|
20
|
+
export interface PluginConfig {
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decoration context passed to plugin decoration builders
|
|
26
|
+
* Provides access to view state and decoration collection
|
|
27
|
+
*/
|
|
28
|
+
export interface DecorationContext {
|
|
29
|
+
/** The EditorView instance (readonly) */
|
|
30
|
+
readonly view: EditorView;
|
|
31
|
+
|
|
32
|
+
/** Array to push decorations into (will be sorted automatically) */
|
|
33
|
+
readonly decorations: Range<Decoration>[];
|
|
34
|
+
|
|
35
|
+
/** Check if selection overlaps with a range (to show raw markdown) */
|
|
36
|
+
selectionOverlapsRange(from: number, to: number): boolean;
|
|
37
|
+
|
|
38
|
+
/** Check if cursor is within a range */
|
|
39
|
+
cursorInRange(from: number, to: number): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Abstract base class for all mardora plugins
|
|
44
|
+
*
|
|
45
|
+
* Implements OOP principles:
|
|
46
|
+
* - Abstraction: abstract name/version must be implemented by subclasses
|
|
47
|
+
* - Encapsulation: private _config, protected _context
|
|
48
|
+
* - Inheritance: specialized plugin classes can extend this
|
|
49
|
+
*/
|
|
50
|
+
export abstract class MardoraPlugin {
|
|
51
|
+
/** Unique plugin identifier (abstract - must be implemented) */
|
|
52
|
+
abstract readonly name: string;
|
|
53
|
+
|
|
54
|
+
/** Plugin version (abstract - must be implemented) */
|
|
55
|
+
abstract readonly version: string;
|
|
56
|
+
|
|
57
|
+
/** Decoration priority (higher = applied later) */
|
|
58
|
+
readonly decorationPriority: number = 100;
|
|
59
|
+
|
|
60
|
+
/** Plugin dependencies - names of required plugins */
|
|
61
|
+
readonly dependencies: string[] = [];
|
|
62
|
+
|
|
63
|
+
/** Node types this plugin handles for decorations and preview rendering */
|
|
64
|
+
readonly requiredNodes: readonly string[] = [];
|
|
65
|
+
|
|
66
|
+
/** Private configuration storage */
|
|
67
|
+
private _config: PluginConfig = {};
|
|
68
|
+
|
|
69
|
+
/** Protected context - accessible to subclasses */
|
|
70
|
+
protected _context: PluginContext | null = null;
|
|
71
|
+
|
|
72
|
+
/** Get plugin configuration */
|
|
73
|
+
get config(): PluginConfig {
|
|
74
|
+
return this._config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Set plugin configuration */
|
|
78
|
+
set config(value: PluginConfig) {
|
|
79
|
+
this._config = value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Get plugin context */
|
|
83
|
+
get context(): PluginContext | null {
|
|
84
|
+
return this._context;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Plugin theme */
|
|
88
|
+
get theme(): (theme: ThemeEnum) => ThemeStyle {
|
|
89
|
+
return createTheme({
|
|
90
|
+
default: {},
|
|
91
|
+
dark: {},
|
|
92
|
+
light: {},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================
|
|
97
|
+
// EXTENSION METHODS (overridable by subclasses)
|
|
98
|
+
// ============================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return CodeMirror extensions for this plugin
|
|
102
|
+
* Override to provide custom extensions
|
|
103
|
+
*/
|
|
104
|
+
getExtensions(): Extension[] {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return markdown parser extensions
|
|
110
|
+
* Override to extend markdown parsing
|
|
111
|
+
*/
|
|
112
|
+
getMarkdownConfig(): MarkdownConfig | null {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Return keybindings for this plugin
|
|
118
|
+
* Override to add custom keyboard shortcuts
|
|
119
|
+
*/
|
|
120
|
+
getKeymap(): KeyBinding[] {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// DECORATION METHODS (overridable by subclasses)
|
|
126
|
+
// ============================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build decorations for the current view state
|
|
130
|
+
* Override to contribute decorations to the editor
|
|
131
|
+
*
|
|
132
|
+
* @param ctx - Decoration context with view and decoration array
|
|
133
|
+
*/
|
|
134
|
+
buildDecorations(_ctx: DecorationContext): void {
|
|
135
|
+
void _ctx;
|
|
136
|
+
// Default implementation does nothing
|
|
137
|
+
// Subclasses override to add decorations
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================
|
|
141
|
+
// LIFECYCLE HOOKS (overridable by subclasses)
|
|
142
|
+
// ============================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Called when plugin is registered with mardora
|
|
146
|
+
* Override to perform initialization
|
|
147
|
+
*
|
|
148
|
+
* @param context - Plugin context with configuration
|
|
149
|
+
*/
|
|
150
|
+
onRegister(context: PluginContext): void | Promise<void> {
|
|
151
|
+
this._context = context;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Called when plugin is unregistered
|
|
156
|
+
* Override to perform cleanup
|
|
157
|
+
*/
|
|
158
|
+
onUnregister(): void {
|
|
159
|
+
this._context = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Called when EditorView is created and ready
|
|
164
|
+
* Override to perform view-specific initialization
|
|
165
|
+
*
|
|
166
|
+
* @param view - The EditorView instance
|
|
167
|
+
*/
|
|
168
|
+
onViewReady(_view: EditorView): void {
|
|
169
|
+
void _view;
|
|
170
|
+
// Default implementation does nothing
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Called on view updates (document changes, selection changes, etc.)
|
|
175
|
+
* Override to react to editor changes
|
|
176
|
+
*
|
|
177
|
+
* @param update - The ViewUpdate with change information
|
|
178
|
+
*/
|
|
179
|
+
onViewUpdate(_update: ViewUpdate): void {
|
|
180
|
+
void _update;
|
|
181
|
+
// Default implementation does nothing
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================
|
|
185
|
+
// PROTECTED UTILITIES (for subclasses)
|
|
186
|
+
// ============================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Helper to get current editor state
|
|
190
|
+
* @param view - The EditorView instance
|
|
191
|
+
*/
|
|
192
|
+
protected getState(view: EditorView) {
|
|
193
|
+
return view.state;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Helper to get current document
|
|
198
|
+
* @param view - The EditorView instance
|
|
199
|
+
*/
|
|
200
|
+
protected getDocument(view: EditorView) {
|
|
201
|
+
return view.state.doc;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================
|
|
205
|
+
// PREVIEW RENDERING METHODS (for mardora/preview)
|
|
206
|
+
// ============================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render a syntax node to HTML for preview mode
|
|
210
|
+
* Override to provide custom HTML rendering for specific node types
|
|
211
|
+
*
|
|
212
|
+
* @param node - The syntax node to render
|
|
213
|
+
* @param children - Pre-rendered children HTML
|
|
214
|
+
* @param ctx - Preview context with document and utilities
|
|
215
|
+
* @returns HTML string to use, or null to use default rendering
|
|
216
|
+
*/
|
|
217
|
+
renderToHTML?(
|
|
218
|
+
node: SyntaxNode,
|
|
219
|
+
children: string,
|
|
220
|
+
ctx: {
|
|
221
|
+
doc: string;
|
|
222
|
+
sliceDoc(from: number, to: number): string;
|
|
223
|
+
sanitize(html: string): string;
|
|
224
|
+
syntaxHighlighters?: readonly import("@lezer/highlight").Highlighter[];
|
|
225
|
+
}
|
|
226
|
+
): string | null | Promise<string | null>;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Return the document position consumed by preview rendering.
|
|
230
|
+
* Plugins can use this when a rendered node owns trailing syntax that is
|
|
231
|
+
* outside the Lezer node range, such as image attributes.
|
|
232
|
+
*/
|
|
233
|
+
getPreviewConsumedTo?(
|
|
234
|
+
node: SyntaxNode,
|
|
235
|
+
ctx: {
|
|
236
|
+
doc: string;
|
|
237
|
+
sliceDoc(from: number, to: number): string;
|
|
238
|
+
}
|
|
239
|
+
): number | null;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get CSS styles for preview mode
|
|
243
|
+
* Override to provide custom CSS for preview rendering
|
|
244
|
+
*
|
|
245
|
+
* @param theme - Current theme enum
|
|
246
|
+
* @returns CSS string for preview styles
|
|
247
|
+
*/
|
|
248
|
+
getPreviewStyles(theme: ThemeEnum, wrapperClass: string): string {
|
|
249
|
+
const themeStyles = this.theme(theme);
|
|
250
|
+
return this.transformToCss(themeStyles, wrapperClass);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Transform ThemeStyle object to CSS string for preview
|
|
255
|
+
* Uses cssClassMap to convert CM selectors to semantic selectors
|
|
256
|
+
*/
|
|
257
|
+
protected transformToCss(themeStyles: ThemeStyle, wrapperClass: string): string {
|
|
258
|
+
const styleMod = new StyleModule(themeStyles, {
|
|
259
|
+
finish: (sel) => {
|
|
260
|
+
return `.${wrapperClass} ${sel}`;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
return styleMod.getRules();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Base class for plugins that primarily contribute decorations
|
|
269
|
+
* Extends MardoraPlugin with decoration-focused defaults
|
|
270
|
+
*/
|
|
271
|
+
export abstract class DecorationPlugin extends MardoraPlugin {
|
|
272
|
+
/**
|
|
273
|
+
* Decoration priority - lower than default for decoration plugins
|
|
274
|
+
* Override to customize
|
|
275
|
+
*/
|
|
276
|
+
override decorationPriority = 50;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Subclasses must implement this to provide decorations
|
|
280
|
+
* @param ctx - Decoration context
|
|
281
|
+
*/
|
|
282
|
+
abstract override buildDecorations(ctx: DecorationContext): void;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Base class for plugins that add syntax/parser extensions
|
|
287
|
+
* Extends MardoraPlugin with syntax-focused requirements
|
|
288
|
+
*/
|
|
289
|
+
export abstract class SyntaxPlugin extends MardoraPlugin {
|
|
290
|
+
/**
|
|
291
|
+
* Subclasses must implement this to provide markdown config
|
|
292
|
+
*/
|
|
293
|
+
abstract override getMarkdownConfig(): MarkdownConfig;
|
|
294
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type NativeSelectionActivationInput = {
|
|
2
|
+
editorSelectionEmpty: boolean;
|
|
3
|
+
nativeSelectionCollapsed: boolean;
|
|
4
|
+
anchorInEditor: boolean;
|
|
5
|
+
focusInEditor: boolean;
|
|
6
|
+
rangeCount: number;
|
|
7
|
+
anchorExcluded?: boolean;
|
|
8
|
+
focusExcluded?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SelectionToolbarSyntaxNodeInput = {
|
|
12
|
+
selectionFrom: number;
|
|
13
|
+
selectionTo: number;
|
|
14
|
+
nodeFrom: number;
|
|
15
|
+
nodeTo: number;
|
|
16
|
+
nodeName: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ClassListLike = {
|
|
20
|
+
length: number;
|
|
21
|
+
item(index: number): string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ElementLike = {
|
|
25
|
+
className?: unknown;
|
|
26
|
+
classList?: unknown;
|
|
27
|
+
parentElement?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const excludedClassPrefixes = [
|
|
31
|
+
"cm-mardora-code-block",
|
|
32
|
+
"cm-mardora-code-caption",
|
|
33
|
+
"cm-mardora-code-copy",
|
|
34
|
+
"cm-mardora-code-diff",
|
|
35
|
+
"cm-mardora-code-fence",
|
|
36
|
+
"cm-mardora-code-header",
|
|
37
|
+
"cm-mardora-code-line",
|
|
38
|
+
"cm-mardora-image-",
|
|
39
|
+
"cm-mardora-math-",
|
|
40
|
+
"cm-mardora-mermaid-",
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
const excludedSyntaxNodeNames = new Set(["FencedCode", "MermaidBlock", "MathBlock", "InlineMath", "Image"]);
|
|
44
|
+
|
|
45
|
+
/** Returns whether a browser native selection should activate the toolbar. */
|
|
46
|
+
export function canActivateFromNativeSelection(input: NativeSelectionActivationInput): boolean {
|
|
47
|
+
return (
|
|
48
|
+
!input.nativeSelectionCollapsed &&
|
|
49
|
+
input.anchorInEditor &&
|
|
50
|
+
input.focusInEditor &&
|
|
51
|
+
input.rangeCount > 0 &&
|
|
52
|
+
!input.anchorExcluded &&
|
|
53
|
+
!input.focusExcluded
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasSelectionToolbarExcludedAncestor(target: unknown, root?: unknown): boolean {
|
|
58
|
+
let current = toElementLike(target);
|
|
59
|
+
while (current) {
|
|
60
|
+
if (hasExcludedClass(current)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (current === root) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
current = toElementLike(current.parentElement);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function selectionOverlapsExcludedSyntaxNode(input: SelectionToolbarSyntaxNodeInput): boolean {
|
|
72
|
+
return (
|
|
73
|
+
excludedSyntaxNodeNames.has(input.nodeName) &&
|
|
74
|
+
input.selectionFrom < input.nodeTo &&
|
|
75
|
+
input.selectionTo > input.nodeFrom
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toElementLike(value: unknown): ElementLike | null {
|
|
80
|
+
if (!value || typeof value !== "object") {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const element = value as ElementLike;
|
|
84
|
+
if (hasClassData(element)) {
|
|
85
|
+
return element;
|
|
86
|
+
}
|
|
87
|
+
return toElementLike(element.parentElement);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasClassData(value: ElementLike): boolean {
|
|
91
|
+
return value.className !== undefined || value.classList !== undefined || value.parentElement !== undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasExcludedClass(element: ElementLike): boolean {
|
|
95
|
+
for (const className of classNamesFrom(element)) {
|
|
96
|
+
if (excludedClassPrefixes.some((prefix) => className.startsWith(prefix))) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function classNamesFrom(element: ElementLike): string[] {
|
|
104
|
+
const classList = element.classList;
|
|
105
|
+
if (isClassListLike(classList)) {
|
|
106
|
+
const classNames: string[] = [];
|
|
107
|
+
for (let index = 0; index < classList.length; index += 1) {
|
|
108
|
+
const item = classList.item(index);
|
|
109
|
+
if (item) classNames.push(item);
|
|
110
|
+
}
|
|
111
|
+
return classNames;
|
|
112
|
+
}
|
|
113
|
+
return typeof element.className === "string" ? element.className.split(/\s+/).filter(Boolean) : [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isClassListLike(value: unknown): value is ClassListLike {
|
|
117
|
+
return (
|
|
118
|
+
!!value &&
|
|
119
|
+
typeof value === "object" &&
|
|
120
|
+
typeof (value as ClassListLike).length === "number" &&
|
|
121
|
+
typeof (value as ClassListLike).item === "function"
|
|
122
|
+
);
|
|
123
|
+
}
|