remons-markdown-editor 1.0.0-beta.2 → 1.0.0-beta.21

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.
Files changed (34) hide show
  1. package/dist/App.d.ts +2 -0
  2. package/dist/MarkdownEditor.d.ts +23 -0
  3. package/dist/components/Dialog/EditDialog.d.ts +21 -0
  4. package/dist/components/Editor/codemirrorExtensions.d.ts +12 -0
  5. package/dist/components/Editor/containerWidget.d.ts +20 -0
  6. package/dist/components/Editor/index.d.ts +16 -0
  7. package/dist/components/Icon/index.d.ts +2 -0
  8. package/dist/components/Panel/SplitPanels.d.ts +7 -0
  9. package/dist/components/Preview/index.d.ts +10 -0
  10. package/dist/components/Toolbar/buttons.d.ts +59 -0
  11. package/dist/components/Toolbar/config.d.ts +6 -0
  12. package/dist/components/Toolbar/hooks/useToolbar.d.ts +14 -0
  13. package/dist/components/Toolbar/index.d.ts +13 -0
  14. package/dist/components/Toolbar/renderers/TablePicker/TableDropdown.d.ts +5 -0
  15. package/dist/components/Toolbar/renderers/TablePicker/index.d.ts +9 -0
  16. package/dist/components/Toolbar/renderers/ToolbarItemRenderer.d.ts +7 -0
  17. package/dist/hooks/useFullscreen.d.ts +4 -0
  18. package/dist/hooks/useRegisterToolbar.d.ts +101 -0
  19. package/dist/hooks/useScrollSync.d.ts +6 -0
  20. package/dist/index.cjs +1612 -1202
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.css +184 -113
  23. package/dist/index.d.ts +5 -98
  24. package/dist/index.js +1588 -1182
  25. package/dist/index.js.map +1 -1
  26. package/dist/main.d.ts +0 -0
  27. package/dist/registry/componentRegistry.d.ts +34 -0
  28. package/dist/types/index.d.ts +148 -0
  29. package/dist/utils/parseContainer.d.ts +18 -0
  30. package/dist/utils/writeContainer.d.ts +16 -0
  31. package/package.json +77 -81
  32. package/readme.md +309 -91
  33. package/dist/index.css.map +0 -1
  34. package/dist/index.d.cts +0 -98
package/dist/index.js CHANGED
@@ -1,1232 +1,1638 @@
1
- // src/MarkdownEditor.tsx
2
- import { useState as useState3, useRef as useRef4, useCallback as useCallback5, useEffect as useEffect3, forwardRef as forwardRef2, useImperativeHandle } from "react";
3
-
4
- // src/components/Editor/index.tsx
5
- import { useEffect, useRef } from "react";
6
- import { EditorView as EditorView2, basicSetup } from "codemirror";
7
- import { EditorState as EditorState2 } from "@codemirror/state";
8
-
9
- // src/components/Editor/codemirrorExtensions.ts
10
- import { EditorView, keymap } from "@codemirror/view";
1
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
+ import { Button, ColorPicker, DatePicker, Dropdown, Form, Input, InputNumber, Modal, Rate, Select, Slider, Splitter, Switch, Tooltip, message } from "antd";
3
+ import { EditorView, basicSetup } from "codemirror";
4
+ import { EditorState, Facet } from "@codemirror/state";
5
+ import { Decoration, EditorView as EditorView$1, ViewPlugin, WidgetType, keymap } from "@codemirror/view";
11
6
  import { defaultKeymap, indentWithTab } from "@codemirror/commands";
12
7
  import { markdown } from "@codemirror/lang-markdown";
13
8
  import { oneDark } from "@codemirror/theme-one-dark";
14
-
15
- // src/components/Editor/containerWidget.ts
16
- import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
17
- import { Facet } from "@codemirror/state";
18
-
19
- // src/utils/parseContainer.ts
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import RenderMarkdown, { initHighlighter, languagesCommon } from "remons-render-markdown";
11
+ import "remons-render-markdown/dist/index.css";
12
+ import { BoldOutlined, CheckSquareOutlined, CodeOutlined, DownOutlined, FontSizeOutlined, FullscreenExitOutlined, FullscreenOutlined, ItalicOutlined, LinkOutlined, MinusOutlined, OrderedListOutlined, PictureOutlined, ReadOutlined, StrikethroughOutlined, TableOutlined, UnderlineOutlined, UnorderedListOutlined, createFromIconfontCN } from "@ant-design/icons";
13
+ //#region src/utils/parseContainer.ts
14
+ /**
15
+ * 解析容器属性 JSON 字符串。
16
+ * 支持标准 JSON 和宽松格式: { key: 'value', key2: 'value2' }
17
+ */
20
18
  function parseContainerProps(jsonStr) {
21
- var _a, _b, _c;
22
- try {
23
- const props = {};
24
- const trimmed = jsonStr.trim();
25
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
26
- const inner = trimmed.slice(1, -1).trim();
27
- if (!inner) return props;
28
- const matches = inner.matchAll(/(?:["']?)(\w+)(?:["']?)\s*:\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g);
29
- for (const m of matches) {
30
- props[m[1]] = (_c = (_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : m[4]) != null ? _c : "";
31
- }
32
- }
33
- return props;
34
- } catch (e) {
35
- return {};
36
- }
19
+ try {
20
+ const props = {};
21
+ const trimmed = jsonStr.trim();
22
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
23
+ const inner = trimmed.slice(1, -1).trim();
24
+ if (!inner) return props;
25
+ const matches = inner.matchAll(/(?:["']?)(\w+)(?:["']?)\s*:\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g);
26
+ for (const m of matches) props[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
27
+ }
28
+ return props;
29
+ } catch {
30
+ return {};
31
+ }
37
32
  }
38
33
  function findContainers(docText) {
39
- const containers = [];
40
- const lines = docText.split("\n");
41
- let i = 0;
42
- while (i < lines.length) {
43
- const line = lines[i];
44
- const match = line.match(/^:::(\w+)$/);
45
- if (match) {
46
- const typeName = match[1];
47
- let charPos = 0;
48
- for (let l = 0; l < i; l++) {
49
- charPos += lines[l].length + 1;
50
- }
51
- let props = {};
52
- let propsLineFrom = 0;
53
- let propsLineTo = 0;
54
- let propsLineIndex = i + 1;
55
- if (propsLineIndex < lines.length) {
56
- const propsLine = lines[propsLineIndex].trim();
57
- const propsMatch = propsLine.match(/^\{[\s\S]*\}$/);
58
- if (propsMatch) {
59
- props = parseContainerProps(propsLine);
60
- propsLineFrom = charPos + line.length + 1;
61
- propsLineTo = propsLineFrom + lines[propsLineIndex].length;
62
- }
63
- }
64
- let endPos = 0;
65
- for (let j = i + 1; j < lines.length; j++) {
66
- if (lines[j].trim() === ":::") {
67
- let endCharPos = 0;
68
- for (let l = 0; l < j; l++) {
69
- endCharPos += lines[l].length + 1;
70
- }
71
- endPos = endCharPos;
72
- i = j + 1;
73
- break;
74
- }
75
- }
76
- containers.push({
77
- startPos: charPos,
78
- typeName,
79
- propsLineFrom,
80
- propsLineTo,
81
- props,
82
- endPos
83
- });
84
- }
85
- i++;
86
- }
87
- return containers;
34
+ const containers = [];
35
+ const lines = docText.split("\n");
36
+ let i = 0;
37
+ while (i < lines.length) {
38
+ const line = lines[i];
39
+ const match = line.match(/^:::(\w+)$/);
40
+ if (match) {
41
+ const typeName = match[1];
42
+ let charPos = 0;
43
+ for (let l = 0; l < i; l++) charPos += lines[l].length + 1;
44
+ let props = {};
45
+ let propsLineFrom = 0;
46
+ let propsLineTo = 0;
47
+ let propsLineIndex = i + 1;
48
+ if (propsLineIndex < lines.length) {
49
+ const propsLine = lines[propsLineIndex].trim();
50
+ if (propsLine.match(/^\{[\s\S]*\}$/)) {
51
+ props = parseContainerProps(propsLine);
52
+ propsLineFrom = charPos + line.length + 1;
53
+ propsLineTo = propsLineFrom + lines[propsLineIndex].length;
54
+ }
55
+ }
56
+ let endPos = 0;
57
+ for (let j = i + 1; j < lines.length; j++) if (lines[j].trim() === ":::") {
58
+ let endCharPos = 0;
59
+ for (let l = 0; l < j; l++) endCharPos += lines[l].length + 1;
60
+ endPos = endCharPos;
61
+ i = j + 1;
62
+ break;
63
+ }
64
+ containers.push({
65
+ startPos: charPos,
66
+ typeName,
67
+ propsLineFrom,
68
+ propsLineTo,
69
+ props,
70
+ endPos
71
+ });
72
+ }
73
+ i++;
74
+ }
75
+ return containers;
88
76
  }
89
-
90
- // src/registry/componentRegistry.tsx
91
- import { Input, Select } from "antd";
92
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
77
+ //#endregion
78
+ //#region src/registry/componentRegistry.tsx
79
+ /**
80
+ * 组件注册中心,用于管理自定义组件的 schema。
81
+ * 每个容器组件(如 :::linkCard)在此注册其可编辑字段。
82
+ */
93
83
  var ComponentRegistry = class {
94
- constructor() {
95
- this.schemas = /* @__PURE__ */ new Map();
96
- }
97
- /**
98
- * 注册组件 schema
99
- */
100
- register(name, schema) {
101
- this.schemas.set(name, schema);
102
- }
103
- /**
104
- * 根据组件名获取已注册的 schema
105
- */
106
- get(name) {
107
- return this.schemas.get(name);
108
- }
109
- /**
110
- * 检查组件类型是否已注册
111
- */
112
- has(name) {
113
- return this.schemas.has(name);
114
- }
115
- /**
116
- * 获取所有已注册的组件名
117
- */
118
- getAllNames() {
119
- return Array.from(this.schemas.keys());
120
- }
121
- /**
122
- * 获取所有已注册的 schema
123
- */
124
- getAll() {
125
- return Array.from(this.schemas.values());
126
- }
127
- /**
128
- * 移除组件注册
129
- */
130
- unregister(name) {
131
- this.schemas.delete(name);
132
- }
84
+ schemas = /* @__PURE__ */ new Map();
85
+ /**
86
+ * 注册组件 schema
87
+ */
88
+ register(name, schema) {
89
+ this.schemas.set(name, schema);
90
+ }
91
+ /**
92
+ * 根据组件名获取已注册的 schema
93
+ */
94
+ get(name) {
95
+ return this.schemas.get(name);
96
+ }
97
+ /**
98
+ * 检查组件类型是否已注册
99
+ */
100
+ has(name) {
101
+ return this.schemas.has(name);
102
+ }
103
+ /**
104
+ * 获取所有已注册的组件名
105
+ */
106
+ getAllNames() {
107
+ return Array.from(this.schemas.keys());
108
+ }
109
+ /**
110
+ * 获取所有已注册的 schema
111
+ */
112
+ getAll() {
113
+ return Array.from(this.schemas.values());
114
+ }
115
+ /**
116
+ * 移除组件注册
117
+ */
118
+ unregister(name) {
119
+ this.schemas.delete(name);
120
+ }
133
121
  };
134
122
  var componentRegistry = new ComponentRegistry();
135
- componentRegistry.register("linkCard", {
136
- name: "linkCard",
137
- label: "\u94FE\u63A5\u5361\u7247",
138
- fields: [
139
- { key: "title", defaultValue: "\u94FE\u63A5\u6807\u9898" },
140
- { key: "link", defaultValue: "https://example.com" },
141
- { key: "description", defaultValue: "\u94FE\u63A5\u63CF\u8FF0" },
142
- { key: "theme", defaultValue: "default" }
143
- ],
144
- renderDialog: (props, onChange) => /* @__PURE__ */ jsxs(Fragment, { children: [
145
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
146
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u6807\u9898" }),
147
- /* @__PURE__ */ jsx(Input, { value: props.title, onChange: (e) => onChange("title", e.target.value) })
148
- ] }),
149
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
150
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u94FE\u63A5" }),
151
- /* @__PURE__ */ jsx(Input, { value: props.link, onChange: (e) => onChange("link", e.target.value) })
152
- ] }),
153
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
154
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u63CF\u8FF0" }),
155
- /* @__PURE__ */ jsx(Input.TextArea, { rows: 3, value: props.description, onChange: (e) => onChange("description", e.target.value) })
156
- ] }),
157
- /* @__PURE__ */ jsxs("div", { children: [
158
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u4E3B\u9898" }),
159
- /* @__PURE__ */ jsx(
160
- Select,
161
- {
162
- value: props.theme,
163
- onChange: (v) => onChange("theme", v),
164
- options: [
165
- { label: "\u9ED8\u8BA4", value: "default" },
166
- { label: "\u4E3B\u8981", value: "primary" },
167
- { label: "\u8B66\u544A", value: "warning" }
168
- ]
169
- }
170
- )
171
- ] })
172
- ] })
173
- });
174
- componentRegistry.register("note", {
175
- name: "note",
176
- label: "\u6CE8\u91CA",
177
- fields: [
178
- { key: "type", defaultValue: "info" },
179
- { key: "title", defaultValue: "\u6CE8\u610F" },
180
- { key: "content", defaultValue: "\u5907\u6CE8\u5185\u5BB9" }
181
- ],
182
- renderDialog: (props, onChange) => /* @__PURE__ */ jsxs(Fragment, { children: [
183
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
184
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u7C7B\u578B" }),
185
- /* @__PURE__ */ jsx(
186
- Select,
187
- {
188
- value: props.type,
189
- onChange: (v) => onChange("type", v),
190
- options: [
191
- { label: "\u4FE1\u606F", value: "info" },
192
- { label: "\u63D0\u793A", value: "tip" },
193
- { label: "\u8B66\u544A", value: "warning" },
194
- { label: "\u5371\u9669", value: "danger" }
195
- ]
196
- }
197
- )
198
- ] }),
199
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
200
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u6807\u9898" }),
201
- /* @__PURE__ */ jsx(Input, { value: props.title, onChange: (e) => onChange("title", e.target.value) })
202
- ] }),
203
- /* @__PURE__ */ jsxs("div", { children: [
204
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 4 }, children: "\u5185\u5BB9" }),
205
- /* @__PURE__ */ jsx(Input.TextArea, { rows: 3, value: props.content, onChange: (e) => onChange("content", e.target.value) })
206
- ] })
207
- ] })
208
- });
209
- var componentRegistry_default = componentRegistry;
210
-
211
- // src/components/Editor/containerWidget.ts
212
- var containerEditCallback = Facet.define({
213
- combine: (values) => values[0]
214
- });
123
+ //#endregion
124
+ //#region src/components/Editor/containerWidget.ts
125
+ /**
126
+ * 自定义 facet 用于传递编辑回调
127
+ */
128
+ var containerEditCallback = Facet.define({ combine: (values) => values[0] });
129
+ /**
130
+ * 只读的容器头行 Widget(显示 :::typeName + 编辑按钮)
131
+ * 使用 Decoration.replace 替换原始文本,使其不可直接编辑
132
+ */
215
133
  var ContainerHeaderWidget = class extends WidgetType {
216
- constructor(typeName) {
217
- super();
218
- this.typeName = typeName;
219
- }
220
- toDOM() {
221
- var _a, _b;
222
- const container = document.createElement("span");
223
- container.className = "container-widget-header";
224
- const text = document.createElement("span");
225
- text.className = "container-widget-text";
226
- text.textContent = `:::${this.typeName}`;
227
- const button = document.createElement("button");
228
- button.className = "container-edit-btn";
229
- button.title = `\u7F16\u8F91 ${(_b = (_a = componentRegistry_default.get(this.typeName)) == null ? void 0 : _a.label) != null ? _b : this.typeName}`;
230
- button.textContent = "\u270E";
231
- button.__typeName = this.typeName;
232
- container.appendChild(text);
233
- container.appendChild(button);
234
- return container;
235
- }
236
- eq(other) {
237
- return this.typeName === other.typeName;
238
- }
239
- ignoreEvent() {
240
- return false;
241
- }
134
+ typeName;
135
+ constructor(typeName) {
136
+ super();
137
+ this.typeName = typeName;
138
+ }
139
+ toDOM() {
140
+ const container = document.createElement("span");
141
+ container.className = "container-widget-header";
142
+ const text = document.createElement("span");
143
+ text.className = "container-widget-text";
144
+ text.textContent = `:::${this.typeName}`;
145
+ const button = document.createElement("button");
146
+ button.className = "container-edit-btn";
147
+ button.title = `编辑 ${componentRegistry.get(this.typeName)?.label ?? this.typeName}`;
148
+ button.textContent = "✎";
149
+ button.__typeName = this.typeName;
150
+ container.appendChild(text);
151
+ container.appendChild(button);
152
+ return container;
153
+ }
154
+ eq(other) {
155
+ return this.typeName === other.typeName;
156
+ }
157
+ ignoreEvent() {
158
+ return false;
159
+ }
242
160
  };
