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,806 @@
|
|
|
1
|
+
import { Decoration, EditorView, KeyBinding, WidgetType } from "@codemirror/view";
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
import { DecorationContext, DecorationPlugin } from "../editor/plugin";
|
|
4
|
+
import { createTheme } from "../editor";
|
|
5
|
+
import { consumeMediaLightboxTrigger, openMediaLightbox } from "../editor/media-lightbox";
|
|
6
|
+
import { mediaLightboxTheme } from "../editor/media-lightbox-theme";
|
|
7
|
+
import { createMardoraIcon } from "../editor/icons";
|
|
8
|
+
import type { PreviewContext } from "../preview/types";
|
|
9
|
+
import { SyntaxNode } from "@lezer/common";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mark decorations for image syntax elements
|
|
13
|
+
*/
|
|
14
|
+
const imageMarkDecorations = {
|
|
15
|
+
"image-block": Decoration.line({ class: "cm-mardora-image-block" }),
|
|
16
|
+
"image-marker": Decoration.mark({ class: "cm-mardora-image-marker" }),
|
|
17
|
+
"image-alt": Decoration.mark({ class: "cm-mardora-image-alt" }),
|
|
18
|
+
"image-url": Decoration.mark({ class: "cm-mardora-image-url" }),
|
|
19
|
+
"image-hidden": Decoration.mark({ class: "cm-mardora-image-hidden" }),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface ParsedImageMarkdown {
|
|
23
|
+
readonly alt: string;
|
|
24
|
+
readonly url: string;
|
|
25
|
+
readonly title?: string;
|
|
26
|
+
readonly width?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ImageMarkdownChange {
|
|
30
|
+
readonly from: number;
|
|
31
|
+
readonly to: number;
|
|
32
|
+
readonly insert: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ImageMarkdownRange {
|
|
36
|
+
readonly from: number;
|
|
37
|
+
readonly imageTo: number;
|
|
38
|
+
readonly to: number;
|
|
39
|
+
readonly markdown: string;
|
|
40
|
+
readonly width?: number;
|
|
41
|
+
readonly widthAttrFrom?: number;
|
|
42
|
+
readonly widthAttrTo?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const imageWidthAttributePattern = /^(\s*)\{width=(\d+)\}/;
|
|
46
|
+
const minImageWidth = 120;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse image markdown to extract alt text, URL, optional title, and optional pixel width.
|
|
50
|
+
* Format: {width=420}
|
|
51
|
+
*/
|
|
52
|
+
export function parseImageMarkdown(content: string): ParsedImageMarkdown | null {
|
|
53
|
+
const trimmed = content.trim();
|
|
54
|
+
const widthMatch = trimmed.match(/\{width=(\d+)\}$/);
|
|
55
|
+
const markdown = widthMatch ? trimmed.slice(0, widthMatch.index).trimEnd() : trimmed;
|
|
56
|
+
|
|
57
|
+
// Regex to match:  or 
|
|
58
|
+
const match = markdown.match(/^!\[([^\]]*)\]\(([^"\s)]+)(?:\s+"([^"]*)")?\s*\)$/);
|
|
59
|
+
if (!match) return null;
|
|
60
|
+
|
|
61
|
+
const result: ParsedImageMarkdown = {
|
|
62
|
+
alt: match[1] || "",
|
|
63
|
+
url: match[2]!,
|
|
64
|
+
...(widthMatch ? { width: Number(widthMatch[1]) } : {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (match[3] !== undefined) {
|
|
68
|
+
return { ...result, title: match[3] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readImageMarkdownRange(doc: string, from: number, imageTo: number): ImageMarkdownRange {
|
|
75
|
+
const lineEnd = doc.indexOf("\n", imageTo);
|
|
76
|
+
const scanTo = lineEnd === -1 ? doc.length : lineEnd;
|
|
77
|
+
const afterImage = doc.slice(imageTo, scanTo);
|
|
78
|
+
const widthMatch = afterImage.match(imageWidthAttributePattern);
|
|
79
|
+
if (widthMatch) {
|
|
80
|
+
const widthAttrTo = imageTo + widthMatch[0].length;
|
|
81
|
+
return {
|
|
82
|
+
from,
|
|
83
|
+
imageTo,
|
|
84
|
+
to: widthAttrTo,
|
|
85
|
+
markdown: doc.slice(from, widthAttrTo),
|
|
86
|
+
width: Number(widthMatch[2]),
|
|
87
|
+
widthAttrFrom: imageTo,
|
|
88
|
+
widthAttrTo,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
from,
|
|
94
|
+
imageTo,
|
|
95
|
+
to: imageTo,
|
|
96
|
+
markdown: doc.slice(from, imageTo),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveImageWidthChange(input: {
|
|
101
|
+
readonly doc: string;
|
|
102
|
+
readonly from: number;
|
|
103
|
+
readonly to: number;
|
|
104
|
+
readonly width: number;
|
|
105
|
+
}): ImageMarkdownChange {
|
|
106
|
+
const range = readImageMarkdownRange(input.doc, input.from, input.to);
|
|
107
|
+
const width = Math.max(1, Math.round(input.width));
|
|
108
|
+
return {
|
|
109
|
+
from: range.widthAttrFrom ?? input.to,
|
|
110
|
+
to: range.widthAttrTo ?? input.to,
|
|
111
|
+
insert: `{width=${width}}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveImageResetWidthChange(input: {
|
|
116
|
+
readonly doc: string;
|
|
117
|
+
readonly from: number;
|
|
118
|
+
readonly to: number;
|
|
119
|
+
}): ImageMarkdownChange | null {
|
|
120
|
+
const range = readImageMarkdownRange(input.doc, input.from, input.to);
|
|
121
|
+
if (range.widthAttrFrom === undefined || range.widthAttrTo === undefined) return null;
|
|
122
|
+
return { from: range.widthAttrFrom, to: range.widthAttrTo, insert: "" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveImageDeleteChange(input: {
|
|
126
|
+
readonly doc: string;
|
|
127
|
+
readonly from: number;
|
|
128
|
+
readonly to: number;
|
|
129
|
+
}): ImageMarkdownChange {
|
|
130
|
+
const range = readImageMarkdownRange(input.doc, input.from, input.to);
|
|
131
|
+
return { from: input.from, to: range.to, insert: "" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Widget to render an image with optional caption using figure/figcaption
|
|
136
|
+
* Placed below the markdown syntax as a widget decoration
|
|
137
|
+
*/
|
|
138
|
+
class ImageWidget extends WidgetType {
|
|
139
|
+
constructor(
|
|
140
|
+
readonly url: string,
|
|
141
|
+
readonly alt: string,
|
|
142
|
+
readonly from: number,
|
|
143
|
+
readonly imageTo: number,
|
|
144
|
+
readonly to: number,
|
|
145
|
+
readonly width?: number,
|
|
146
|
+
readonly title?: string
|
|
147
|
+
) {
|
|
148
|
+
super();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override eq(other: ImageWidget): boolean {
|
|
152
|
+
return (
|
|
153
|
+
other.url === this.url &&
|
|
154
|
+
other.alt === this.alt &&
|
|
155
|
+
other.from === this.from &&
|
|
156
|
+
other.imageTo === this.imageTo &&
|
|
157
|
+
other.to === this.to &&
|
|
158
|
+
other.width === this.width &&
|
|
159
|
+
other.title === this.title
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
toDOM(view: EditorView) {
|
|
164
|
+
// Create figure element for semantic image container
|
|
165
|
+
const figure = document.createElement("figure");
|
|
166
|
+
figure.className = "cm-mardora-image-figure cm-mardora-media-preview";
|
|
167
|
+
figure.setAttribute("role", "figure");
|
|
168
|
+
figure.style.cursor = "pointer";
|
|
169
|
+
if (this.width) {
|
|
170
|
+
figure.style.width = `${this.width}px`;
|
|
171
|
+
}
|
|
172
|
+
if (this.title) {
|
|
173
|
+
figure.setAttribute("aria-label", this.title);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const activateFigure = () => figure.classList.add("cm-mardora-image-figure-active");
|
|
177
|
+
const deactivateFigure = () => {
|
|
178
|
+
if (!figure.matches(":focus-within")) figure.classList.remove("cm-mardora-image-figure-active");
|
|
179
|
+
};
|
|
180
|
+
figure.addEventListener("pointerenter", activateFigure);
|
|
181
|
+
figure.addEventListener("mouseenter", activateFigure);
|
|
182
|
+
figure.addEventListener("focusin", activateFigure);
|
|
183
|
+
figure.addEventListener("pointerleave", deactivateFigure);
|
|
184
|
+
figure.addEventListener("mouseleave", deactivateFigure);
|
|
185
|
+
figure.addEventListener("focusout", deactivateFigure);
|
|
186
|
+
|
|
187
|
+
// Click handler to select the raw markdown text
|
|
188
|
+
figure.addEventListener("click", (e) => {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
view.dispatch({
|
|
192
|
+
selection: { anchor: this.from, head: this.to },
|
|
193
|
+
scrollIntoView: true,
|
|
194
|
+
});
|
|
195
|
+
view.focus();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Create image element with accessibility attributes
|
|
199
|
+
const img = document.createElement("img");
|
|
200
|
+
img.className = "cm-mardora-image";
|
|
201
|
+
img.src = this.url;
|
|
202
|
+
img.alt = this.alt;
|
|
203
|
+
img.setAttribute("loading", "lazy");
|
|
204
|
+
img.setAttribute("decoding", "async");
|
|
205
|
+
if (this.title) {
|
|
206
|
+
img.title = this.title;
|
|
207
|
+
}
|
|
208
|
+
if (this.width) {
|
|
209
|
+
img.style.width = "100%";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Handle image loading error
|
|
213
|
+
img.onerror = () => {
|
|
214
|
+
img.style.display = "none";
|
|
215
|
+
const errorSpan = document.createElement("span");
|
|
216
|
+
errorSpan.className = "cm-mardora-image-error";
|
|
217
|
+
errorSpan.setAttribute("role", "alert");
|
|
218
|
+
errorSpan.textContent = `[Image not found: ${this.alt || this.url}]`;
|
|
219
|
+
figure.appendChild(errorSpan);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
figure.appendChild(img);
|
|
223
|
+
figure.appendChild(this.createToolbar(view, figure));
|
|
224
|
+
figure.appendChild(this.createResizeHandle(view, figure, "left"));
|
|
225
|
+
figure.appendChild(this.createResizeHandle(view, figure, "right"));
|
|
226
|
+
|
|
227
|
+
// Add figcaption if title exists
|
|
228
|
+
if (this.title) {
|
|
229
|
+
const figcaption = document.createElement("figcaption");
|
|
230
|
+
figcaption.className = "cm-mardora-image-caption";
|
|
231
|
+
figcaption.textContent = this.title;
|
|
232
|
+
figure.appendChild(figcaption);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return figure;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
override ignoreEvent(event: Event) {
|
|
239
|
+
return !["click", "mousedown", "mouseup", "mousemove", "pointerdown", "pointermove", "pointerup"].includes(
|
|
240
|
+
event.type
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private createToolbar(view: EditorView, figure: HTMLElement): HTMLElement {
|
|
245
|
+
const toolbar = figure.ownerDocument.createElement("div");
|
|
246
|
+
toolbar.className = "cm-mardora-image-toolbar";
|
|
247
|
+
toolbar.appendChild(
|
|
248
|
+
this.createToolButton(figure.ownerDocument, "还原默认大小", "rotate-ccw", () => {
|
|
249
|
+
const change = resolveImageResetWidthChange({
|
|
250
|
+
doc: view.state.doc.toString(),
|
|
251
|
+
from: this.from,
|
|
252
|
+
to: this.imageTo,
|
|
253
|
+
});
|
|
254
|
+
if (!change) return;
|
|
255
|
+
view.dispatch({ changes: change });
|
|
256
|
+
view.focus();
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
toolbar.appendChild(
|
|
260
|
+
this.createToolButton(figure.ownerDocument, "放大查看图片", "maximize-2", () => {
|
|
261
|
+
openMediaLightbox(figure.ownerDocument, {
|
|
262
|
+
content: {
|
|
263
|
+
kind: "image",
|
|
264
|
+
src: this.url,
|
|
265
|
+
alt: this.alt,
|
|
266
|
+
...(this.title === undefined ? {} : { title: this.title }),
|
|
267
|
+
},
|
|
268
|
+
returnFocus: toolbar.querySelector(".cm-mardora-image-preview-button") as HTMLElement | null,
|
|
269
|
+
});
|
|
270
|
+
}, "cm-mardora-image-preview-button")
|
|
271
|
+
);
|
|
272
|
+
toolbar.appendChild(
|
|
273
|
+
this.createToolButton(figure.ownerDocument, "删除图片", "trash-2", () => {
|
|
274
|
+
view.dispatch({
|
|
275
|
+
changes: resolveImageDeleteChange({
|
|
276
|
+
doc: view.state.doc.toString(),
|
|
277
|
+
from: this.from,
|
|
278
|
+
to: this.imageTo,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
view.focus();
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
return toolbar;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private createToolButton(
|
|
288
|
+
ownerDocument: Document,
|
|
289
|
+
label: string,
|
|
290
|
+
iconName: string,
|
|
291
|
+
onActivate: () => void,
|
|
292
|
+
extraClass = ""
|
|
293
|
+
): HTMLButtonElement {
|
|
294
|
+
const button = ownerDocument.createElement("button");
|
|
295
|
+
button.type = "button";
|
|
296
|
+
button.className = `cm-mardora-image-tool-button${extraClass ? ` ${extraClass}` : ""}`;
|
|
297
|
+
button.setAttribute("aria-label", label);
|
|
298
|
+
button.title = label;
|
|
299
|
+
const icon = createMardoraIcon(iconName);
|
|
300
|
+
if (icon) button.appendChild(icon);
|
|
301
|
+
button.addEventListener("click", (event) => {
|
|
302
|
+
consumeMediaLightboxTrigger(event);
|
|
303
|
+
onActivate();
|
|
304
|
+
});
|
|
305
|
+
return button;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private createResizeHandle(view: EditorView, figure: HTMLElement, side: "left" | "right"): HTMLElement {
|
|
309
|
+
const handle = figure.ownerDocument.createElement("span");
|
|
310
|
+
handle.className = `cm-mardora-image-resize-handle cm-mardora-image-resize-handle-${side}`;
|
|
311
|
+
handle.setAttribute("role", "separator");
|
|
312
|
+
handle.setAttribute("aria-label", side === "left" ? "向左拖拽调整图片宽度" : "向右拖拽调整图片宽度");
|
|
313
|
+
if (figure.ownerDocument.defaultView?.PointerEvent) {
|
|
314
|
+
handle.addEventListener("pointerdown", (event) => this.startResize(event, view, figure, side));
|
|
315
|
+
} else {
|
|
316
|
+
handle.addEventListener("mousedown", (event) => this.startResize(event, view, figure, side));
|
|
317
|
+
}
|
|
318
|
+
return handle;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private startResize(event: PointerEvent | MouseEvent, view: EditorView, figure: HTMLElement, side: "left" | "right") {
|
|
322
|
+
consumeMediaLightboxTrigger(event);
|
|
323
|
+
const ownerDocument = figure.ownerDocument;
|
|
324
|
+
const startX = event.clientX;
|
|
325
|
+
const startWidth = Math.max(minImageWidth, figure.getBoundingClientRect().width || this.width || minImageWidth);
|
|
326
|
+
const maxWidth = resolveImageMaxWidth(view, figure);
|
|
327
|
+
const direction = side === "right" ? 1 : -1;
|
|
328
|
+
const image = figure.querySelector("img");
|
|
329
|
+
if (image) image.style.width = "100%";
|
|
330
|
+
let nextWidth = startWidth;
|
|
331
|
+
|
|
332
|
+
const moveEvent = event.type.startsWith("pointer") ? "pointermove" : "mousemove";
|
|
333
|
+
const upEvent = event.type.startsWith("pointer") ? "pointerup" : "mouseup";
|
|
334
|
+
const onMove = (move: Event) => {
|
|
335
|
+
const pointer = move as PointerEvent | MouseEvent;
|
|
336
|
+
consumeMediaLightboxTrigger(pointer);
|
|
337
|
+
nextWidth = clampImageWidth(startWidth + (pointer.clientX - startX) * direction, maxWidth);
|
|
338
|
+
figure.style.width = `${nextWidth}px`;
|
|
339
|
+
};
|
|
340
|
+
const onUp = (up: Event) => {
|
|
341
|
+
consumeMediaLightboxTrigger(up);
|
|
342
|
+
ownerDocument.removeEventListener(moveEvent, onMove);
|
|
343
|
+
ownerDocument.removeEventListener(upEvent, onUp);
|
|
344
|
+
view.dispatch({
|
|
345
|
+
changes: resolveImageWidthChange({
|
|
346
|
+
doc: view.state.doc.toString(),
|
|
347
|
+
from: this.from,
|
|
348
|
+
to: this.imageTo,
|
|
349
|
+
width: nextWidth,
|
|
350
|
+
}),
|
|
351
|
+
});
|
|
352
|
+
view.focus();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
ownerDocument.addEventListener(moveEvent, onMove);
|
|
356
|
+
ownerDocument.addEventListener(upEvent, onUp);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function clampImageWidth(width: number, maxWidth: number): number {
|
|
361
|
+
return Math.max(minImageWidth, Math.min(Math.round(width), maxWidth));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resolveImageMaxWidth(view: EditorView, figure: HTMLElement): number {
|
|
365
|
+
const content = figure.closest(".cm-content") ?? view.contentDOM ?? view.dom;
|
|
366
|
+
const width = content.getBoundingClientRect().width;
|
|
367
|
+
return Math.max(minImageWidth, Math.round(width || figure.getBoundingClientRect().width || 800));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* ImagePlugin - Decorates and renders images in markdown
|
|
372
|
+
*
|
|
373
|
+
* Supports the full image syntax: 
|
|
374
|
+
* - Shows image widget below the node when cursor is not in range
|
|
375
|
+
* - Hides the markdown syntax when cursor is not in range
|
|
376
|
+
* - Shows raw markdown when cursor is in the image syntax
|
|
377
|
+
* - Uses figure/figcaption for semantic HTML with accessibility attributes
|
|
378
|
+
*/
|
|
379
|
+
export class ImagePlugin extends DecorationPlugin {
|
|
380
|
+
readonly name = "image";
|
|
381
|
+
readonly version = "1.0.0";
|
|
382
|
+
override decorationPriority = 25;
|
|
383
|
+
override readonly requiredNodes = ["Image"] as const;
|
|
384
|
+
|
|
385
|
+
constructor() {
|
|
386
|
+
super();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Plugin theme
|
|
391
|
+
*/
|
|
392
|
+
override get theme() {
|
|
393
|
+
return theme;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Keyboard shortcuts for image formatting
|
|
398
|
+
*/
|
|
399
|
+
override getKeymap(): KeyBinding[] {
|
|
400
|
+
return [
|
|
401
|
+
{
|
|
402
|
+
key: "Mod-Shift-i",
|
|
403
|
+
run: (view) => this.toggleImage(view),
|
|
404
|
+
preventDefault: true,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* URL regex pattern
|
|
411
|
+
*/
|
|
412
|
+
private readonly urlPattern = /^(https?:\/\/|www\.)[^\s]+$/i;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Toggle image on selection
|
|
416
|
+
* - If text selected and is a URL:  with cursor in brackets
|
|
417
|
+
* - If text selected (not URL): ![text]() with cursor in parentheses
|
|
418
|
+
* - If nothing selected: ![Alt Text]() with cursor in parentheses
|
|
419
|
+
* - If already an image: remove syntax, leave just the URL
|
|
420
|
+
*/
|
|
421
|
+
private toggleImage(view: EditorView): boolean {
|
|
422
|
+
const { state } = view;
|
|
423
|
+
const { from, to, empty } = state.selection.main;
|
|
424
|
+
const selectedText = state.sliceDoc(from, to);
|
|
425
|
+
|
|
426
|
+
// Check if selection is already an image 
|
|
427
|
+
const imageMatch = selectedText.match(/^!\[([^\]]*)\]\(([^)]*)\)$/);
|
|
428
|
+
if (imageMatch) {
|
|
429
|
+
// Already an image - extract just the URL and replace
|
|
430
|
+
const imageUrl = imageMatch[2] || "";
|
|
431
|
+
view.dispatch({
|
|
432
|
+
changes: { from, to, insert: imageUrl },
|
|
433
|
+
selection: { anchor: from, head: from + imageUrl.length },
|
|
434
|
+
});
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check if we're inside an image by looking at surrounding context
|
|
439
|
+
const lineStart = state.doc.lineAt(from).from;
|
|
440
|
+
const lineEnd = state.doc.lineAt(to).to;
|
|
441
|
+
const lineText = state.sliceDoc(lineStart, lineEnd);
|
|
442
|
+
|
|
443
|
+
// Find image pattern in line that contains the selection
|
|
444
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]*)\)/g;
|
|
445
|
+
let match;
|
|
446
|
+
while ((match = imageRegex.exec(lineText)) !== null) {
|
|
447
|
+
const matchFrom = lineStart + match.index;
|
|
448
|
+
const matchTo = matchFrom + match[0].length;
|
|
449
|
+
|
|
450
|
+
// Check if selection is within this image
|
|
451
|
+
if (from >= matchFrom && to <= matchTo) {
|
|
452
|
+
// Remove the image syntax, leave just the URL
|
|
453
|
+
const imageUrl = match[2] || "";
|
|
454
|
+
view.dispatch({
|
|
455
|
+
changes: { from: matchFrom, to: matchTo, insert: imageUrl },
|
|
456
|
+
selection: { anchor: matchFrom, head: matchFrom + imageUrl.length },
|
|
457
|
+
});
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (empty) {
|
|
463
|
+
// No selection - insert ![Alt Text]() and place cursor in parentheses
|
|
464
|
+
const defaultAlt = "Alt Text";
|
|
465
|
+
const newText = `![${defaultAlt}]()`;
|
|
466
|
+
view.dispatch({
|
|
467
|
+
changes: { from, insert: newText },
|
|
468
|
+
selection: { anchor: from + defaultAlt.length + 4 }, // After ;
|
|
470
|
+
} else if (this.urlPattern.test(selectedText)) {
|
|
471
|
+
// Selected text is a URL - put it in parentheses with default alt text
|
|
472
|
+
const defaultAlt = "Alt Text";
|
|
473
|
+
const newText = ``;
|
|
474
|
+
view.dispatch({
|
|
475
|
+
changes: { from, to, insert: newText },
|
|
476
|
+
selection: { anchor: from + 2, head: from + 2 + defaultAlt.length }, // Select "Alt Text"
|
|
477
|
+
});
|
|
478
|
+
} else {
|
|
479
|
+
// Selected text is not a URL - use as alt text, cursor in parentheses
|
|
480
|
+
const newText = `![${selectedText}]()`;
|
|
481
|
+
view.dispatch({
|
|
482
|
+
changes: { from, to, insert: newText },
|
|
483
|
+
selection: { anchor: from + selectedText.length + 4 }, // After ;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
buildDecorations(ctx: DecorationContext): void {
|
|
491
|
+
const { view, decorations } = ctx;
|
|
492
|
+
const tree = syntaxTree(view.state);
|
|
493
|
+
|
|
494
|
+
tree.iterate({
|
|
495
|
+
enter: (node) => {
|
|
496
|
+
const { from, to, name } = node;
|
|
497
|
+
|
|
498
|
+
// Handle Image nodes
|
|
499
|
+
if (name === "Image") {
|
|
500
|
+
const imageRange = readImageMarkdownRange(view.state.doc.toString(), from, to);
|
|
501
|
+
const parsed = parseImageMarkdown(imageRange.markdown);
|
|
502
|
+
|
|
503
|
+
if (!parsed) return;
|
|
504
|
+
|
|
505
|
+
const cursorInRange = ctx.selectionOverlapsRange(from, imageRange.to);
|
|
506
|
+
|
|
507
|
+
// Add line decoration for image
|
|
508
|
+
decorations.push(imageMarkDecorations["image-block"].range(from));
|
|
509
|
+
|
|
510
|
+
// Always add the image widget below the node
|
|
511
|
+
decorations.push(
|
|
512
|
+
Decoration.widget({
|
|
513
|
+
widget: new ImageWidget(parsed.url, parsed.alt, from, to, imageRange.to, parsed.width, parsed.title),
|
|
514
|
+
side: 1, // Place after the position
|
|
515
|
+
block: false, // Don't create a new line
|
|
516
|
+
}).range(imageRange.to)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
if (cursorInRange) {
|
|
520
|
+
// Cursor in range: show raw markdown with styling
|
|
521
|
+
this.decorateRawImage(node.node, decorations, view);
|
|
522
|
+
} else {
|
|
523
|
+
// Cursor out of range: hide the raw markdown text
|
|
524
|
+
decorations.push(imageMarkDecorations["image-hidden"].range(from, imageRange.to));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Decorate raw image markdown when cursor is in range
|
|
533
|
+
*/
|
|
534
|
+
private decorateRawImage(
|
|
535
|
+
node: SyntaxNode,
|
|
536
|
+
decorations: import("@codemirror/state").Range<Decoration>[],
|
|
537
|
+
view: import("@codemirror/view").EditorView
|
|
538
|
+
): void {
|
|
539
|
+
// Find and style child nodes
|
|
540
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
541
|
+
if (child.name === "URL") {
|
|
542
|
+
decorations.push(imageMarkDecorations["image-url"].range(child.from, child.to));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Style the markers (! [ ] ( ))
|
|
547
|
+
const content = view.state.sliceDoc(node.from, node.to);
|
|
548
|
+
const bangBracket = node.from; // Position of !
|
|
549
|
+
if (content.startsWith("![")) {
|
|
550
|
+
decorations.push(imageMarkDecorations["image-marker"].range(bangBracket, bangBracket + 2));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Find and style closing bracket and parentheses
|
|
554
|
+
const altEnd = content.indexOf("](");
|
|
555
|
+
if (altEnd !== -1) {
|
|
556
|
+
const altStart = 2;
|
|
557
|
+
// Style alt text
|
|
558
|
+
if (altEnd > altStart) {
|
|
559
|
+
decorations.push(imageMarkDecorations["image-alt"].range(node.from + altStart, node.from + altEnd));
|
|
560
|
+
}
|
|
561
|
+
// Style ]( markers
|
|
562
|
+
decorations.push(imageMarkDecorations["image-marker"].range(node.from + altEnd, node.from + altEnd + 2));
|
|
563
|
+
// Style closing )
|
|
564
|
+
decorations.push(imageMarkDecorations["image-marker"].range(node.to - 1, node.to));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Render image to HTML for preview mode using figure/figcaption
|
|
570
|
+
*/
|
|
571
|
+
override renderToHTML(
|
|
572
|
+
node: SyntaxNode,
|
|
573
|
+
_children: string,
|
|
574
|
+
ctx: PreviewContext
|
|
575
|
+
): string | null {
|
|
576
|
+
if (node.name !== "Image") return null;
|
|
577
|
+
|
|
578
|
+
const range = readImageMarkdownRange(ctx.doc, node.from, node.to);
|
|
579
|
+
const content = ctx.sliceDoc(node.from, range.to);
|
|
580
|
+
const parsed = parseImageMarkdown(content);
|
|
581
|
+
if (!parsed) return null;
|
|
582
|
+
|
|
583
|
+
const altAttr = ctx.sanitize(parsed.alt);
|
|
584
|
+
const titleAttr = parsed.title ? ` title="${ctx.sanitize(parsed.title)}"` : "";
|
|
585
|
+
const ariaLabel = parsed.title ? ` aria-label="${ctx.sanitize(parsed.title)}"` : "";
|
|
586
|
+
const widthStyle = parsed.width ? ` style="width: ${parsed.width}px;"` : "";
|
|
587
|
+
|
|
588
|
+
let html = `<figure class="cm-mardora-image-figure" role="figure"${ariaLabel}>`;
|
|
589
|
+
html += `<img class="cm-mardora-image" src="${ctx.sanitize(parsed.url)}" alt="${altAttr}"${titleAttr}${widthStyle} loading="lazy" decoding="async" />`;
|
|
590
|
+
|
|
591
|
+
if (parsed.title) {
|
|
592
|
+
html += `<figcaption class="cm-mardora-image-caption">${ctx.sanitize(parsed.title)}</figcaption>`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
html += `</figure>`;
|
|
596
|
+
return html;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
override getPreviewConsumedTo(node: SyntaxNode, ctx: { doc: string }): number | null {
|
|
600
|
+
if (node.name !== "Image") return null;
|
|
601
|
+
return readImageMarkdownRange(ctx.doc, node.from, node.to).to;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Theme for image styling
|
|
607
|
+
*/
|
|
608
|
+
const imageTheme = createTheme({
|
|
609
|
+
default: {
|
|
610
|
+
".cm-mardora-image-block br": {
|
|
611
|
+
display: "none",
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
// Image markers (! [ ] ( ))
|
|
615
|
+
".cm-mardora-image-marker": {
|
|
616
|
+
color: "#6a737d",
|
|
617
|
+
fontFamily: "var(--font-jetbrains-mono, monospace)",
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
// Alt text
|
|
621
|
+
".cm-mardora-image-alt": {
|
|
622
|
+
color: "#22863a",
|
|
623
|
+
fontStyle: "italic",
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
// URL
|
|
627
|
+
".cm-mardora-image-url": {
|
|
628
|
+
color: "#0366d6",
|
|
629
|
+
textDecoration: "underline",
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
// Hidden markdown syntax (when cursor is not in range)
|
|
633
|
+
".cm-mardora-image-hidden": {
|
|
634
|
+
display: "none",
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
// Figure container — fill the content width so the image aligns with
|
|
638
|
+
// surrounding text lines instead of shrinking to its intrinsic size and
|
|
639
|
+
// leaving a gap on the right. Inline width (from {width=} / drag-resize)
|
|
640
|
+
// overrides this via the style attribute.
|
|
641
|
+
".cm-mardora-image-figure": {
|
|
642
|
+
display: "flex",
|
|
643
|
+
flexDirection: "column",
|
|
644
|
+
alignItems: "start",
|
|
645
|
+
width: "100%",
|
|
646
|
+
maxWidth: "100%",
|
|
647
|
+
padding: "0",
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
".cm-mardora-image-toolbar": {
|
|
651
|
+
position: "absolute",
|
|
652
|
+
top: "0.5rem",
|
|
653
|
+
right: "0.5rem",
|
|
654
|
+
display: "inline-flex",
|
|
655
|
+
alignItems: "center",
|
|
656
|
+
gap: "0.125rem",
|
|
657
|
+
padding: "0.1875rem",
|
|
658
|
+
border: "1px solid rgba(148, 163, 184, 0.4)",
|
|
659
|
+
borderRadius: "0.375rem",
|
|
660
|
+
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
661
|
+
boxShadow: "0 8px 24px rgba(15, 23, 42, 0.14)",
|
|
662
|
+
opacity: "0",
|
|
663
|
+
pointerEvents: "auto",
|
|
664
|
+
transition: "opacity 120ms ease, background-color 120ms ease, border-color 120ms ease",
|
|
665
|
+
zIndex: "3",
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
".cm-mardora-image-figure:hover .cm-mardora-image-toolbar, .cm-mardora-image-figure-active .cm-mardora-image-toolbar, .cm-mardora-image-toolbar:focus-within": {
|
|
669
|
+
opacity: "1",
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
".cm-mardora-image-tool-button": {
|
|
673
|
+
width: "1.75rem",
|
|
674
|
+
height: "1.75rem",
|
|
675
|
+
border: "0",
|
|
676
|
+
borderRadius: "0.25rem",
|
|
677
|
+
backgroundColor: "transparent",
|
|
678
|
+
color: "#334155",
|
|
679
|
+
display: "inline-flex",
|
|
680
|
+
alignItems: "center",
|
|
681
|
+
justifyContent: "center",
|
|
682
|
+
padding: "0",
|
|
683
|
+
cursor: "pointer",
|
|
684
|
+
transition: "background-color 120ms ease, color 120ms ease",
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
".cm-mardora-image-tool-button:hover, .cm-mardora-image-tool-button:focus-visible": {
|
|
688
|
+
backgroundColor: "rgba(37, 99, 235, 0.1)",
|
|
689
|
+
color: "#2563eb",
|
|
690
|
+
outline: "none",
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
".cm-mardora-image-tool-button svg": {
|
|
694
|
+
width: "1rem",
|
|
695
|
+
height: "1rem",
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
".cm-mardora-image-resize-handle": {
|
|
699
|
+
position: "absolute",
|
|
700
|
+
top: "50%",
|
|
701
|
+
width: "0.375rem",
|
|
702
|
+
height: "2.75rem",
|
|
703
|
+
borderRadius: "999px",
|
|
704
|
+
backgroundColor: "#3b82f6",
|
|
705
|
+
boxShadow: "0 0 0 2px rgba(255, 255, 255, 0.9), 0 8px 20px rgba(37, 99, 235, 0.24)",
|
|
706
|
+
cursor: "ew-resize",
|
|
707
|
+
opacity: "0",
|
|
708
|
+
transform: "translateY(-50%)",
|
|
709
|
+
transition: "opacity 120ms ease, background-color 120ms ease",
|
|
710
|
+
zIndex: "2",
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
".cm-mardora-image-figure:hover .cm-mardora-image-resize-handle, .cm-mardora-image-figure-active .cm-mardora-image-resize-handle, .cm-mardora-image-resize-handle:focus-visible": {
|
|
714
|
+
opacity: "1",
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
".cm-mardora-image-resize-handle:hover, .cm-mardora-image-resize-handle:focus-visible": {
|
|
718
|
+
backgroundColor: "#2563eb",
|
|
719
|
+
outline: "none",
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
".cm-mardora-image-resize-handle-left": {
|
|
723
|
+
left: "-0.1875rem",
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
".cm-mardora-image-resize-handle-right": {
|
|
727
|
+
right: "-0.1875rem",
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
// Image element — fill the figure by default so the image stretches to
|
|
731
|
+
// the content width. When an explicit width is set, img gets an inline
|
|
732
|
+
// width:100% too (figure carries the pixel width), so this stays consistent.
|
|
733
|
+
".cm-mardora-image": {
|
|
734
|
+
width: "100%",
|
|
735
|
+
maxWidth: "100%",
|
|
736
|
+
maxHeight: "800px",
|
|
737
|
+
height: "auto",
|
|
738
|
+
borderRadius: "4px",
|
|
739
|
+
},
|
|
740
|
+
|
|
741
|
+
// Figcaption
|
|
742
|
+
".cm-mardora-image-caption": {
|
|
743
|
+
display: "block",
|
|
744
|
+
width: "100%",
|
|
745
|
+
fontSize: "0.875em",
|
|
746
|
+
color: "#6a737d",
|
|
747
|
+
marginTop: "0.5em",
|
|
748
|
+
textAlign: "center",
|
|
749
|
+
fontStyle: "italic",
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
// Error state
|
|
753
|
+
".cm-mardora-image-error": {
|
|
754
|
+
display: "inline-block",
|
|
755
|
+
padding: "0.5em 1em",
|
|
756
|
+
backgroundColor: "rgba(255, 0, 0, 0.1)",
|
|
757
|
+
color: "#d73a49",
|
|
758
|
+
borderRadius: "4px",
|
|
759
|
+
fontSize: "0.875em",
|
|
760
|
+
fontStyle: "italic",
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
dark: {
|
|
765
|
+
".cm-mardora-image-marker": {
|
|
766
|
+
color: "#8b949e",
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
".cm-mardora-image-alt": {
|
|
770
|
+
color: "#7ee787",
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
".cm-mardora-image-url": {
|
|
774
|
+
color: "#58a6ff",
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
".cm-mardora-image-caption": {
|
|
778
|
+
color: "#8b949e",
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
".cm-mardora-image-toolbar": {
|
|
782
|
+
borderColor: "rgba(71, 85, 105, 0.72)",
|
|
783
|
+
backgroundColor: "rgba(15, 23, 42, 0.88)",
|
|
784
|
+
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.32)",
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
".cm-mardora-image-tool-button": {
|
|
788
|
+
color: "#cbd5e1",
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
".cm-mardora-image-tool-button:hover, .cm-mardora-image-tool-button:focus-visible": {
|
|
792
|
+
backgroundColor: "rgba(96, 165, 250, 0.18)",
|
|
793
|
+
color: "#93c5fd",
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
".cm-mardora-image-error": {
|
|
797
|
+
backgroundColor: "rgba(255, 0, 0, 0.15)",
|
|
798
|
+
color: "#f85149",
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const theme = (theme: Parameters<typeof imageTheme>[0]) => ({
|
|
804
|
+
...imageTheme(theme),
|
|
805
|
+
...mediaLightboxTheme(theme),
|
|
806
|
+
});
|