161
+ /**
162
+ * 只读的容器属性行 Widget(显示 { key: value, ... })
163
+ * 使用 Decoration.replace 替换原始文本,使其不可直接编辑
164
+ */
243
165
  var ContainerPropsWidget = class extends WidgetType {
244
- constructor(propsText) {
245
- super();
246
- this.propsText = propsText;
247
- }
248
- toDOM() {
249
- const span = document.createElement("span");
250
- span.className = "container-widget-props";
251
- span.textContent = this.propsText;
252
- return span;
253
- }
254
- eq(other) {
255
- return this.propsText === other.propsText;
256
- }
257
- ignoreEvent() {
258
- return false;
259
- }
166
+ propsText;
167
+ constructor(propsText) {
168
+ super();
169
+ this.propsText = propsText;
170
+ }
171
+ toDOM() {
172
+ const span = document.createElement("span");
173
+ span.className = "container-widget-props";
174
+ span.textContent = this.propsText;
175
+ return span;
176
+ }
177
+ eq(other) {
178
+ return this.propsText === other.propsText;
179
+ }
180
+ ignoreEvent() {
181
+ return false;
182
+ }
260
183
  };
184
+ /**
185
+ * 只读的容器结束行 Widget(显示 :::)
186
+ * 使用 Decoration.replace 替换原始文本,使其不可直接编辑
187
+ */
261
188
  var ContainerCloseWidget = class extends WidgetType {
262
- toDOM() {
263
- const span = document.createElement("span");
264
- span.className = "container-widget-text";
265
- span.textContent = ":::";
266
- return span;
267
- }
268
- eq() {
269
- return true;
270
- }
271
- ignoreEvent() {
272
- return false;
273
- }
189
+ toDOM() {
190
+ const span = document.createElement("span");
191
+ span.className = "container-widget-text";
192
+ span.textContent = ":::";
193
+ return span;
194
+ }
195
+ eq() {
196
+ return true;
197
+ }
198
+ ignoreEvent() {
199
+ return false;
200
+ }
274
201
  };
202
+ /**
203
+ * 查找文档中的容器,创建 Decoration 装饰物
204
+ * 将 ::: 头行、{} 属性行和 ::: 结束行替换为只读 widget,使其无法在编辑器中直接编辑
205
+ */
275
206
  function findContainerDecorations(state) {
276
- const docText = state.doc.toString();
277
- const containers = findContainers(docText);
278
- const decorations = [];
279
- for (const container of containers) {
280
- const headerLine = state.doc.lineAt(container.startPos);
281
- decorations.push({
282
- from: headerLine.from,
283
- to: headerLine.to,
284
- value: Decoration.replace({
285
- widget: new ContainerHeaderWidget(container.typeName)
286
- })
287
- });
288
- if (container.propsLineFrom > 0 && container.propsLineTo > 0) {
289
- const propsLine = state.doc.lineAt(container.propsLineFrom);
290
- const propsText = propsLine.text;
291
- decorations.push({
292
- from: propsLine.from,
293
- to: propsLine.to,
294
- value: Decoration.replace({
295
- widget: new ContainerPropsWidget(propsText)
296
- })
297
- });
298
- }
299
- if (container.endPos > 0) {
300
- const endLine = state.doc.lineAt(container.endPos);
301
- decorations.push({
302
- from: endLine.from,
303
- to: endLine.to,
304
- value: Decoration.replace({
305
- widget: new ContainerCloseWidget()
306
- })
307
- });
308
- }
309
- }
310
- return decorations;
207
+ const containers = findContainers(state.doc.toString());
208
+ const decorations = [];
209
+ for (const container of containers) {
210
+ const headerLine = state.doc.lineAt(container.startPos);
211
+ decorations.push({
212
+ from: headerLine.from,
213
+ to: headerLine.to,
214
+ value: Decoration.replace({ widget: new ContainerHeaderWidget(container.typeName) })
215
+ });
216
+ if (container.propsLineFrom > 0 && container.propsLineTo > 0) {
217
+ const propsLine = state.doc.lineAt(container.propsLineFrom);
218
+ const propsText = propsLine.text;
219
+ decorations.push({
220
+ from: propsLine.from,
221
+ to: propsLine.to,
222
+ value: Decoration.replace({ widget: new ContainerPropsWidget(propsText) })
223
+ });
224
+ }
225
+ if (container.endPos > 0) {
226
+ const endLine = state.doc.lineAt(container.endPos);
227
+ decorations.push({
228
+ from: endLine.from,
229
+ to: endLine.to,
230
+ value: Decoration.replace({ widget: new ContainerCloseWidget() })
231
+ });
232
+ }
233
+ }
234
+ return decorations;
311
235
  }
312
- var containerWidgetPlugin = ViewPlugin.fromClass(
313
- class {
314
- constructor(view) {
315
- this.decorations = Decoration.set(findContainerDecorations(view.state), true);
316
- }
317
- update(update) {
318
- if (update.docChanged || update.selectionSet) {
319
- this.decorations = Decoration.set(findContainerDecorations(update.state), true);
320
- }
321
- }
322
- },
323
- {
324
- decorations: (value) => value.decorations,
325
- eventHandlers: {
326
- // 点击编辑按钮时触发回调
327
- click: (event, view) => {
328
- const target = event.target;
329
- if (target.classList.contains("container-edit-btn")) {
330
- const typeName = target.__typeName;
331
- if (typeName) {
332
- const docText = view.state.doc.toString();
333
- const containers = findContainers(docText);
334
- const buttonPos = view.posAtDOM(target);
335
- for (const container of containers) {
336
- const headerLine = view.state.doc.lineAt(container.startPos);
337
- if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
338
- const callback = view.state.facet(containerEditCallback);
339
- if (callback) {
340
- callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
341
- }
342
- return true;
343
- }
344
- }
345
- }
346
- return false;
347
- }
348
- return false;
349
- }
350
- }
351
- }
352
- );
353
-
354
- // src/components/Editor/codemirrorExtensions.ts
355
- var getCodeMirrorExtensions = (opts) => {
356
- const extensions = [
357
- markdown(),
358
- oneDark,
359
- keymap.of([...defaultKeymap, indentWithTab]),
360
- EditorView.lineWrapping,
361
- EditorView.baseTheme({
362
- ".cm-scroller": {
363
- height: "100%",
364
- overflow: "auto"
365
- },
366
- // 容器头行 Widget 样式
367
- ".container-widget-header": {
368
- display: "inline",
369
- fontSize: "inherit",
370
- fontFamily: "inherit"
371
- },
372
- ".container-widget-text": {
373
- fontWeight: "bold",
374
- color: "#79c0ff"
375
- },
376
- // 容器属性行 Widget 样式
377
- ".container-widget-props": {
378
- display: "inline",
379
- fontSize: "inherit",
380
- fontFamily: "inherit",
381
- color: "#a5d6ff"
382
- },
383
- // 容器编辑按钮样式
384
- ".container-edit-btn": {
385
- display: "inline-block",
386
- padding: "0 4px",
387
- fontSize: "12px",
388
- color: "#58a6ff",
389
- background: "transparent",
390
- border: "none",
391
- borderRadius: "4px",
392
- cursor: "pointer",
393
- marginRight: "4px",
394
- verticalAlign: "middle"
395
- },
396
- ".container-edit-btn:hover": {
397
- background: "rgba(88, 166, 255, 0.16)"
398
- }
399
- })
400
- ];
401
- extensions.push(containerWidgetPlugin);
402
- if (opts == null ? void 0 : opts.onEditContainer) {
403
- extensions.push(containerEditCallback.of(opts.onEditContainer));
404
- }
405
- return extensions;
406
- };
407
-
408
- // src/components/Editor/index.tsx
409
- import { jsx as jsx2 } from "react/jsx-runtime";
410
- function Editor({ value, onChange, onScrollReady, onViewReady, onEditContainer }) {
411
- const editorRef = useRef(null);
412
- const viewRef = useRef(null);
413
- useEffect(() => {
414
- if (!editorRef.current) return;
415
- const extensions = [
416
- basicSetup,
417
- ...getCodeMirrorExtensions({ onEditContainer }),
418
- EditorView2.updateListener.of((update) => {
419
- if (update.docChanged) {
420
- onChange(update.state.doc.toString());
421
- }
422
- })
423
- ];
424
- const state = EditorState2.create({
425
- doc: value,
426
- extensions
427
- });
428
- viewRef.current = new EditorView2({
429
- state,
430
- parent: editorRef.current
431
- });
432
- if (onScrollReady && viewRef.current) {
433
- onScrollReady(viewRef.current.scrollDOM);
434
- }
435
- if (onViewReady) {
436
- onViewReady(viewRef.current);
437
- }
438
- return () => {
439
- var _a;
440
- (_a = viewRef.current) == null ? void 0 : _a.destroy();
441
- };
442
- }, []);
443
- useEffect(() => {
444
- if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
445
- viewRef.current.dispatch({
446
- changes: { from: 0, to: viewRef.current.state.doc.length, insert: value }
447
- });
448
- }
449
- }, [value]);
450
- return /* @__PURE__ */ jsx2("div", { ref: editorRef, className: "editor" });
451
- }
452
-
453
- // src/components/Preview/index.tsx
454
- import { forwardRef } from "react";
455
- import { registerAll, excludedSelectors } from "remons-markdown-plugins";
456
- import RenderMarkdown, { languagesCommon, initHighlighter } from "remons-render-markdown";
457
- import "remons-markdown-plugins/style.css";
458
- import "remons-render-markdown/dist/index.css";
459
- import { jsx as jsx3 } from "react/jsx-runtime";
460
- initHighlighter(languagesCommon);
461
- var Preview = forwardRef(({ content }, ref) => {
462
- return /* @__PURE__ */ jsx3("div", { ref, className: "preview", children: /* @__PURE__ */ jsx3(RenderMarkdown, { content, isSlotMermaid: true, excludedSelectors, customRenderers: [(md) => md.use(registerAll)] }) });
236
+ /**
237
+ * 容器编辑 ViewPlugin
238
+ * 将容器的 ::: 头行和 {} 属性行替换为只读 widget,使其无法在编辑器中直接编辑
239
+ */
240
+ var containerWidgetPlugin = ViewPlugin.fromClass(class {
241
+ decorations;
242
+ constructor(view) {
243
+ this.decorations = Decoration.set(findContainerDecorations(view.state), true);
244
+ }
245
+ update(update) {
246
+ if (update.docChanged || update.selectionSet) this.decorations = Decoration.set(findContainerDecorations(update.state), true);
247
+ }
248
+ }, {
249
+ decorations: (value) => value.decorations,
250
+ eventHandlers: {
251
+ mousedown: (event, view) => {
252
+ if (event.target.classList.contains("container-edit-btn")) {
253
+ event.preventDefault();
254
+ return true;
255
+ }
256
+ return false;
257
+ },
258
+ click: (event, view) => {
259
+ const target = event.target;
260
+ if (target.classList.contains("container-edit-btn")) {
261
+ event.preventDefault();
262
+ event.stopPropagation();
263
+ if (target.__typeName) {
264
+ const containers = findContainers(view.state.doc.toString());
265
+ const buttonPos = view.posAtDOM(target);
266
+ for (const container of containers) {
267
+ const headerLine = view.state.doc.lineAt(container.startPos);
268
+ if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
269
+ const callback = view.state.facet(containerEditCallback);
270
+ if (callback) callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
271
+ return true;
272
+ }
273
+ }
274
+ }
275
+ return false;
276
+ }
277
+ return false;
278
+ }
279
+ }
463
280
  });
464
- Preview.displayName = "Preview";
465
- var Preview_default = Preview;
466
-
467
- // src/components/Panel/SplitPanels.tsx
468
- import { Splitter } from "antd";
469
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
470
- function SplitPanels({ leftPanel, rightPanel }) {
471
- return /* @__PURE__ */ jsxs2(Splitter, { className: "split-panels", children: [
472
- /* @__PURE__ */ jsx4(Splitter.Panel, { defaultSize: "50%", min: "20%", max: "80%", children: leftPanel }),
473
- /* @__PURE__ */ jsx4(Splitter.Panel, { children: rightPanel })
474
- ] });
475
- }
476
-
477
- // src/components/Toolbar/index.tsx
478
- import { useCallback as useCallback4 } from "react";
479
- import { Button as Button3 } from "antd";
480
- import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons";
481
-
482
- // src/components/Toolbar/hooks/useToolbar.ts
483
- import { useCallback, useMemo } from "react";
484
-
485
- // src/components/Toolbar/config.ts
486
- import {
487
- BoldOutlined,
488
- ItalicOutlined,
489
- StrikethroughOutlined,
490
- UnderlineOutlined,
491
- UnorderedListOutlined,
492
- OrderedListOutlined,
493
- LinkOutlined,
494
- PictureOutlined,
495
- CodeOutlined,
496
- BlockOutlined,
497
- MinusOutlined,
498
- FieldStringOutlined,
499
- FontSizeOutlined,
500
- TagsOutlined
501
- } from "@ant-design/icons";
502
-
503
- // src/components/Toolbar/buttons.tsx
281
+ //#endregion
282
+ //#region src/components/Toolbar/buttons.tsx
283
+ /**
284
+ * 在光标位置插入文本
285
+ */
504
286
  function insertAtCursor(view, before, after = "", placeholder = "") {
505
- const { from, to } = view.state.selection.main;
506
- const selectedText = view.state.doc.sliceString(from, to);
507
- const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
508
- view.dispatch({ changes: { from, to, insert } });
509
- if (!selectedText && placeholder) {
510
- view.dispatch({ selection: { anchor: from + before.length } });
511
- }
287
+ const { from, to } = view.state.selection.main;
288
+ const selectedText = view.state.doc.sliceString(from, to);
289
+ const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
290
+ view.dispatch({ changes: {
291
+ from,
292
+ to,
293
+ insert
294
+ } });
295
+ if (!selectedText && placeholder) view.dispatch({ selection: { anchor: from + before.length } });
512
296
  }
297
+ /**
298
+ * 在当前行行首插入文本
299
+ */
513
300
  function insertAtLineStart(view, text) {
514
- const { from } = view.state.selection.main;
515
- const line = view.state.doc.lineAt(from);
516
- view.dispatch({ changes: { from: line.from, to: line.from, insert: text } });
517
- view.dispatch({ selection: { anchor: line.from + text.length } });
301
+ const { from } = view.state.selection.main;
302
+ const line = view.state.doc.lineAt(from);
303
+ view.dispatch({ changes: {
304
+ from: line.from,
305
+ to: line.from,
306
+ insert: text
307
+ } });
308
+ view.dispatch({ selection: { anchor: line.from + text.length } });
309
+ }
310
+ /**
311
+ * 切换标题:
312
+ * - 当前行已是目标级别标题 → 移除行首 # 前缀(变回普通文本)
313
+ * - 当前行是其他级别标题 → 替换为目标级别
314
+ * - 当前行是普通文本 → 插入目标级别前缀
315
+ * @param view - CodeMirror 编辑器实例
316
+ * @param level - 标题级别(1-6)
317
+ */
318
+ function toggleHeading(view, level) {
319
+ const line = view.state.doc.lineAt(view.state.selection.main.from);
320
+ const m = line.text.match(/^(\s*)(#{1,6})(\s+|\s*$)/);
321
+ const indent = m ? m[1] : "";
322
+ const text = "#".repeat(level) + " ";
323
+ if (m && m[2].length === level) view.dispatch({ changes: {
324
+ from: line.from,
325
+ to: line.from + m[0].length,
326
+ insert: indent
327
+ } });
328
+ else {
329
+ view.dispatch({ changes: {
330
+ from: line.from,
331
+ to: m ? line.from + m[0].length : line.from,
332
+ insert: indent + text
333
+ } });
334
+ view.dispatch({ selection: { anchor: line.from + indent.length + text.length } });
335
+ }
336
+ }
337
+ /**
338
+ * 切换任务列表(checkbox 待办)
339
+ * - 支持多行:对选区内每一行应用 `- [ ] `
340
+ * - 普通列表项(如 `- 内容`)直接转换为 `- [ ] 内容`
341
+ * - 切换:若所有目标行都已是任务项,则移除行首前缀
342
+ */
343
+ function toggleTaskList(view) {
344
+ const doc = view.state.doc;
345
+ const { from, to } = view.state.selection.main;
346
+ const first = doc.lineAt(from);
347
+ const last = doc.lineAt(to);
348
+ const lines = [];
349
+ for (let n = first.number; n <= last.number; n++) lines.push(doc.line(n));
350
+ const taskRe = /^(\s*)([-*+])\s\[( |x)\]\s?/;
351
+ const allTask = lines.every((line) => taskRe.test(line.text));
352
+ const changes = [];
353
+ for (const line of lines) {
354
+ const taskMatch = line.text.match(taskRe);
355
+ if (taskMatch) changes.push({
356
+ from: line.from,
357
+ to: line.from + taskMatch[0].length,
358
+ insert: allTask ? "" : `${taskMatch[1]}${taskMatch[2]} [ ] `
359
+ });
360
+ else {
361
+ const bulletMatch = line.text.match(/^(\s*)([-*+])\s/);
362
+ if (bulletMatch) {
363
+ const idx = line.from + bulletMatch[1].length + bulletMatch[2].length;
364
+ changes.push({
365
+ from: idx,
366
+ to: idx + 1,
367
+ insert: " [ ] "
368
+ });
369
+ } else changes.push({
370
+ from: line.from,
371
+ to: line.from,
372
+ insert: "- [ ] "
373
+ });
374
+ }
375
+ }
376
+ view.dispatch({ changes });
377
+ }
378
+ /**
379
+ * 切换行首前缀(支持多行选区)
380
+ * - 已是对应前缀的行 → 移除前缀(变回普通文本)
381
+ * - 未加前缀的行 → 在行首插入前缀
382
+ * @param view - CodeMirror 编辑器实例
383
+ * @param pattern - 匹配行首前缀的正则
384
+ * @param insertText - 未匹配时插入的前缀文本
385
+ */
386
+ function toggleLinePrefix(view, pattern, insertText) {
387
+ const doc = view.state.doc;
388
+ const { from, to } = view.state.selection.main;
389
+ const first = doc.lineAt(from);
390
+ const last = doc.lineAt(Math.max(to, from));
391
+ const changes = [];
392
+ for (let n = first.number; n <= last.number; n++) {
393
+ const line = doc.line(n);
394
+ const m = line.text.match(pattern);
395
+ if (m) changes.push({
396
+ from: line.from,
397
+ to: line.from + m[0].length,
398
+ insert: ""
399
+ });
400
+ else changes.push({
401
+ from: line.from,
402
+ to: line.from,
403
+ insert: insertText
404
+ });
405
+ }
406
+ view.dispatch({ changes });
407
+ }
408
+ /**
409
+ * 上传图片
410
+ * @param file - 要上传的文件
411
+ * @param uploadConfig - 上传配置
412
+ */
413
+ async function uploadFileToServer(file, uploadConfig) {
414
+ if (uploadConfig.onUploadImage) return uploadConfig.onUploadImage(file);
415
+ if (uploadConfig.uploadUrl) {
416
+ const formData = new FormData();
417
+ formData.append("file", file);
418
+ const response = await fetch(uploadConfig.uploadUrl, {
419
+ method: "POST",
420
+ body: formData
421
+ });
422
+ if (!response.ok) return null;
423
+ const result = await response.json();
424
+ if (uploadConfig.onGetUploadUrl) return uploadConfig.onGetUploadUrl(result);
425
+ return result.data?.path || null;
426
+ }
427
+ return null;
518
428
  }
519
- async function uploadFileToServer(file) {
520
- var _a;
521
- const formData = new FormData();
522
- formData.append("file", file);
523
- const response = await fetch("https://remons.cn:3008/content/uploadMarkdownImg", {
524
- method: "POST",
525
- body: formData
526
- });
527
- if (!response.ok) return null;
528
- const result = await response.json();
529
- if (result.code === 200 && result.success && ((_a = result.data) == null ? void 0 : _a.path)) {
530
- return `https://remons.cn:3008${result.data.path}`;
531
- }
532
- return null;
429
+ /**
430
+ * 判断是否配置了图片上传
431
+ */
432
+ function isUploadConfigured(uploadConfig) {
433
+ return !!(uploadConfig?.uploadUrl || uploadConfig?.onUploadImage);
533
434
  }
534
- function uploadImage(view) {
535
- const input = document.createElement("input");
536
- input.type = "file";
537
- input.accept = "image/*";
538
- input.style.display = "none";
539
- input.addEventListener("change", async () => {
540
- if (!input.files || input.files.length === 0) return;
541
- const file = input.files[0];
542
- const { from, to } = view.state.selection.main;
543
- const selectedText = view.state.doc.sliceString(from, to) || "\u63CF\u8FF0";
544
- const url = await uploadFileToServer(file);
545
- if (url) {
546
- const insert = `![${selectedText}](${url})`;
547
- view.dispatch({ changes: { from, to, insert } });
548
- }
549
- input.remove();
550
- });
551
- document.body.appendChild(input);
552
- input.click();
435
+ /**
436
+ * 图片上传:创建隐藏的 file input 触发选择,上传后插入 markdown 图片语法
437
+ * @param view - CodeMirror 编辑器实例
438
+ * @param uploadConfig - 上传配置(包含 uploadUrl 或 onUploadImage)
439
+ */
440
+ function uploadImage(view, uploadConfig) {
441
+ const input = document.createElement("input");
442
+ input.type = "file";
443
+ input.accept = "image/*";
444
+ input.style.display = "none";
445
+ input.addEventListener("change", async () => {
446
+ if (!input.files || input.files.length === 0) return;
447
+ const file = input.files[0];
448
+ const { from, to } = view.state.selection.main;
449
+ const selectedText = view.state.doc.sliceString(from, to) || "描述";
450
+ const url = await uploadFileToServer(file, uploadConfig);
451
+ if (url) {
452
+ const insert = `![${selectedText}](${url})`;
453
+ view.dispatch({ changes: {
454
+ from,
455
+ to,
456
+ insert
457
+ } });
458
+ }
459
+ input.remove();
460
+ });
461
+ document.body.appendChild(input);
462
+ input.click();
553
463
  }
464
+ /**
465
+ * 用前缀和后缀包裹选中文本
466
+ */
554
467
  function wrapSelection(view, prefix, suffix) {
555
- const { from, to, empty } = view.state.selection.main;
556
- if (empty) return;
557
- const selected = view.state.doc.sliceString(from, to);
558
- view.dispatch({ changes: { from, to, insert: `${prefix}${selected}${suffix}` } });
468
+ const { from, to, empty } = view.state.selection.main;
469
+ if (empty) return;
470
+ const selected = view.state.doc.sliceString(from, to);
471
+ view.dispatch({ changes: {
472
+ from,
473
+ to,
474
+ insert: `${prefix}${selected}${suffix}`
475
+ } });
559
476
  }
477
+ /**
478
+ * 切换包裹:选中的文本已包含标签则移除,否则添加
479
+ */
560
480
  function toggleWrap(view, openTag, closeTag) {
561
- const { from, to, empty } = view.state.selection.main;
562
- const doc = view.state.doc;
563
- if (!empty) {
564
- const selected = doc.sliceString(from, to);
565
- if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
566
- const inner = selected.slice(openTag.length, -closeTag.length);
567
- view.dispatch({ changes: { from, to, insert: inner } });
568
- return;
569
- }
570
- }
571
- {
572
- const cursorPos = empty ? from : to;
573
- const line = doc.lineAt(cursorPos);
574
- const lineText = doc.sliceString(line.from, line.to);
575
- const openIdx = lineText.indexOf(openTag);
576
- const closeIdx = lineText.lastIndexOf(closeTag);
577
- if (openIdx >= 0 && closeIdx > openIdx) {
578
- const absOpen = line.from + openIdx;
579
- const absClose = line.from + closeIdx;
580
- if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
581
- const inner = doc.sliceString(absOpen + openTag.length, absClose);
582
- view.dispatch({
583
- changes: {
584
- from: absOpen,
585
- to: absClose + closeTag.length,
586
- insert: inner
587
- }
588
- });
589
- return;
590
- }
591
- }
592
- }
593
- wrapSelection(view, openTag, closeTag);
481
+ const { from, to, empty } = view.state.selection.main;
482
+ const doc = view.state.doc;
483
+ if (!empty) {
484
+ const selected = doc.sliceString(from, to);
485
+ if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
486
+ const inner = selected.slice(openTag.length, -closeTag.length);
487
+ view.dispatch({ changes: {
488
+ from,
489
+ to,
490
+ insert: inner
491
+ } });
492
+ return;
493
+ }
494
+ }
495
+ {
496
+ const cursorPos = empty ? from : to;
497
+ const line = doc.lineAt(cursorPos);
498
+ const lineText = doc.sliceString(line.from, line.to);
499
+ const openIdx = lineText.indexOf(openTag);
500
+ const closeIdx = lineText.lastIndexOf(closeTag);
501
+ if (openIdx >= 0 && closeIdx > openIdx) {
502
+ const absOpen = line.from + openIdx;
503
+ const absClose = line.from + closeIdx;
504
+ if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
505
+ const inner = doc.sliceString(absOpen + openTag.length, absClose);
506
+ view.dispatch({ changes: {
507
+ from: absOpen,
508
+ to: absClose + closeTag.length,
509
+ insert: inner
510
+ } });
511
+ return;
512
+ }
513
+ }
514
+ }
515
+ wrapSelection(view, openTag, closeTag);
594
516
  }
595
-
596
- // src/components/Toolbar/config.ts
597
- function createToolbarConfig() {
598
- return [
599
- {
600
- id: "heading",
601
- icon: FontSizeOutlined,
602
- label: "\u6807\u9898",
603
- children: [
604
- { id: "heading1", label: "\u6807\u9898 1", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "# ") },
605
- { id: "heading2", label: "\u6807\u9898 2", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "## ") },
606
- { id: "heading3", label: "\u6807\u9898 3", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "### ") },
607
- { id: "heading4", label: "\u6807\u9898 4", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "#### ") },
608
- { id: "heading5", label: "\u6807\u9898 5", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "##### ") },
609
- { id: "heading6", label: "\u6807\u9898 6", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "###### ") }
610
- ]
611
- },
612
- { separatorBefore: true },
613
- { id: "bold", label: "\u52A0\u7C97", icon: BoldOutlined, handler: (v) => wrapSelection(v, "**", "**") },
614
- { id: "italic", label: "\u659C\u4F53", icon: ItalicOutlined, handler: (v) => wrapSelection(v, "*", "*") },
615
- { id: "strikethrough", label: "\u5220\u9664\u7EBF", icon: StrikethroughOutlined, handler: (v) => wrapSelection(v, "~~", "~~") },
616
- { id: "underline", label: "\u4E0B\u5212\u7EBF", icon: UnderlineOutlined, handler: (v) => wrapSelection(v, "<u>", "</u>") },
617
- { separatorBefore: true },
618
- { id: "ul", label: "\u65E0\u5E8F\u5217\u8868", icon: UnorderedListOutlined, handler: (v) => insertAtLineStart(v, "- ") },
619
- { id: "ol", label: "\u6709\u5E8F\u5217\u8868", icon: OrderedListOutlined, handler: (v) => insertAtLineStart(v, "1. ") },
620
- { separatorBefore: true },
621
- { id: "link", label: "\u94FE\u63A5", icon: LinkOutlined, handler: (v) => insertAtCursor(v, "[", "](url)", "\u6587\u672C") },
622
- { id: "image", label: "\u56FE\u7247", icon: PictureOutlined, handler: (v) => uploadImage(v) },
623
- { separatorBefore: true },
624
- { id: "code", label: "\u884C\u5185\u4EE3\u7801", icon: FieldStringOutlined, handler: (v) => wrapSelection(v, "`", "`") },
625
- {
626
- id: "codeblock",
627
- label: "\u4EE3\u7801\u5757",
628
- icon: CodeOutlined,
629
- handler: (v) => {
630
- const { from, to } = v.state.selection.main;
631
- const selected = v.state.doc.sliceString(from, to);
632
- const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
633
- v.dispatch({ changes: { from, to, insert } });
634
- }
635
- },
636
- { id: "quote", label: "\u5F15\u7528", icon: BlockOutlined, handler: (v) => insertAtLineStart(v, "> ") },
637
- { id: "textContainer", label: "\u6587\u5B57\u5BB9\u5668", icon: TagsOutlined, handler: (v) => toggleWrap(v, "<text-container>", "</text-container>") },
638
- {
639
- id: "hr",
640
- label: "\u5206\u5272\u7EBF",
641
- icon: MinusOutlined,
642
- handler: (v) => {
643
- const { from } = v.state.selection.main;
644
- v.dispatch({ changes: { from, insert: "\n---\n" } });
645
- }
646
- }
647
- ];
517
+ //#endregion
518
+ //#region src/components/Editor/codemirrorExtensions.ts
519
+ /** 图片上传配置 facet */
520
+ var uploadConfigFacet = Facet.define({ combine: (values) => values[values.length - 1] });
521
+ /** 未配置上传时的提示回调 facet */
522
+ var messageFacet = Facet.define({ combine: (values) => values[values.length - 1] });
523
+ /** 粘贴事件处理:拦截图片并上传 */
524
+ var pasteHandler = EditorView$1.domEventHandlers({ paste: (event, view) => {
525
+ const files = event.clipboardData?.files;
526
+ if (!files) return false;
527
+ const imageFiles = Array.from(files).filter((f) => f.type.startsWith("image/"));
528
+ if (imageFiles.length === 0) return false;
529
+ event.preventDefault();
530
+ const uploadConfig = view.state.facet(uploadConfigFacet);
531
+ const showMessage = view.state.facet(messageFacet);
532
+ if (!isUploadConfigured(uploadConfig)) {
533
+ showMessage?.();
534
+ return true;
535
+ }
536
+ const { from, to } = view.state.selection.main;
537
+ let offset = 0;
538
+ for (const file of imageFiles) uploadFileToServer(file, uploadConfig).then((url) => {
539
+ if (url) {
540
+ const insert = `![${file.name.replace(/\.[^.]+$/, "")}](${url})`;
541
+ view.dispatch({ changes: {
542
+ from: from + offset,
543
+ to: to + offset,
544
+ insert
545
+ } });
546
+ offset += insert.length;
547
+ }
548
+ });
549
+ return true;
550
+ } });
551
+ var getCodeMirrorExtensions = (opts) => {
552
+ const extensions = [
553
+ markdown(),
554
+ oneDark,
555
+ keymap.of([...defaultKeymap, indentWithTab]),
556
+ EditorView$1.lineWrapping,
557
+ pasteHandler,
558
+ EditorView$1.baseTheme({
559
+ ".cm-scroller": {
560
+ height: "100%",
561
+ overflow: "auto"
562
+ },
563
+ ".container-widget-header": {
564
+ display: "inline",
565
+ fontSize: "inherit",
566
+ fontFamily: "inherit"
567
+ },
568
+ ".container-widget-text": {
569
+ fontWeight: "bold",
570
+ color: "#79c0ff"
571
+ },
572
+ ".container-widget-props": {
573
+ display: "inline",
574
+ fontSize: "inherit",
575
+ fontFamily: "inherit",
576
+ color: "#a5d6ff"
577
+ },
578
+ ".container-edit-btn": {
579
+ display: "inline-block",
580
+ padding: "0 4px",
581
+ fontSize: "12px",
582
+ color: "#58a6ff",
583
+ background: "transparent",
584
+ border: "none",
585
+ borderRadius: "4px",
586
+ cursor: "pointer",
587
+ marginRight: "4px",
588
+ verticalAlign: "middle"
589
+ },
590
+ ".container-edit-btn:hover": { background: "rgba(88, 166, 255, 0.16)" }
591
+ })
592
+ ];
593
+ extensions.push(containerWidgetPlugin);
594
+ if (opts?.onEditContainer) extensions.push(containerEditCallback.of(opts.onEditContainer));
595
+ if (opts?.uploadConfig) extensions.push(uploadConfigFacet.of(opts.uploadConfig));
596
+ if (opts?.onMessageNoUploadConfig) extensions.push(messageFacet.of(opts.onMessageNoUploadConfig));
597
+ return extensions;
598
+ };
599
+ //#endregion
600
+ //#region src/components/Editor/index.tsx
601
+ function Editor({ value, onChange, onScrollReady, onViewReady, onEditContainer, uploadConfig, onMessageNoUploadConfig }) {
602
+ const editorRef = useRef(null);
603
+ const viewRef = useRef(null);
604
+ useEffect(() => {
605
+ if (!editorRef.current) return;
606
+ const extensions = [
607
+ basicSetup,
608
+ ...getCodeMirrorExtensions({
609
+ onEditContainer,
610
+ uploadConfig,
611
+ onMessageNoUploadConfig
612
+ }),
613
+ EditorView.updateListener.of((update) => {
614
+ if (update.docChanged) onChange(update.state.doc.toString());
615
+ })
616
+ ];
617
+ const state = EditorState.create({
618
+ doc: value,
619
+ extensions
620
+ });
621
+ viewRef.current = new EditorView({
622
+ state,
623
+ parent: editorRef.current
624
+ });
625
+ if (onScrollReady && viewRef.current) onScrollReady(viewRef.current.scrollDOM);
626
+ if (onViewReady) onViewReady(viewRef.current);
627
+ return () => {
628
+ viewRef.current?.destroy();
629
+ };
630
+ }, []);
631
+ useEffect(() => {
632
+ if (viewRef.current && viewRef.current.state.doc.toString() !== value) viewRef.current.dispatch({ changes: {
633
+ from: 0,
634
+ to: viewRef.current.state.doc.length,
635
+ insert: value
636
+ } });
637
+ }, [value]);
638
+ return /* @__PURE__ */ jsx("div", {
639
+ ref: editorRef,
640
+ className: "editor"
641
+ });
648
642
  }
649
-
650
- // src/components/Toolbar/hooks/useToolbar.ts
651
- function useToolbar(editorView, extraItems) {
652
- const config = useMemo(() => {
653
- const base = createToolbarConfig();
654
- return extraItems ? [...base, { separatorBefore: true }, ...extraItems] : base;
655
- }, [extraItems]);
656
- const handleClick = useCallback((button) => {
657
- if (button.handler && editorView) {
658
- editorView.focus();
659
- button.handler(editorView, editorView.state);
660
- }
661
- }, [editorView]);
662
- return { config, handleClick };
643
+ //#endregion
644
+ //#region src/components/Preview/index.tsx
645
+ var Preview = forwardRef(({ content, previewOptions }, ref) => {
646
+ useEffect(() => {
647
+ initHighlighter({
648
+ ...languagesCommon,
649
+ ...previewOptions?.languages ? previewOptions?.languages : {}
650
+ });
651
+ }, [previewOptions?.languages]);
652
+ return /* @__PURE__ */ jsx("div", {
653
+ ref,
654
+ className: "preview",
655
+ children: /* @__PURE__ */ jsx(RenderMarkdown, {
656
+ content,
657
+ ...previewOptions
658
+ })
659
+ });
660
+ });
661
+ Preview.displayName = "Preview";
662
+ //#endregion
663
+ //#region src/components/Panel/SplitPanels.tsx
664
+ var { Panel } = Splitter;
665
+ function SplitPanels({ leftPanel, rightPanel }) {
666
+ return /* @__PURE__ */ jsxs(Splitter, {
667
+ className: "split-panels",
668
+ children: [/* @__PURE__ */ jsx(Panel, {
669
+ defaultSize: "50%",
670
+ min: "20%",
671
+ max: "80%",
672
+ children: leftPanel
673
+ }), /* @__PURE__ */ jsx(Panel, { children: rightPanel })]
674
+ });
663
675
  }
664
-
665
- // src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
666
- import { Button, Dropdown } from "antd";
667
- import { DownOutlined } from "@ant-design/icons";
668
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
676
+ //#endregion
677
+ //#region src/components/Icon/index.tsx
678
+ var Icon = createFromIconfontCN({ scriptUrl: "https://at.alicdn.com/t/c/font_5241307_xoojyr52dqn.js" });
679
+ function IconComponent(type, className) {
680
+ return /* @__PURE__ */ jsx(Icon, {
681
+ type: `icon-${type}`,
682
+ className
683
+ });
684
+ }
685
+ //#endregion
686
+ //#region src/components/Toolbar/config.ts
687
+ function Quote() {
688
+ return IconComponent("quote");
689
+ }
690
+ function InlineCode() {
691
+ return IconComponent("inlinecode");
692
+ }
693
+ /**
694
+ * 创建工具栏配置
695
+ * @param options - 配置选项
696
+ */
697
+ function createToolbarConfig(options) {
698
+ const hasUploadConfig = isUploadConfigured(options);
699
+ return [
700
+ {
701
+ id: "heading",
702
+ icon: FontSizeOutlined,
703
+ label: "标题",
704
+ children: [
705
+ {
706
+ id: "heading1",
707
+ label: "H1",
708
+ handler: (v) => toggleHeading(v, 1)
709
+ },
710
+ {
711
+ id: "heading2",
712
+ label: "H2",
713
+ handler: (v) => toggleHeading(v, 2)
714
+ },
715
+ {
716
+ id: "heading3",
717
+ label: "H3",
718
+ handler: (v) => toggleHeading(v, 3)
719
+ },
720
+ {
721
+ id: "heading4",
722
+ label: "H4",
723
+ handler: (v) => toggleHeading(v, 4)
724
+ },
725
+ {
726
+ id: "heading5",
727
+ label: "H5",
728
+ handler: (v) => toggleHeading(v, 5)
729
+ },
730
+ {
731
+ id: "heading6",
732
+ label: "H6",
733
+ handler: (v) => toggleHeading(v, 6)
734
+ }
735
+ ]
736
+ },
737
+ { separatorBefore: true },
738
+ {
739
+ id: "bold",
740
+ label: "加粗",
741
+ icon: BoldOutlined,
742
+ handler: (v) => toggleWrap(v, "**", "**")
743
+ },
744
+ {
745
+ id: "italic",
746
+ label: "斜体",
747
+ icon: ItalicOutlined,
748
+ handler: (v) => toggleWrap(v, "*", "*")
749
+ },
750
+ {
751
+ id: "strikethrough",
752
+ label: "删除线",
753
+ icon: StrikethroughOutlined,
754
+ handler: (v) => toggleWrap(v, "~~", "~~")
755
+ },
756
+ {
757
+ id: "underline",
758
+ label: "下划线",
759
+ icon: UnderlineOutlined,
760
+ handler: (v) => toggleWrap(v, "<u>", "</u>")
761
+ },
762
+ { separatorBefore: true },
763
+ {
764
+ id: "ul",
765
+ label: "无序列表",
766
+ icon: UnorderedListOutlined,
767
+ handler: (v) => toggleLinePrefix(v, /^(\s*)-(?!\s*\[)\s/, "- ")
768
+ },
769
+ {
770
+ id: "ol",
771
+ label: "有序列表",
772
+ icon: OrderedListOutlined,
773
+ handler: (v) => toggleLinePrefix(v, /^(\s*)\d+\.\s/, "1. ")
774
+ },
775
+ {
776
+ id: "checkbox",
777
+ label: "待办",
778
+ icon: CheckSquareOutlined,
779
+ handler: (v) => toggleTaskList(v)
780
+ },
781
+ { separatorBefore: true },
782
+ {
783
+ id: "link",
784
+ label: "链接",
785
+ icon: LinkOutlined,
786
+ handler: (v) => insertAtCursor(v, "[", "](url)", "文本")
787
+ },
788
+ {
789
+ id: "image",
790
+ label: hasUploadConfig ? "图片" : "请配置图片上传地址",
791
+ icon: PictureOutlined,
792
+ disabled: !hasUploadConfig,
793
+ handler: hasUploadConfig ? (v) => uploadImage(v, {
794
+ uploadUrl: options?.uploadUrl,
795
+ onUploadImage: options?.onUploadImage
796
+ }) : void 0
797
+ },
798
+ { separatorBefore: true },
799
+ {
800
+ id: "code",
801
+ label: "行内代码",
802
+ icon: InlineCode,
803
+ handler: (v) => wrapSelection(v, "`", "`")
804
+ },
805
+ {
806
+ id: "codeblock",
807
+ label: "代码块",
808
+ icon: CodeOutlined,
809
+ handler: (v) => {
810
+ const { from, to } = v.state.selection.main;
811
+ const selected = v.state.doc.sliceString(from, to);
812
+ const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
813
+ v.dispatch({ changes: {
814
+ from,
815
+ to,
816
+ insert
817
+ } });
818
+ }
819
+ },
820
+ {
821
+ id: "quote",
822
+ label: "引用",
823
+ icon: Quote,
824
+ handler: (v) => insertAtLineStart(v, "> ")
825
+ },
826
+ {
827
+ id: "hr",
828
+ label: "分割线",
829
+ icon: MinusOutlined,
830
+ handler: (v) => {
831
+ const { from } = v.state.selection.main;
832
+ v.dispatch({ changes: {
833
+ from,
834
+ insert: "\n---\n"
835
+ } });
836
+ }
837
+ }
838
+ ];
839
+ }
840
+ //#endregion
841
+ //#region src/components/Toolbar/hooks/useToolbar.ts
842
+ function useToolbar(editorView, options) {
843
+ const { extraItems, uploadConfig } = options || {};
844
+ return {
845
+ config: useMemo(() => {
846
+ return createToolbarConfig(uploadConfig);
847
+ }, [uploadConfig]),
848
+ extraItemsConfig: useMemo(() => {
849
+ return extraItems || [];
850
+ }, [extraItems]),
851
+ handleClick: useCallback((button) => {
852
+ if (!editorView) return;
853
+ if (button.wrapTags) {
854
+ const [openTag, closeTag] = button.wrapTags;
855
+ toggleWrap(editorView, openTag, closeTag);
856
+ return;
857
+ }
858
+ if (button.handler) {
859
+ editorView.focus();
860
+ button.handler(editorView, editorView.state);
861
+ }
862
+ }, [editorView])
863
+ };
864
+ }
865
+ //#endregion
866
+ //#region src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
669
867
  function ToolbarItemRenderer({ item, onClick }) {
670
- const Icon = item.icon;
671
- if (item.children && item.children.length > 0) {
672
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
673
- item.separatorBefore && /* @__PURE__ */ jsx5("div", { className: "toolbar-separator" }),
674
- /* @__PURE__ */ jsx5("div", { className: "toolbar-group", children: /* @__PURE__ */ jsx5(
675
- Dropdown,
676
- {
677
- menu: {
678
- items: item.children.map((child) => {
679
- var _a;
680
- return {
681
- type: "item",
682
- key: (_a = child.id) != null ? _a : "",
683
- label: /* @__PURE__ */ jsxs3("span", { onClick: () => onClick(child), children: [
684
- child.icon && (() => {
685
- const ChildIcon = child.icon;
686
- return /* @__PURE__ */ jsx5(ChildIcon, {});
687
- })(),
688
- " ",
689
- child.label
690
- ] })
691
- };
692
- })
693
- },
694
- placement: "bottomLeft",
695
- children: /* @__PURE__ */ jsxs3(Button, { className: "toolbar-button", type: "text", size: "small", title: item.label, children: [
696
- /* @__PURE__ */ jsx5(Icon, {}),
697
- /* @__PURE__ */ jsx5(DownOutlined, { style: { marginLeft: 2, fontSize: 10 } })
698
- ] })
699
- }
700
- ) })
701
- ] });
702
- }
703
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
704
- item.separatorBefore && /* @__PURE__ */ jsx5("div", { className: "toolbar-separator" }),
705
- /* @__PURE__ */ jsx5("div", { className: "toolbar-group", children: /* @__PURE__ */ jsx5(
706
- Button,
707
- {
708
- id: item.id,
709
- className: "toolbar-button",
710
- type: "text",
711
- size: "small",
712
- title: item.label,
713
- onClick: () => onClick(item),
714
- children: /* @__PURE__ */ jsx5(Icon, {})
715
- }
716
- ) })
717
- ] });
868
+ const Icon = item.icon;
869
+ if (item.children && item.children.length > 0) return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
870
+ className: "toolbar-group",
871
+ children: /* @__PURE__ */ jsx(Tooltip, {
872
+ title: item.label,
873
+ children: /* @__PURE__ */ jsx(Dropdown, {
874
+ menu: { items: item.children.map((child) => ({
875
+ type: "item",
876
+ key: child.id ?? "",
877
+ label: /* @__PURE__ */ jsxs("span", {
878
+ onClick: () => onClick(child),
879
+ children: [
880
+ child.icon && (() => {
881
+ const ChildIcon = child.icon;
882
+ return /* @__PURE__ */ jsx(ChildIcon, {});
883
+ })(),
884
+ " ",
885
+ child.label
886
+ ]
887
+ })
888
+ })) },
889
+ placement: "bottomLeft",
890
+ children: /* @__PURE__ */ jsxs(Button, {
891
+ className: "toolbar-button",
892
+ type: "text",
893
+ size: "small",
894
+ children: [/* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsx(DownOutlined, { style: { fontSize: 10 } })]
895
+ })
896
+ })
897
+ })
898
+ })] });
899
+ const isDisabled = item.disabled;
900
+ return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
901
+ className: "toolbar-group",
902
+ children: /* @__PURE__ */ jsx(Tooltip, {
903
+ title: item.label,
904
+ children: /* @__PURE__ */ jsx(Button, {
905
+ id: item.id,
906
+ className: "toolbar-button",
907
+ type: "text",
908
+ size: "small",
909
+ disabled: isDisabled,
910
+ onClick: () => !isDisabled && onClick(item),
911
+ children: /* @__PURE__ */ jsx(Icon, {})
912
+ })
913
+ })
914
+ })] });
718
915
  }
719
-
720
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
721
- import React, { useCallback as useCallback3, useRef as useRef3 } from "react";
722
- import { Button as Button2, Dropdown as Dropdown2 } from "antd";
723
- import { TableOutlined } from "@ant-design/icons";
724
-
725
- // src/components/Toolbar/renderers/TablePicker/index.tsx
726
- import { useState, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
727
-
728
- // src/components/Toolbar/renderers/TablePicker/index.module.less
729
- var index_module_default = {};
730
-
731
- // src/components/Toolbar/renderers/TablePicker/index.tsx
732
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
733
- var MAX_ROW = 12;
734
- var MAX_COL = 12;
735
- var REFRESH_TIME = 200;
916
+ var index_module_default = {
917
+ container: "index-module__container___l6BT-",
918
+ grid: "index-module__grid___tCZ-r",
919
+ cell: "index-module__cell___vTDQz",
920
+ cellActive: "index-module__cellActive___0EYvU",
921
+ hint: "index-module__hint___dTObk",
922
+ label: "index-module__label___2CmK1"
923
+ };
924
+ //#endregion
925
+ //#region src/components/Toolbar/renderers/TablePicker/index.tsx
926
+ var MAX_ROW = 8;
927
+ var MAX_COL = 8;
736
928
  function TablePicker({ onSelect }) {
737
- const [hoverSize, setHoverSize] = useState({ row: 0, col: 0 });
738
- const [prevHoverSize, setPrevHoverSize] = useState({ row: 0, col: 0 });
739
- const [confirmedSize, setConfirmedSize] = useState({ row: 0, col: 0 });
740
- const maxColRef = useRef2(0);
741
- const timerIdRef = useRef2(null);
742
- const refreshTimerRef = useRef2(null);
743
- const commitRef = useRef2(null);
744
- const clearTimer = useCallback2((flushFn) => {
745
- if (timerIdRef.current) {
746
- timerIdRef.current = null;
747
- }
748
- if (flushFn && maxColRef.current > 0) {
749
- timerIdRef.current = setTimeout(flushFn, REFRESH_TIME);
750
- }
751
- }, []);
752
- useEffect2(
753
- () => () => {
754
- if (timerIdRef.current) clearTimeout(timerIdRef.current);
755
- if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
756
- },
757
- []
758
- );
759
- function extend(col, row) {
760
- if (col > maxColRef.current) {
761
- maxColRef.current = col;
762
- setHoverSize({ row, col });
763
- setConfirmedSize({ row, col });
764
- } else {
765
- setHoverSize({ row, col });
766
- if (row !== confirmedSize.row) {
767
- setConfirmedSize({ row, col: maxColRef.current });
768
- }
769
- if (row === 1) {
770
- commitRef.current = { cols: col, rows: 1 };
771
- setConfirmedSize({ row: 1, col });
772
- }
773
- }
774
- }
775
- function flash(col, dir, limit, finalCallback) {
776
- let current = dir === 1 ? 1 : limit;
777
- const step = dir === 1 ? 1 : -1;
778
- let timer = 0;
779
- refreshTimerRef.current = setInterval(() => {
780
- setHoverSize({ row: current, col });
781
- if (current > confirmedSize.row || dir === -1) {
782
- maxColRef.current = Math.min(MAX_COL, col);
783
- }
784
- current += step;
785
- if (dir === 1 && current > limit || dir === -1 && current < 1) {
786
- clearInterval(refreshTimerRef.current);
787
- setHoverSize({ row: current - step, col });
788
- finalCallback(col, current - step);
789
- }
790
- }, 50);
791
- clearTimeout(timer);
792
- return () => {
793
- clearInterval(refreshTimerRef.current);
794
- };
795
- }
796
- function onCellEnterCheck(index) {
797
- clearTimer();
798
- const row = Math.floor(index / MAX_COL) + 1;
799
- const col = index % MAX_COL + 1;
800
- const isDown = row > hoverSize.row;
801
- if (Math.abs(row - hoverSize.row) <= 1) {
802
- extend(col, row);
803
- } else {
804
- if (isDown && (col > maxColRef.current || maxColRef.current >= MAX_COL)) {
805
- const finalRow = Math.min(row, MAX_ROW);
806
- flash(col, 1, finalRow, (c, r) => {
807
- setConfirmedSize({ row: r, col: c });
808
- });
809
- } else if (!isDown) {
810
- flash(col, -1, row, (c, r) => {
811
- setConfirmedSize({ row: r, col: c });
812
- if (r === 1) {
813
- commitRef.current = { cols: Math.min(MAX_COL, col), rows: 1 };
814
- setConfirmedSize({ row: 1, col: Math.min(MAX_COL, col) });
815
- }
816
- });
817
- }
818
- }
819
- if (col >= MAX_COL && row >= prevHoverSize.row) {
820
- if (row !== prevHoverSize.row) {
821
- maxColRef.current = 0;
822
- setPrevHoverSize({ row: 0, col: 0 });
823
- }
824
- }
825
- setPrevHoverSize({ row, col });
826
- }
827
- function onMouseDown() {
828
- const size = commitRef.current || { cols: maxColRef.current, rows: confirmedSize.row };
829
- if (size.rows > 0 && size.cols > 0) {
830
- onSelect(size);
831
- }
832
- }
833
- return /* @__PURE__ */ jsxs4(
834
- "div",
835
- {
836
- className: index_module_default.container,
837
- onMouseDown,
838
- children: [
839
- /* @__PURE__ */ jsx6(
840
- "div",
841
- {
842
- className: index_module_default.grid,
843
- style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
844
- children: Array.from({ length: MAX_ROW * MAX_COL }).map((_, i) => {
845
- const row = Math.floor(i / MAX_COL) + 1;
846
- const col = i % MAX_COL + 1;
847
- const isSelected = row <= hoverSize.row && col <= maxColRef.current;
848
- return /* @__PURE__ */ jsx6(
849
- "div",
850
- {
851
- className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
852
- onMouseEnter: () => onCellEnterCheck(i)
853
- },
854
- i
855
- );
856
- })
857
- }
858
- ),
859
- /* @__PURE__ */ jsxs4("div", { className: index_module_default.label, children: [
860
- hoverSize.row,
861
- " x ",
862
- maxColRef.current || 1
863
- ] })
864
- ]
865
- }
866
- );
929
+ const [hoverSize, setHoverSize] = useState({
930
+ row: 0,
931
+ col: 0
932
+ });
933
+ const [confirmedSize, setConfirmedSize] = useState({
934
+ row: 0,
935
+ col: 0
936
+ });
937
+ const maxColRef = useRef(0);
938
+ const prevRowRef = useRef(0);
939
+ const timerIdRef = useRef(null);
940
+ const refreshTimerRef = useRef(null);
941
+ const commitRef = useRef(null);
942
+ const clearTimer = useCallback(() => {
943
+ if (timerIdRef.current) {
944
+ clearTimeout(timerIdRef.current);
945
+ timerIdRef.current = null;
946
+ }
947
+ }, []);
948
+ const clearFlash = useCallback(() => {
949
+ if (refreshTimerRef.current) {
950
+ clearInterval(refreshTimerRef.current);
951
+ refreshTimerRef.current = null;
952
+ }
953
+ }, []);
954
+ useEffect(() => () => {
955
+ if (timerIdRef.current) clearTimeout(timerIdRef.current);
956
+ if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
957
+ }, []);
958
+ /**
959
+ * 扩展选中区域至 (col, row)
960
+ */
961
+ function extend(col, row) {
962
+ if (row < prevRowRef.current || col < maxColRef.current) maxColRef.current = 0;
963
+ if (col > maxColRef.current) maxColRef.current = col;
964
+ setHoverSize({
965
+ row,
966
+ col
967
+ });
968
+ setConfirmedSize({
969
+ row,
970
+ col: maxColRef.current
971
+ });
972
+ if (row === 1) commitRef.current = {
973
+ cols: maxColRef.current,
974
+ rows: 1
975
+ };
976
+ }
977
+ /**
978
+ * 跳跃动画效果
979
+ */
980
+ function flash(col, dir, limit, finalCallback) {
981
+ let current = dir === 1 ? 1 : limit;
982
+ const step = dir === 1 ? 1 : -1;
983
+ refreshTimerRef.current = setInterval(() => {
984
+ setHoverSize({
985
+ row: current,
986
+ col
987
+ });
988
+ if (current > confirmedSize.row || dir === -1) maxColRef.current = Math.min(MAX_COL, col);
989
+ current += step;
990
+ if (dir === 1 && current > limit || dir === -1 && current < 1) {
991
+ clearInterval(refreshTimerRef.current);
992
+ refreshTimerRef.current = null;
993
+ const finalRow = current - step;
994
+ setHoverSize({
995
+ row: finalRow,
996
+ col
997
+ });
998
+ finalCallback(col, finalRow);
999
+ }
1000
+ }, 50);
1001
+ }
1002
+ /**
1003
+ * 核心逻辑:鼠标进入格子
1004
+ */
1005
+ function onCellEnterCheck(index) {
1006
+ clearTimer();
1007
+ clearFlash();
1008
+ const row = Math.floor(index / MAX_COL) + 1;
1009
+ const col = index % MAX_COL + 1;
1010
+ const prevRow = prevRowRef.current;
1011
+ const isJump = Math.abs(row - prevRow) > 1;
1012
+ const isDown = row > prevRow;
1013
+ prevRowRef.current = row;
1014
+ if (!isJump) extend(col, row);
1015
+ else if (isDown) {
1016
+ if (col > maxColRef.current || maxColRef.current >= MAX_COL) flash(col, 1, Math.min(row, MAX_ROW), (c, r) => {
1017
+ setConfirmedSize({
1018
+ row: r,
1019
+ col: c
1020
+ });
1021
+ });
1022
+ else extend(col, row);
1023
+ } else {
1024
+ maxColRef.current = 0;
1025
+ flash(col, -1, row, (c, r) => {
1026
+ setConfirmedSize({
1027
+ row: r,
1028
+ col: c
1029
+ });
1030
+ if (r === 1) {
1031
+ commitRef.current = {
1032
+ cols: Math.min(MAX_COL, col),
1033
+ rows: 1
1034
+ };
1035
+ setConfirmedSize({
1036
+ row: 1,
1037
+ col: Math.min(MAX_COL, col)
1038
+ });
1039
+ }
1040
+ });
1041
+ }
1042
+ }
1043
+ function onMouseDown() {
1044
+ const size = commitRef.current || {
1045
+ cols: maxColRef.current,
1046
+ rows: confirmedSize.row
1047
+ };
1048
+ if (size.rows > 0 && size.cols > 0) onSelect(size);
1049
+ }
1050
+ return /* @__PURE__ */ jsxs("div", {
1051
+ className: index_module_default.container,
1052
+ onMouseDown,
1053
+ children: [/* @__PURE__ */ jsx("div", {
1054
+ className: index_module_default.grid,
1055
+ style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
1056
+ children: Array.from({ length: 64 }).map((_, i) => {
1057
+ const row = Math.floor(i / MAX_COL) + 1;
1058
+ const col = i % MAX_COL + 1;
1059
+ const isSelected = row <= hoverSize.row && col <= maxColRef.current;
1060
+ return /* @__PURE__ */ jsx("div", {
1061
+ className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
1062
+ onMouseEnter: () => onCellEnterCheck(i)
1063
+ }, i);
1064
+ })
1065
+ }), /* @__PURE__ */ jsxs("div", {
1066
+ className: index_module_default.label,
1067
+ children: [
1068
+ hoverSize.row,
1069
+ " x ",
1070
+ maxColRef.current || 1
1071
+ ]
1072
+ })]
1073
+ });
867
1074
  }
868
-
869
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
870
- import { jsx as jsx7 } from "react/jsx-runtime";
1075
+ //#endregion
1076
+ //#region src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
871
1077
  function TableDropdown({ editorView }) {
872
- const [open, setOpen] = React.useState(false);
873
- const openCountRef = useRef3(0);
874
- const handleTableSelect = useCallback3((rows, cols) => {
875
- if (!editorView || rows <= 0 || cols <= 0) return;
876
- editorView.focus();
877
- const headerCells = Array(cols).fill(null).map((_, i) => ` \u5217${i + 1} `).join("|");
878
- const separator = Array(cols).fill(" --- ").join("|");
879
- const dataCell = Array(cols).fill(" \u5185\u5BB9 ").join("|");
880
- const dataRows = Array(rows).fill(`|${dataCell}|`).join("\n");
881
- const table = `
882
- |${headerCells}|
883
- |${separator}|
884
- ${dataRows}
885
- `;
886
- const { from } = editorView.state.selection.main;
887
- editorView.dispatch({ changes: { from, insert: table } });
888
- }, [editorView]);
889
- return /* @__PURE__ */ jsx7(
890
- Dropdown2,
891
- {
892
- trigger: ["click"],
893
- open,
894
- onOpenChange: setOpen,
895
- popupRender: () => {
896
- openCountRef.current++;
897
- return /* @__PURE__ */ jsx7("div", { style: { padding: 4 }, onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx7(
898
- TablePicker,
899
- {
900
- onSelect: (size) => {
901
- handleTableSelect(size.rows, size.cols);
902
- setOpen(false);
903
- }
904
- },
905
- openCountRef.current
906
- ) });
907
- },
908
- placement: "bottomLeft",
909
- children: /* @__PURE__ */ jsx7(Button2, { className: "toolbar-button", type: "text", size: "small", title: "\u8868\u683C", children: /* @__PURE__ */ jsx7(TableOutlined, {}) })
910
- }
911
- );
1078
+ const [open, setOpen] = React.useState(false);
1079
+ const openCountRef = useRef(0);
1080
+ const handleTableSelect = useCallback((rows, cols) => {
1081
+ if (!editorView || rows <= 0 || cols <= 0) return;
1082
+ editorView.focus();
1083
+ const headerCells = Array(cols).fill(null).map((_, i) => ` 列${i + 1} `).join("|");
1084
+ const separator = Array(cols).fill(" --- ").join("|");
1085
+ const dataCell = Array(cols).fill(" 内容 ").join("|");
1086
+ const table = `\n|${headerCells}|\n|${separator}|\n${Array(rows).fill(`|${dataCell}|`).join("\n")}\n`;
1087
+ const { from } = editorView.state.selection.main;
1088
+ editorView.dispatch({ changes: {
1089
+ from,
1090
+ insert: table
1091
+ } });
1092
+ }, [editorView]);
1093
+ return /* @__PURE__ */ jsx(Dropdown, {
1094
+ trigger: ["click"],
1095
+ open,
1096
+ onOpenChange: setOpen,
1097
+ popupRender: () => {
1098
+ openCountRef.current++;
1099
+ return /* @__PURE__ */ jsx("div", {
1100
+ style: { padding: 4 },
1101
+ onClick: (e) => e.stopPropagation(),
1102
+ children: /* @__PURE__ */ jsx(TablePicker, { onSelect: (size) => {
1103
+ handleTableSelect(size.rows, size.cols);
1104
+ setOpen(false);
1105
+ } }, openCountRef.current)
1106
+ });
1107
+ },
1108
+ placement: "bottomLeft",
1109
+ children: /* @__PURE__ */ jsx(Button, {
1110
+ className: "toolbar-button",
1111
+ type: "text",
1112
+ size: "small",
1113
+ title: "表格",
1114
+ children: /* @__PURE__ */ jsx(TableOutlined, {})
1115
+ })
1116
+ });
912
1117
  }
913
-
914
- // src/components/Toolbar/index.tsx
915
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
916
- function Toolbar({ editorView, fullscreen, onToggleFullscreen, extraItems }) {
917
- const { config, handleClick } = useToolbar(editorView, extraItems);
918
- const renderFullscreen = useCallback4(() => {
919
- if (!onToggleFullscreen) return null;
920
- const Icon = fullscreen ? FullscreenExitOutlined : FullscreenOutlined;
921
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
922
- /* @__PURE__ */ jsx8("div", { className: "toolbar-separator" }),
923
- /* @__PURE__ */ jsx8(
924
- Button3,
925
- {
926
- className: "toolbar-button",
927
- type: "text",
928
- size: "small",
929
- title: fullscreen ? "\u9000\u51FA\u5168\u5C4F" : "\u5168\u5C4F",
930
- onClick: () => {
931
- onToggleFullscreen();
932
- },
933
- children: /* @__PURE__ */ jsx8(Icon, {})
934
- }
935
- )
936
- ] });
937
- }, [fullscreen, onToggleFullscreen]);
938
- function isSeparator(item) {
939
- return !!(!item.id && item.separatorBefore);
940
- }
941
- return /* @__PURE__ */ jsxs5("div", { className: "toolbar", children: [
942
- config.map((item, idx) => {
943
- if (isSeparator(item)) {
944
- return /* @__PURE__ */ jsx8("div", { className: "toolbar-separator" }, `sep-${idx}`);
945
- }
946
- return /* @__PURE__ */ jsx8(
947
- ToolbarItemRenderer,
948
- {
949
- item,
950
- onClick: handleClick
951
- },
952
- item.id
953
- );
954
- }),
955
- /* @__PURE__ */ jsx8("div", { className: "toolbar-separator" }),
956
- /* @__PURE__ */ jsx8(TableDropdown, { editorView }),
957
- renderFullscreen()
958
- ] });
1118
+ //#endregion
1119
+ //#region src/components/Toolbar/index.tsx
1120
+ function Toolbar({ editorView, fullscreen = true, onToggleFullscreen, extraItems, uploadConfig }) {
1121
+ const { config, extraItemsConfig, handleClick } = useToolbar(editorView, {
1122
+ extraItems,
1123
+ uploadConfig
1124
+ });
1125
+ const renderFullscreen = useCallback(() => {
1126
+ if (!onToggleFullscreen) return null;
1127
+ return /* @__PURE__ */ jsx(Button, {
1128
+ className: "toolbar-button",
1129
+ type: "text",
1130
+ size: "small",
1131
+ title: fullscreen ? "退出全屏" : "全屏",
1132
+ onClick: () => {
1133
+ onToggleFullscreen();
1134
+ },
1135
+ children: /* @__PURE__ */ jsx(fullscreen ? FullscreenExitOutlined : FullscreenOutlined, {})
1136
+ });
1137
+ }, [fullscreen, onToggleFullscreen]);
1138
+ function isSeparator(item) {
1139
+ return !!(!item.id && item.separatorBefore);
1140
+ }
1141
+ return /* @__PURE__ */ jsxs("div", {
1142
+ className: "toolbar",
1143
+ children: [
1144
+ config.map((item, idx) => {
1145
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `sep-${idx}`);
1146
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1147
+ item,
1148
+ onClick: handleClick
1149
+ }, item.id);
1150
+ }),
1151
+ /* @__PURE__ */ jsx(TableDropdown, { editorView }),
1152
+ extraItemsConfig?.map((item, idx) => {
1153
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `extra-sep-${idx}`);
1154
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1155
+ item,
1156
+ onClick: handleClick
1157
+ }, item.id);
1158
+ }),
1159
+ renderFullscreen()
1160
+ ]
1161
+ });
959
1162
  }
960
-
961
- // src/components/Dialog/EditDialog.tsx
962
- import { useState as useState2, useMemo as useMemo2 } from "react";
963
- import { Modal } from "antd";
964
- import { jsx as jsx9 } from "react/jsx-runtime";
1163
+ //#endregion
1164
+ //#region src/components/Dialog/EditDialog.tsx
1165
+ /**
1166
+ * 组件类型到 antd 表单组件的映射
1167
+ */
1168
+ var FormComponents = {
1169
+ input: Input,
1170
+ textarea: Input.TextArea,
1171
+ number: InputNumber,
1172
+ select: Select,
1173
+ switch: Switch,
1174
+ slider: Slider,
1175
+ rate: Rate,
1176
+ color: ColorPicker,
1177
+ date: DatePicker,
1178
+ dateRange: DatePicker.RangePicker
1179
+ };
1180
+ /**
1181
+ * 渲染对应类型的 antd 表单组件
1182
+ */
1183
+ function renderFormItemComponent(field) {
1184
+ const props = field.componentProps || {};
1185
+ const Component = field.componentType ? FormComponents[field.componentType] : Input;
1186
+ return /* @__PURE__ */ jsx(Component, { ...props });
1187
+ }
1188
+ /**
1189
+ * 根据 fields 自动生成 Form.Item 列表
1190
+ */
1191
+ function renderAutoForm(form, fields) {
1192
+ if (!fields || fields.length === 0) return null;
1193
+ return /* @__PURE__ */ jsx(Form, {
1194
+ form,
1195
+ layout: "vertical",
1196
+ children: fields.map((field) => {
1197
+ if (!field.componentType) return null;
1198
+ const formItemProps = {
1199
+ name: field.key,
1200
+ label: field.name || field.key,
1201
+ valuePropName: field.componentType === "switch" ? "checked" : void 0,
1202
+ ...field.componentProps
1203
+ };
1204
+ return /* @__PURE__ */ jsx(Form.Item, {
1205
+ ...formItemProps,
1206
+ children: renderFormItemComponent(field)
1207
+ }, field.key);
1208
+ })
1209
+ });
1210
+ }
1211
+ /**
1212
+ * 属性编辑弹窗组件
1213
+ * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
1214
+ */
965
1215
  function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
966
- const schema = componentRegistry_default.get(typeName);
967
- const [formProps, setFormProps] = useState2({});
968
- const mergedProps = useMemo2(() => {
969
- var _a;
970
- if (!schema) return currentProps;
971
- const defaults = {};
972
- for (const field of schema.fields) {
973
- defaults[field.key] = (_a = field.defaultValue) != null ? _a : "";
974
- }
975
- return { ...defaults, ...currentProps };
976
- }, [schema, currentProps]);
977
- const handleDialogChange = (key, value) => {
978
- setFormProps((prev) => ({ ...prev, [key]: value }));
979
- };
980
- const handleOk = () => {
981
- const finalProps = visible ? { ...mergedProps, ...formProps } : mergedProps;
982
- onSave(finalProps);
983
- setFormProps({});
984
- };
985
- const handleClose = () => {
986
- setFormProps({});
987
- onClose();
988
- };
989
- if (!schema) {
990
- return null;
991
- }
992
- return /* @__PURE__ */ jsx9(
993
- Modal,
994
- {
995
- title: `${schema.label} \u5C5E\u6027\u7F16\u8F91`,
996
- open: visible,
997
- onOk: handleOk,
998
- onCancel: handleClose,
999
- okText: "\u4FDD\u5B58",
1000
- cancelText: "\u53D6\u6D88",
1001
- children: schema.renderDialog(visible ? { ...mergedProps, ...formProps } : mergedProps, handleDialogChange)
1002
- }
1003
- );
1216
+ const schema = componentRegistry.get(typeName);
1217
+ const [formProps, setFormProps] = useState({});
1218
+ const [form] = Form.useForm();
1219
+ const onSaveRef = useRef(onSave);
1220
+ onSaveRef.current = onSave;
1221
+ const dirtyRef = useRef(false);
1222
+ const mergedProps = useMemo(() => {
1223
+ if (!schema) return currentProps;
1224
+ const defaults = {};
1225
+ for (const field of schema.fields || []) defaults[field.key] = field.defaultValue ?? "";
1226
+ return {
1227
+ ...defaults,
1228
+ ...currentProps
1229
+ };
1230
+ }, [schema, currentProps]);
1231
+ useEffect(() => {
1232
+ if (visible) {
1233
+ form.setFieldsValue(mergedProps);
1234
+ setFormProps({});
1235
+ }
1236
+ }, [
1237
+ visible,
1238
+ mergedProps,
1239
+ form
1240
+ ]);
1241
+ const handleDialogChange = (key, value) => {
1242
+ setFormProps((prev) => ({
1243
+ ...prev,
1244
+ [key]: value
1245
+ }));
1246
+ };
1247
+ const handleOk = async () => {
1248
+ let finalProps = visible ? {
1249
+ ...mergedProps,
1250
+ ...formProps
1251
+ } : mergedProps;
1252
+ try {
1253
+ const formValues = await form.validateFields();
1254
+ finalProps = {
1255
+ ...finalProps,
1256
+ ...formValues
1257
+ };
1258
+ } catch {}
1259
+ onSaveRef.current(finalProps);
1260
+ dirtyRef.current = false;
1261
+ onClose();
1262
+ };
1263
+ const handleClose = () => {
1264
+ setFormProps({});
1265
+ dirtyRef.current = false;
1266
+ onClose();
1267
+ };
1268
+ if (!schema) return null;
1269
+ const contextSave = (props) => {
1270
+ setFormProps((prev) => ({
1271
+ ...prev,
1272
+ ...props
1273
+ }));
1274
+ dirtyRef.current = true;
1275
+ };
1276
+ const context = {
1277
+ props: mergedProps,
1278
+ form,
1279
+ onChange: handleDialogChange,
1280
+ onSave: contextSave
1281
+ };
1282
+ const renderDialogContent = () => {
1283
+ if (schema.renderDialog) return schema.renderDialog(context);
1284
+ if (schema.fields?.some((f) => f.componentType) && schema.fields) return renderAutoForm(form, schema.fields);
1285
+ return null;
1286
+ };
1287
+ return /* @__PURE__ */ jsx(Modal, {
1288
+ title: `${schema.label} 属性编辑`,
1289
+ open: visible,
1290
+ onOk: handleOk,
1291
+ onCancel: handleClose,
1292
+ okText: "保存",
1293
+ cancelText: "取消",
1294
+ children: renderDialogContent()
1295
+ });
1004
1296
  }
1005
-
1006
- // src/utils/writeContainer.ts
1297
+ //#endregion
1298
+ //#region src/utils/writeContainer.ts
1299
+ /**
1300
+ * 从属性对象生成 JSON 属性行。
1301
+ * 格式: { key: 'value', key2: 'value2' }
1302
+ */
1007
1303
  function writeContainerProps(props) {
1008
- const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1009
- return entries ? `{ ${entries} }` : "{}";
1304
+ const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1305
+ return entries ? `{ ${entries} }` : "{}";
1010
1306
  }
1307
+ /**
1308
+ * 更新编辑器文档中的容器属性。
1309
+ * 替换 JSON 属性行(::: 打开行之后的一行)为新属性。
1310
+ */
1011
1311
  function updateContainerProps(view, propsLineFrom, propsLineTo, newProps) {
1012
- const newPropsLine = writeContainerProps(newProps);
1013
- view.dispatch({
1014
- changes: {
1015
- from: propsLineFrom,
1016
- to: propsLineTo,
1017
- insert: newPropsLine
1018
- }
1019
- });
1312
+ const newPropsLine = writeContainerProps(newProps);
1313
+ view.dispatch({ changes: {
1314
+ from: propsLineFrom,
1315
+ to: propsLineTo,
1316
+ insert: newPropsLine
1317
+ } });
1020
1318
  }
1319
+ /**
1320
+ * 在当前光标位置插入新容器。
1321
+ * 创建 :::type 打开行 + JSON 属性 + ::: 关闭行的结构。
1322
+ */
1021
1323
  function insertContainer(view, typeName, defaultProps) {
1022
- const { from } = view.state.selection.main;
1023
- const propsLine = writeContainerProps(defaultProps);
1024
- const container = `:::${typeName}
1025
- ${propsLine}
1026
- :::
1027
- `;
1028
- view.dispatch({ changes: { from, insert: container } });
1324
+ const { from } = view.state.selection.main;
1325
+ const container = `:::${typeName}\n${writeContainerProps(defaultProps)}\n:::\n`;
1326
+ view.dispatch({ changes: {
1327
+ from,
1328
+ insert: container
1329
+ } });
1029
1330
  }
1030
-
1031
- // src/MarkdownEditor.tsx
1032
- import { ReadOutlined } from "@ant-design/icons";
1033
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1034
- var MarkdownEditor_default = forwardRef2(function MarkdownEditor({
1035
- defaultValue = "",
1036
- value,
1037
- onChange,
1038
- showPreview = true,
1039
- fullscreen: fullscreenProp = false
1040
- }, ref) {
1041
- const [content, setContent] = useState3(value != null ? value : defaultValue);
1042
- const [editorView, setEditorView] = useState3(null);
1043
- const scrollDOMRef = useRef4(null);
1044
- const previewElRef = useRef4(null);
1045
- const [fullscreen, setFullscreen] = useState3(false);
1046
- const [dialogVisible, setDialogVisible] = useState3(false);
1047
- const [dialogTypeName, setDialogTypeName] = useState3("");
1048
- const [dialogProps, setDialogProps] = useState3({});
1049
- const [dialogPropsRange, setDialogPropsRange] = useState3([0, 0]);
1050
- useEffect3(() => {
1051
- if (value !== void 0) {
1052
- setContent(value);
1053
- }
1054
- }, [value]);
1055
- const internalSetContent = useCallback5((newContent) => {
1056
- setContent(newContent);
1057
- onChange == null ? void 0 : onChange(newContent);
1058
- }, [onChange]);
1059
- useImperativeHandle(ref, () => ({
1060
- getContent: () => content
1061
- }), [content]);
1062
- const toggleFullscreen = useCallback5(() => {
1063
- if (document.fullscreenElement) {
1064
- document.exitFullscreen().catch((e) => {
1065
- console.error("\u9000\u51FA\u5168\u5C4F\u5931\u8D25:", e);
1066
- });
1067
- } else {
1068
- const el = document.documentElement;
1069
- const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1070
- if (requestMethod) {
1071
- requestMethod.call(el).catch((e) => {
1072
- console.error("\u8FDB\u5165\u5168\u5C4F\u5931\u8D25:", e);
1073
- });
1074
- }
1075
- }
1076
- }, []);
1077
- useEffect3(() => {
1078
- const handleFullscreenChange = () => {
1079
- setFullscreen(!!document.fullscreenElement);
1080
- };
1081
- document.addEventListener("fullscreenchange", handleFullscreenChange);
1082
- document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1083
- return () => {
1084
- document.removeEventListener("fullscreenchange", handleFullscreenChange);
1085
- document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1086
- };
1087
- }, []);
1088
- const extraToolbarItems = componentRegistry_default.getAll().map((schema) => {
1089
- var _a, _b;
1090
- const defaultProps = {};
1091
- for (const field of schema.fields) {
1092
- defaultProps[field.key] = (_a = field.defaultValue) != null ? _a : "";
1093
- }
1094
- const iconNode = (_b = schema.icon) != null ? _b : /* @__PURE__ */ jsx10(ReadOutlined, {});
1095
- return {
1096
- id: `container-${schema.name}`,
1097
- label: schema.label,
1098
- icon: () => iconNode,
1099
- handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1100
- };
1101
- });
1102
- const handleScrollReady = useCallback5((scrollDOM) => {
1103
- scrollDOMRef.current = scrollDOM;
1104
- }, []);
1105
- useEffect3(() => {
1106
- const source = scrollDOMRef.current;
1107
- const target = previewElRef.current;
1108
- if (!source || !target) return;
1109
- let rafId = null;
1110
- const listener = () => {
1111
- if (rafId) return;
1112
- rafId = requestAnimationFrame(() => {
1113
- const s = scrollDOMRef.current;
1114
- const t = previewElRef.current;
1115
- if (!s || !t) {
1116
- rafId = null;
1117
- return;
1118
- }
1119
- const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1120
- if (sourceScrollHeight <= 0) {
1121
- rafId = null;
1122
- return;
1123
- }
1124
- const ratio = s.scrollTop / sourceScrollHeight;
1125
- const targetScrollHeight = t.scrollHeight - t.clientHeight;
1126
- if (targetScrollHeight > 0) {
1127
- t.scrollTop = ratio * targetScrollHeight;
1128
- }
1129
- rafId = null;
1130
- });
1131
- };
1132
- source.addEventListener("scroll", listener, { passive: true });
1133
- return () => {
1134
- source.removeEventListener("scroll", listener);
1135
- if (rafId) cancelAnimationFrame(rafId);
1136
- };
1137
- }, [previewElRef.current]);
1138
- const handlePreviewRef = useCallback5((node) => {
1139
- previewElRef.current = node;
1140
- }, []);
1141
- const handleViewReady = useCallback5((view) => {
1142
- setEditorView(view);
1143
- }, []);
1144
- const handleEditContainer = useCallback5((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1145
- setDialogTypeName(typeName);
1146
- setDialogProps({ ...currentProps });
1147
- setDialogPropsRange([propsLineFrom, propsLineTo]);
1148
- setDialogVisible(true);
1149
- }, []);
1150
- const handleDialogSave = useCallback5((newProps) => {
1151
- setDialogVisible(false);
1152
- if (editorView) {
1153
- updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1154
- }
1155
- }, [editorView, dialogPropsRange]);
1156
- const handleDialogClose = useCallback5(() => {
1157
- setDialogVisible(false);
1158
- }, []);
1159
- return /* @__PURE__ */ jsxs6("div", { className: fullscreen ? "app fullscreen" : "app", children: [
1160
- /* @__PURE__ */ jsx10(
1161
- Toolbar,
1162
- {
1163
- editorView,
1164
- fullscreen,
1165
- onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1166
- extraItems: extraToolbarItems
1167
- }
1168
- ),
1169
- showPreview ? /* @__PURE__ */ jsx10(
1170
- SplitPanels,
1171
- {
1172
- leftPanel: /* @__PURE__ */ jsx10(
1173
- Editor,
1174
- {
1175
- value: content,
1176
- onChange: internalSetContent,
1177
- onScrollReady: handleScrollReady,
1178
- onViewReady: handleViewReady,
1179
- onEditContainer: handleEditContainer
1180
- }
1181
- ),
1182
- rightPanel: /* @__PURE__ */ jsx10(
1183
- Preview_default,
1184
- {
1185
- content,
1186
- ref: handlePreviewRef
1187
- }
1188
- )
1189
- }
1190
- ) : /* @__PURE__ */ jsx10(
1191
- Editor,
1192
- {
1193
- value: content,
1194
- onChange: internalSetContent,
1195
- onScrollReady: handleScrollReady,
1196
- onViewReady: handleViewReady,
1197
- onEditContainer: handleEditContainer
1198
- }
1199
- ),
1200
- /* @__PURE__ */ jsx10(
1201
- EditDialog,
1202
- {
1203
- visible: dialogVisible,
1204
- typeName: dialogTypeName,
1205
- currentProps: dialogProps,
1206
- onSave: handleDialogSave,
1207
- onClose: handleDialogClose
1208
- }
1209
- )
1210
- ] });
1331
+ //#endregion
1332
+ //#region src/hooks/useFullscreen.ts
1333
+ function useFullscreen() {
1334
+ const [fullscreen, setFullscreen] = useState(false);
1335
+ const toggleFullscreen = useCallback(() => {
1336
+ if (document.fullscreenElement) document.exitFullscreen().catch((e) => {
1337
+ console.error("退出全屏失败:", e);
1338
+ });
1339
+ else {
1340
+ const el = document.documentElement;
1341
+ const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1342
+ if (requestMethod) requestMethod.call(el).catch((e) => {
1343
+ console.error("进入全屏失败:", e);
1344
+ });
1345
+ }
1346
+ }, []);
1347
+ useEffect(() => {
1348
+ const handleFullscreenChange = () => {
1349
+ setFullscreen(!!document.fullscreenElement);
1350
+ };
1351
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1352
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1353
+ return () => {
1354
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1355
+ document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1356
+ };
1357
+ }, []);
1358
+ return {
1359
+ fullscreen,
1360
+ toggleFullscreen
1361
+ };
1362
+ }
1363
+ //#endregion
1364
+ //#region src/hooks/useScrollSync.ts
1365
+ /**
1366
+ * 双向滚动同步 Hook
1367
+ * 编辑器与预览区互相同步滚动位置(基于比例)
1368
+ * 使用 síncing 锁防止双向循环触发
1369
+ */
1370
+ function useScrollSync(sourceRef, targetRef) {
1371
+ const rafIdRef = useRef(null);
1372
+ const syncingRef = useRef(null);
1373
+ const syncScroll = (from, fromEl, toEl) => {
1374
+ if (rafIdRef.current) return;
1375
+ rafIdRef.current = requestAnimationFrame(() => {
1376
+ const fromScrollHeight = fromEl.scrollHeight - fromEl.clientHeight;
1377
+ if (fromScrollHeight <= 0) {
1378
+ rafIdRef.current = null;
1379
+ syncingRef.current = null;
1380
+ return;
1381
+ }
1382
+ const ratio = fromEl.scrollTop / fromScrollHeight;
1383
+ const toScrollHeight = toEl.scrollHeight - toEl.clientHeight;
1384
+ if (toScrollHeight > 0) {
1385
+ syncingRef.current = from;
1386
+ toEl.scrollTop = ratio * toScrollHeight;
1387
+ requestAnimationFrame(() => {
1388
+ syncingRef.current = null;
1389
+ });
1390
+ }
1391
+ rafIdRef.current = null;
1392
+ });
1393
+ };
1394
+ useEffect(() => {
1395
+ const source = sourceRef.current;
1396
+ const target = targetRef.current;
1397
+ if (!source || !target) return;
1398
+ const onSourceScroll = () => {
1399
+ if (syncingRef.current !== null) return;
1400
+ const s = sourceRef.current;
1401
+ const t = targetRef.current;
1402
+ syncScroll("source", s, t);
1403
+ };
1404
+ const onTargetScroll = () => {
1405
+ if (syncingRef.current !== null) return;
1406
+ const s = sourceRef.current;
1407
+ const t = targetRef.current;
1408
+ syncScroll("target", t, s);
1409
+ };
1410
+ source.addEventListener("scroll", onSourceScroll, { passive: true });
1411
+ target.addEventListener("scroll", onTargetScroll, { passive: true });
1412
+ return () => {
1413
+ source.removeEventListener("scroll", onSourceScroll);
1414
+ target.removeEventListener("scroll", onTargetScroll);
1415
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
1416
+ };
1417
+ }, [sourceRef, targetRef]);
1418
+ }
1419
+ //#endregion
1420
+ //#region src/MarkdownEditor.tsx
1421
+ var MarkdownEditor_default = forwardRef(function MarkdownEditor({ defaultValue = "", value, onChange, showPreview = true, fullscreen: fullscreenProp = true, previewOptions, uploadConfig }, ref) {
1422
+ const [content, setContent] = useState(value ?? defaultValue);
1423
+ const [editorView, setEditorView] = useState(null);
1424
+ const scrollDOMRef = useRef(null);
1425
+ const previewElRef = useRef(null);
1426
+ const { fullscreen, toggleFullscreen } = useFullscreen();
1427
+ useScrollSync(scrollDOMRef, previewElRef);
1428
+ const [dialogVisible, setDialogVisible] = useState(false);
1429
+ const [dialogTypeName, setDialogTypeName] = useState("");
1430
+ const [dialogProps, setDialogProps] = useState({});
1431
+ const [dialogPropsRange, setDialogPropsRange] = useState([0, 0]);
1432
+ useEffect(() => {
1433
+ if (value !== void 0) setContent(value);
1434
+ }, [value]);
1435
+ const internalSetContent = useCallback((newContent) => {
1436
+ setContent(newContent);
1437
+ onChange?.(newContent);
1438
+ }, [onChange]);
1439
+ useImperativeHandle(ref, () => ({ getContent: () => content }), [content]);
1440
+ const extraToolbarItems = componentRegistry.getAll().flatMap((schema) => {
1441
+ if (schema.wrapTags) return {
1442
+ id: `wrap-${schema.name}`,
1443
+ label: schema.label,
1444
+ icon: () => schema.icon ?? /* @__PURE__ */ jsx(ReadOutlined, {}),
1445
+ wrapTags: schema.wrapTags
1446
+ };
1447
+ const defaultProps = {};
1448
+ for (const field of schema?.fields ?? []) defaultProps[field.key] = field.defaultValue ?? "";
1449
+ return {
1450
+ id: `container-${schema.name}`,
1451
+ label: schema.label,
1452
+ icon: () => schema.icon ?? /* @__PURE__ */ jsx(ReadOutlined, {}),
1453
+ handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1454
+ };
1455
+ });
1456
+ const handleScrollReady = useCallback((scrollDOM) => {
1457
+ scrollDOMRef.current = scrollDOM;
1458
+ }, []);
1459
+ const handlePreviewRef = useCallback((node) => {
1460
+ previewElRef.current = node;
1461
+ }, []);
1462
+ const handleViewReady = useCallback((view) => {
1463
+ setEditorView(view);
1464
+ }, []);
1465
+ const handleEditContainer = useCallback((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1466
+ setDialogTypeName(typeName);
1467
+ setDialogProps({ ...currentProps });
1468
+ setDialogPropsRange([propsLineFrom, propsLineTo]);
1469
+ setDialogVisible(true);
1470
+ }, []);
1471
+ const handleDialogSave = useCallback((newProps) => {
1472
+ if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1473
+ }, [editorView, dialogPropsRange]);
1474
+ const handleDialogClose = useCallback(() => {
1475
+ setDialogVisible(false);
1476
+ }, []);
1477
+ return /* @__PURE__ */ jsxs("div", {
1478
+ className: fullscreen ? "markdown-editor-app fullscreen" : "markdown-editor-app",
1479
+ children: [
1480
+ /* @__PURE__ */ jsx(Toolbar, {
1481
+ editorView,
1482
+ fullscreen,
1483
+ onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1484
+ extraItems: extraToolbarItems,
1485
+ uploadConfig
1486
+ }),
1487
+ showPreview ? /* @__PURE__ */ jsx(SplitPanels, {
1488
+ leftPanel: /* @__PURE__ */ jsx(Editor, {
1489
+ value: content,
1490
+ onChange: internalSetContent,
1491
+ onScrollReady: handleScrollReady,
1492
+ onViewReady: handleViewReady,
1493
+ onEditContainer: handleEditContainer,
1494
+ uploadConfig,
1495
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1496
+ }),
1497
+ rightPanel: /* @__PURE__ */ jsx(Preview, {
1498
+ content,
1499
+ ref: handlePreviewRef,
1500
+ previewOptions
1501
+ })
1502
+ }) : /* @__PURE__ */ jsx(Editor, {
1503
+ value: content,
1504
+ onChange: internalSetContent,
1505
+ onScrollReady: handleScrollReady,
1506
+ onViewReady: handleViewReady,
1507
+ onEditContainer: handleEditContainer,
1508
+ uploadConfig,
1509
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1510
+ }),
1511
+ /* @__PURE__ */ jsx(EditDialog, {
1512
+ visible: dialogVisible,
1513
+ typeName: dialogTypeName,
1514
+ currentProps: dialogProps,
1515
+ onSave: handleDialogSave,
1516
+ onClose: handleDialogClose
1517
+ })
1518
+ ]
1519
+ });
1211
1520
  });
1212
-
1213
- // src/hooks/useRegisterToolbar.ts
1214
- import { useEffect as useEffect4 } from "react";
1521
+ //#endregion
1522
+ //#region src/hooks/useRegisterToolbar.ts
1523
+ /**
1524
+ * 注册自定义容器组件到工具栏。支持单个或批量注册。
1525
+ * 支持两种模式:
1526
+ *
1527
+ * 1. 容器模式(linkCard):name + fields + renderDialog
1528
+ * 插入 :::name 格式的容器,支持弹窗编辑属性
1529
+ *
1530
+ * 2. 标签包裹模式:name + wrapTags
1531
+ * 插入简单的标签包裹文本,如 <plugin-container>...</plugin-container>
1532
+ *
1533
+ * @example 容器模式 - 使用 fields 自动生成表单(最简)
1534
+ * ```tsx
1535
+ * const schema: ComponentSchema = {
1536
+ * name: 'alert',
1537
+ * label: '警告框',
1538
+ * icon: <AlertOutlined />,
1539
+ * fields: [
1540
+ * { key: 'type', name: '类型', componentType: 'select',
1541
+ * componentProps: { options: [{ value: 'price', label: '价格' }, { value: 'info', label: '信息' }] } },
1542
+ * { key: 'content', name: '内容', componentType: 'textarea',
1543
+ * componentProps: { rows: 3 } },
1544
+ * { key: 'enabled', name: '启用', componentType: 'switch' },
1545
+ * ],
1546
+ * }
1547
+ * // 支持的 componentType: 'input' | 'textarea' | 'number' | 'select' | 'switch' | 'slider' | 'rate' | 'color' | 'date' | 'dateRange'
1548
+ * ```
1549
+ *
1550
+ * @example 容器模式 - 使用 Form.Item 自动绑定(推荐)
1551
+ * ```tsx
1552
+ * const schema: ComponentSchema = {
1553
+ * name: 'alert',
1554
+ * label: '警告框',
1555
+ * icon: <AlertOutlined />,
1556
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1557
+ * renderDialog: ({ form }) => (
1558
+ * <Form form={form} layout="vertical">
1559
+ * <Form.Item name="level" label="级别">
1560
+ * <Input />
1561
+ * </Form.Item>
1562
+ * </Form>
1563
+ * ),
1564
+ * }
1565
+ *
1566
+ * function MyPage() {
1567
+ * useRegisterToolbar(schema)
1568
+ * return <MarkdownEditor />
1569
+ * }
1570
+ * ```
1571
+ *
1572
+ * @example 容器模式 - 手动控制
1573
+ * ```tsx
1574
+ * const schema: ComponentSchema = {
1575
+ * name: 'alert',
1576
+ * label: '警告框',
1577
+ * icon: <AlertOutlined />,
1578
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1579
+ * renderDialog: ({ props, onChange }) => (
1580
+ * <Input value={props.level} onChange={e => onChange('level', e.target.value)} />
1581
+ * ),
1582
+ * }
1583
+ * ```
1584
+ *
1585
+ * @example 容器模式 - 完全自定义弹窗
1586
+ * ```tsx
1587
+ * const schema: ComponentSchema = {
1588
+ * name: 'alert',
1589
+ * label: '警告框',
1590
+ * icon: <AlertOutlined />,
1591
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1592
+ * renderDialog: ({ props, onSave }) => (
1593
+ * <MyCustomEditor
1594
+ * value={props.level}
1595
+ * onSave={({ level }) => onSave({ level })}
1596
+ * />
1597
+ * ),
1598
+ * }
1599
+ * ```
1600
+ *
1601
+ * @example 标签包裹模式
1602
+ * ```tsx
1603
+ * function MyPage() {
1604
+ * useRegisterToolbar({
1605
+ * name: 'plugin-container',
1606
+ * label: '自定义容器',
1607
+ * icon: <AlertOutlined />,
1608
+ * wrapTags: ['<plugin-container>', '</plugin-container>'],
1609
+ * })
1610
+ * return <MarkdownEditor />
1611
+ * }
1612
+ * ```
1613
+ *
1614
+ * @example 批量注册
1615
+ * ```tsx
1616
+ * function MyPage() {
1617
+ * useRegisterToolbar([schema1, schema2, schema3])
1618
+ * return <MarkdownEditor />
1619
+ * }
1620
+ * ```
1621
+ */
1215
1622
  function useRegisterToolbar(schemas) {
1216
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1217
- useEffect4(() => {
1218
- schemaList.forEach((schema) => {
1219
- componentRegistry_default.register(schema.name, schema);
1220
- });
1221
- return () => {
1222
- schemaList.forEach((schema) => {
1223
- componentRegistry_default.unregister(schema.name);
1224
- });
1225
- };
1226
- }, [schemaList]);
1623
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1624
+ useEffect(() => {
1625
+ schemaList.forEach((schema) => {
1626
+ componentRegistry.register(schema.name, schema);
1627
+ });
1628
+ return () => {
1629
+ schemaList.forEach((schema) => {
1630
+ componentRegistry.unregister(schema.name);
1631
+ });
1632
+ };
1633
+ }, [schemaList]);
1227
1634
  }
1228
- export {
1229
- MarkdownEditor_default as MarkdownEditor,
1230
- useRegisterToolbar
1231
- };
1635
+ //#endregion
1636
+ export { MarkdownEditor_default as MarkdownEditor, useRegisterToolbar };
1637
+
1232
1638
  //# sourceMappingURL=index.js.map