remons-markdown-editor 1.0.0-beta.1 → 1.0.0-beta.11

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 (33) 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/Panel/SplitPanels.d.ts +7 -0
  8. package/dist/components/Preview/index.d.ts +10 -0
  9. package/dist/components/Toolbar/buttons.d.ts +34 -0
  10. package/dist/components/Toolbar/config.d.ts +6 -0
  11. package/dist/components/Toolbar/hooks/useToolbar.d.ts +14 -0
  12. package/dist/components/Toolbar/index.d.ts +13 -0
  13. package/dist/components/Toolbar/renderers/TablePicker/TableDropdown.d.ts +5 -0
  14. package/dist/components/Toolbar/renderers/TablePicker/index.d.ts +9 -0
  15. package/dist/components/Toolbar/renderers/ToolbarItemRenderer.d.ts +7 -0
  16. package/dist/hooks/useFullscreen.d.ts +4 -0
  17. package/dist/hooks/useRegisterToolbar.d.ts +101 -0
  18. package/dist/hooks/useScrollSync.d.ts +6 -0
  19. package/dist/index.cjs +1480 -1198
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.css +185 -113
  22. package/dist/index.d.ts +5 -90
  23. package/dist/index.js +1456 -1178
  24. package/dist/index.js.map +1 -1
  25. package/dist/main.d.ts +0 -0
  26. package/dist/registry/componentRegistry.d.ts +34 -0
  27. package/dist/types/index.d.ts +148 -0
  28. package/dist/utils/parseContainer.d.ts +18 -0
  29. package/dist/utils/writeContainer.d.ts +16 -0
  30. package/package.json +79 -81
  31. package/readme.md +309 -81
  32. package/dist/index.css.map +0 -1
  33. package/dist/index.d.cts +0 -90
package/dist/index.js CHANGED
@@ -1,1227 +1,1505 @@
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 { BlockOutlined, BoldOutlined, CodeOutlined, DownOutlined, FieldStringOutlined, FontSizeOutlined, FullscreenExitOutlined, FullscreenOutlined, ItalicOutlined, LinkOutlined, MinusOutlined, OrderedListOutlined, PictureOutlined, ReadOutlined, StrikethroughOutlined, TableOutlined, UnderlineOutlined, UnorderedListOutlined } 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;
311
- }
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" });
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;
451
235
  }
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: { click: (event, view) => {
251
+ const target = event.target;
252
+ if (target.classList.contains("container-edit-btn")) {
253
+ if (target.__typeName) {
254
+ const containers = findContainers(view.state.doc.toString());
255
+ const buttonPos = view.posAtDOM(target);
256
+ for (const container of containers) {
257
+ const headerLine = view.state.doc.lineAt(container.startPos);
258
+ if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
259
+ const callback = view.state.facet(containerEditCallback);
260
+ if (callback) callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
261
+ return true;
262
+ }
263
+ }
264
+ }
265
+ return false;
266
+ }
267
+ return false;
268
+ } }
463
269
  });
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
270
+ //#endregion
271
+ //#region src/components/Toolbar/buttons.tsx
272
+ /**
273
+ * 在光标位置插入文本
274
+ */
504
275
  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
- }
276
+ const { from, to } = view.state.selection.main;
277
+ const selectedText = view.state.doc.sliceString(from, to);
278
+ const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
279
+ view.dispatch({ changes: {
280
+ from,
281
+ to,
282
+ insert
283
+ } });
284
+ if (!selectedText && placeholder) view.dispatch({ selection: { anchor: from + before.length } });
512
285
  }
286
+ /**
287
+ * 在当前行行首插入文本
288
+ */
513
289
  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 } });
290
+ const { from } = view.state.selection.main;
291
+ const line = view.state.doc.lineAt(from);
292
+ view.dispatch({ changes: {
293
+ from: line.from,
294
+ to: line.from,
295
+ insert: text
296
+ } });
297
+ view.dispatch({ selection: { anchor: line.from + text.length } });
298
+ }
299
+ /**
300
+ * 上传图片
301
+ * @param file - 要上传的文件
302
+ * @param uploadConfig - 上传配置
303
+ */
304
+ async function uploadFileToServer(file, uploadConfig) {
305
+ if (uploadConfig.onUploadImage) return uploadConfig.onUploadImage(file);
306
+ if (uploadConfig.uploadUrl) {
307
+ const formData = new FormData();
308
+ formData.append("file", file);
309
+ const response = await fetch(uploadConfig.uploadUrl, {
310
+ method: "POST",
311
+ body: formData
312
+ });
313
+ if (!response.ok) return null;
314
+ const result = await response.json();
315
+ if (uploadConfig.onGetUploadUrl) return uploadConfig.onGetUploadUrl(result);
316
+ return result.data?.path || null;
317
+ }
318
+ return null;
518
319
  }
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;
320
+ /**
321
+ * 判断是否配置了图片上传
322
+ */
323
+ function isUploadConfigured(uploadConfig) {
324
+ return !!(uploadConfig?.uploadUrl || uploadConfig?.onUploadImage);
533
325
  }
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();
326
+ /**
327
+ * 图片上传:创建隐藏的 file input 触发选择,上传后插入 markdown 图片语法
328
+ * @param view - CodeMirror 编辑器实例
329
+ * @param uploadConfig - 上传配置(包含 uploadUrl 或 onUploadImage)
330
+ */
331
+ function uploadImage(view, uploadConfig) {
332
+ const input = document.createElement("input");
333
+ input.type = "file";
334
+ input.accept = "image/*";
335
+ input.style.display = "none";
336
+ input.addEventListener("change", async () => {
337
+ if (!input.files || input.files.length === 0) return;
338
+ const file = input.files[0];
339
+ const { from, to } = view.state.selection.main;
340
+ const selectedText = view.state.doc.sliceString(from, to) || "描述";
341
+ const url = await uploadFileToServer(file, uploadConfig);
342
+ if (url) {
343
+ const insert = `![${selectedText}](${url})`;
344
+ view.dispatch({ changes: {
345
+ from,
346
+ to,
347
+ insert
348
+ } });
349
+ }
350
+ input.remove();
351
+ });
352
+ document.body.appendChild(input);
353
+ input.click();
553
354
  }
355
+ /**
356
+ * 用前缀和后缀包裹选中文本
357
+ */
554
358
  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}` } });
359
+ const { from, to, empty } = view.state.selection.main;
360
+ if (empty) return;
361
+ const selected = view.state.doc.sliceString(from, to);
362
+ view.dispatch({ changes: {
363
+ from,
364
+ to,
365
+ insert: `${prefix}${selected}${suffix}`
366
+ } });
559
367
  }
368
+ /**
369
+ * 切换包裹:选中的文本已包含标签则移除,否则添加
370
+ */
560
371
  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);
372
+ const { from, to, empty } = view.state.selection.main;
373
+ const doc = view.state.doc;
374
+ if (!empty) {
375
+ const selected = doc.sliceString(from, to);
376
+ if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
377
+ const inner = selected.slice(openTag.length, -closeTag.length);
378
+ view.dispatch({ changes: {
379
+ from,
380
+ to,
381
+ insert: inner
382
+ } });
383
+ return;
384
+ }
385
+ }
386
+ {
387
+ const cursorPos = empty ? from : to;
388
+ const line = doc.lineAt(cursorPos);
389
+ const lineText = doc.sliceString(line.from, line.to);
390
+ const openIdx = lineText.indexOf(openTag);
391
+ const closeIdx = lineText.lastIndexOf(closeTag);
392
+ if (openIdx >= 0 && closeIdx > openIdx) {
393
+ const absOpen = line.from + openIdx;
394
+ const absClose = line.from + closeIdx;
395
+ if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
396
+ const inner = doc.sliceString(absOpen + openTag.length, absClose);
397
+ view.dispatch({ changes: {
398
+ from: absOpen,
399
+ to: absClose + closeTag.length,
400
+ insert: inner
401
+ } });
402
+ return;
403
+ }
404
+ }
405
+ }
406
+ wrapSelection(view, openTag, closeTag);
594
407
  }
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
- ];
408
+ //#endregion
409
+ //#region src/components/Editor/codemirrorExtensions.ts
410
+ /** 图片上传配置 facet */
411
+ var uploadConfigFacet = Facet.define({ combine: (values) => values[values.length - 1] });
412
+ /** 未配置上传时的提示回调 facet */
413
+ var messageFacet = Facet.define({ combine: (values) => values[values.length - 1] });
414
+ /** 粘贴事件处理:拦截图片并上传 */
415
+ var pasteHandler = EditorView$1.domEventHandlers({ paste: (event, view) => {
416
+ const files = event.clipboardData?.files;
417
+ if (!files) return false;
418
+ const imageFiles = Array.from(files).filter((f) => f.type.startsWith("image/"));
419
+ if (imageFiles.length === 0) return false;
420
+ event.preventDefault();
421
+ const uploadConfig = view.state.facet(uploadConfigFacet);
422
+ const showMessage = view.state.facet(messageFacet);
423
+ if (!isUploadConfigured(uploadConfig)) {
424
+ showMessage?.();
425
+ return true;
426
+ }
427
+ const { from, to } = view.state.selection.main;
428
+ let offset = 0;
429
+ for (const file of imageFiles) uploadFileToServer(file, uploadConfig).then((url) => {
430
+ if (url) {
431
+ const insert = `![${file.name.replace(/\.[^.]+$/, "")}](${url})`;
432
+ view.dispatch({ changes: {
433
+ from: from + offset,
434
+ to: to + offset,
435
+ insert
436
+ } });
437
+ offset += insert.length;
438
+ }
439
+ });
440
+ return true;
441
+ } });
442
+ var getCodeMirrorExtensions = (opts) => {
443
+ const extensions = [
444
+ markdown(),
445
+ oneDark,
446
+ keymap.of([...defaultKeymap, indentWithTab]),
447
+ EditorView$1.lineWrapping,
448
+ pasteHandler,
449
+ EditorView$1.baseTheme({
450
+ ".cm-scroller": {
451
+ height: "100%",
452
+ overflow: "auto"
453
+ },
454
+ ".container-widget-header": {
455
+ display: "inline",
456
+ fontSize: "inherit",
457
+ fontFamily: "inherit"
458
+ },
459
+ ".container-widget-text": {
460
+ fontWeight: "bold",
461
+ color: "#79c0ff"
462
+ },
463
+ ".container-widget-props": {
464
+ display: "inline",
465
+ fontSize: "inherit",
466
+ fontFamily: "inherit",
467
+ color: "#a5d6ff"
468
+ },
469
+ ".container-edit-btn": {
470
+ display: "inline-block",
471
+ padding: "0 4px",
472
+ fontSize: "12px",
473
+ color: "#58a6ff",
474
+ background: "transparent",
475
+ border: "none",
476
+ borderRadius: "4px",
477
+ cursor: "pointer",
478
+ marginRight: "4px",
479
+ verticalAlign: "middle"
480
+ },
481
+ ".container-edit-btn:hover": { background: "rgba(88, 166, 255, 0.16)" }
482
+ })
483
+ ];
484
+ extensions.push(containerWidgetPlugin);
485
+ if (opts?.onEditContainer) extensions.push(containerEditCallback.of(opts.onEditContainer));
486
+ if (opts?.uploadConfig) extensions.push(uploadConfigFacet.of(opts.uploadConfig));
487
+ if (opts?.onMessageNoUploadConfig) extensions.push(messageFacet.of(opts.onMessageNoUploadConfig));
488
+ return extensions;
489
+ };
490
+ //#endregion
491
+ //#region src/components/Editor/index.tsx
492
+ function Editor({ value, onChange, onScrollReady, onViewReady, onEditContainer, uploadConfig, onMessageNoUploadConfig }) {
493
+ const editorRef = useRef(null);
494
+ const viewRef = useRef(null);
495
+ useEffect(() => {
496
+ if (!editorRef.current) return;
497
+ const extensions = [
498
+ basicSetup,
499
+ ...getCodeMirrorExtensions({
500
+ onEditContainer,
501
+ uploadConfig,
502
+ onMessageNoUploadConfig
503
+ }),
504
+ EditorView.updateListener.of((update) => {
505
+ if (update.docChanged) onChange(update.state.doc.toString());
506
+ })
507
+ ];
508
+ const state = EditorState.create({
509
+ doc: value,
510
+ extensions
511
+ });
512
+ viewRef.current = new EditorView({
513
+ state,
514
+ parent: editorRef.current
515
+ });
516
+ if (onScrollReady && viewRef.current) onScrollReady(viewRef.current.scrollDOM);
517
+ if (onViewReady) onViewReady(viewRef.current);
518
+ return () => {
519
+ viewRef.current?.destroy();
520
+ };
521
+ }, []);
522
+ useEffect(() => {
523
+ if (viewRef.current && viewRef.current.state.doc.toString() !== value) viewRef.current.dispatch({ changes: {
524
+ from: 0,
525
+ to: viewRef.current.state.doc.length,
526
+ insert: value
527
+ } });
528
+ }, [value]);
529
+ return /* @__PURE__ */ jsx("div", {
530
+ ref: editorRef,
531
+ className: "editor"
532
+ });
648
533
  }
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 };
534
+ //#endregion
535
+ //#region src/components/Preview/index.tsx
536
+ var Preview = forwardRef(({ content, previewOptions }, ref) => {
537
+ useEffect(() => {
538
+ initHighlighter({
539
+ ...languagesCommon,
540
+ ...previewOptions?.languages ? previewOptions?.languages : {}
541
+ });
542
+ }, [previewOptions?.languages]);
543
+ return /* @__PURE__ */ jsx("div", {
544
+ ref,
545
+ className: "preview",
546
+ children: /* @__PURE__ */ jsx(RenderMarkdown, {
547
+ content,
548
+ ...previewOptions
549
+ })
550
+ });
551
+ });
552
+ Preview.displayName = "Preview";
553
+ //#endregion
554
+ //#region src/components/Panel/SplitPanels.tsx
555
+ var { Panel } = Splitter;
556
+ function SplitPanels({ leftPanel, rightPanel }) {
557
+ return /* @__PURE__ */ jsxs(Splitter, {
558
+ className: "split-panels",
559
+ children: [/* @__PURE__ */ jsx(Panel, {
560
+ defaultSize: "50%",
561
+ min: "20%",
562
+ max: "80%",
563
+ children: leftPanel
564
+ }), /* @__PURE__ */ jsx(Panel, { children: rightPanel })]
565
+ });
663
566
  }
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";
567
+ //#endregion
568
+ //#region src/components/Toolbar/config.ts
569
+ /**
570
+ * 创建工具栏配置
571
+ * @param options - 配置选项
572
+ */
573
+ function createToolbarConfig(options) {
574
+ const hasUploadConfig = isUploadConfigured(options);
575
+ return [
576
+ {
577
+ id: "heading",
578
+ icon: FontSizeOutlined,
579
+ label: "标题",
580
+ children: [
581
+ {
582
+ id: "heading1",
583
+ label: "标题 1",
584
+ icon: FontSizeOutlined,
585
+ handler: (v) => insertAtLineStart(v, "# ")
586
+ },
587
+ {
588
+ id: "heading2",
589
+ label: "标题 2",
590
+ icon: FontSizeOutlined,
591
+ handler: (v) => insertAtLineStart(v, "## ")
592
+ },
593
+ {
594
+ id: "heading3",
595
+ label: "标题 3",
596
+ icon: FontSizeOutlined,
597
+ handler: (v) => insertAtLineStart(v, "### ")
598
+ },
599
+ {
600
+ id: "heading4",
601
+ label: "标题 4",
602
+ icon: FontSizeOutlined,
603
+ handler: (v) => insertAtLineStart(v, "#### ")
604
+ },
605
+ {
606
+ id: "heading5",
607
+ label: "标题 5",
608
+ icon: FontSizeOutlined,
609
+ handler: (v) => insertAtLineStart(v, "##### ")
610
+ },
611
+ {
612
+ id: "heading6",
613
+ label: "标题 6",
614
+ icon: FontSizeOutlined,
615
+ handler: (v) => insertAtLineStart(v, "###### ")
616
+ }
617
+ ]
618
+ },
619
+ { separatorBefore: true },
620
+ {
621
+ id: "bold",
622
+ label: "加粗",
623
+ icon: BoldOutlined,
624
+ handler: (v) => wrapSelection(v, "**", "**")
625
+ },
626
+ {
627
+ id: "italic",
628
+ label: "斜体",
629
+ icon: ItalicOutlined,
630
+ handler: (v) => wrapSelection(v, "*", "*")
631
+ },
632
+ {
633
+ id: "strikethrough",
634
+ label: "删除线",
635
+ icon: StrikethroughOutlined,
636
+ handler: (v) => wrapSelection(v, "~~", "~~")
637
+ },
638
+ {
639
+ id: "underline",
640
+ label: "下划线",
641
+ icon: UnderlineOutlined,
642
+ handler: (v) => wrapSelection(v, "<u>", "</u>")
643
+ },
644
+ { separatorBefore: true },
645
+ {
646
+ id: "ul",
647
+ label: "无序列表",
648
+ icon: UnorderedListOutlined,
649
+ handler: (v) => insertAtLineStart(v, "- ")
650
+ },
651
+ {
652
+ id: "ol",
653
+ label: "有序列表",
654
+ icon: OrderedListOutlined,
655
+ handler: (v) => insertAtLineStart(v, "1. ")
656
+ },
657
+ { separatorBefore: true },
658
+ {
659
+ id: "link",
660
+ label: "链接",
661
+ icon: LinkOutlined,
662
+ handler: (v) => insertAtCursor(v, "[", "](url)", "文本")
663
+ },
664
+ {
665
+ id: "image",
666
+ label: hasUploadConfig ? "图片" : "请配置图片上传地址",
667
+ icon: PictureOutlined,
668
+ disabled: !hasUploadConfig,
669
+ handler: hasUploadConfig ? (v) => uploadImage(v, {
670
+ uploadUrl: options?.uploadUrl,
671
+ onUploadImage: options?.onUploadImage
672
+ }) : void 0
673
+ },
674
+ { separatorBefore: true },
675
+ {
676
+ id: "code",
677
+ label: "行内代码",
678
+ icon: FieldStringOutlined,
679
+ handler: (v) => wrapSelection(v, "`", "`")
680
+ },
681
+ {
682
+ id: "codeblock",
683
+ label: "代码块",
684
+ icon: CodeOutlined,
685
+ handler: (v) => {
686
+ const { from, to } = v.state.selection.main;
687
+ const selected = v.state.doc.sliceString(from, to);
688
+ const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
689
+ v.dispatch({ changes: {
690
+ from,
691
+ to,
692
+ insert
693
+ } });
694
+ }
695
+ },
696
+ {
697
+ id: "quote",
698
+ label: "引用",
699
+ icon: BlockOutlined,
700
+ handler: (v) => insertAtLineStart(v, "> ")
701
+ },
702
+ {
703
+ id: "hr",
704
+ label: "分割线",
705
+ icon: MinusOutlined,
706
+ handler: (v) => {
707
+ const { from } = v.state.selection.main;
708
+ v.dispatch({ changes: {
709
+ from,
710
+ insert: "\n---\n"
711
+ } });
712
+ }
713
+ }
714
+ ];
715
+ }
716
+ //#endregion
717
+ //#region src/components/Toolbar/hooks/useToolbar.ts
718
+ function useToolbar(editorView, options) {
719
+ const { extraItems, uploadConfig } = options || {};
720
+ return {
721
+ config: useMemo(() => {
722
+ return createToolbarConfig(uploadConfig);
723
+ }, [uploadConfig]),
724
+ extraItemsConfig: useMemo(() => {
725
+ return extraItems || [];
726
+ }, [extraItems]),
727
+ handleClick: useCallback((button) => {
728
+ if (!editorView) return;
729
+ if (button.wrapTags) {
730
+ const [openTag, closeTag] = button.wrapTags;
731
+ toggleWrap(editorView, openTag, closeTag);
732
+ return;
733
+ }
734
+ if (button.handler) {
735
+ editorView.focus();
736
+ button.handler(editorView, editorView.state);
737
+ }
738
+ }, [editorView])
739
+ };
740
+ }
741
+ //#endregion
742
+ //#region src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
669
743
  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
- ] });
744
+ const Icon = item.icon;
745
+ if (item.children && item.children.length > 0) return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
746
+ className: "toolbar-group",
747
+ children: /* @__PURE__ */ jsx(Tooltip, {
748
+ title: item.label,
749
+ children: /* @__PURE__ */ jsx(Dropdown, {
750
+ menu: { items: item.children.map((child) => ({
751
+ type: "item",
752
+ key: child.id ?? "",
753
+ label: /* @__PURE__ */ jsxs("span", {
754
+ onClick: () => onClick(child),
755
+ children: [
756
+ child.icon && (() => {
757
+ const ChildIcon = child.icon;
758
+ return /* @__PURE__ */ jsx(ChildIcon, {});
759
+ })(),
760
+ " ",
761
+ child.label
762
+ ]
763
+ })
764
+ })) },
765
+ placement: "bottomLeft",
766
+ children: /* @__PURE__ */ jsxs(Button, {
767
+ className: "toolbar-button",
768
+ type: "text",
769
+ size: "small",
770
+ children: [/* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsx(DownOutlined, { style: {
771
+ marginLeft: 2,
772
+ fontSize: 10
773
+ } })]
774
+ })
775
+ })
776
+ })
777
+ })] });
778
+ const isDisabled = item.disabled;
779
+ return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
780
+ className: "toolbar-group",
781
+ children: /* @__PURE__ */ jsx(Tooltip, {
782
+ title: item.label,
783
+ children: /* @__PURE__ */ jsx(Button, {
784
+ id: item.id,
785
+ className: "toolbar-button",
786
+ type: "text",
787
+ size: "small",
788
+ disabled: isDisabled,
789
+ onClick: () => !isDisabled && onClick(item),
790
+ children: /* @__PURE__ */ jsx(Icon, {})
791
+ })
792
+ })
793
+ })] });
718
794
  }
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;
795
+ var index_module_default = {
796
+ container: "index-module__container___l6BT-",
797
+ grid: "index-module__grid___tCZ-r",
798
+ cell: "index-module__cell___vTDQz",
799
+ cellActive: "index-module__cellActive___0EYvU",
800
+ hint: "index-module__hint___dTObk",
801
+ label: "index-module__label___2CmK1"
802
+ };
803
+ //#endregion
804
+ //#region src/components/Toolbar/renderers/TablePicker/index.tsx
805
+ var MAX_ROW = 8;
806
+ var MAX_COL = 8;
736
807
  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
- );
808
+ const [hoverSize, setHoverSize] = useState({
809
+ row: 0,
810
+ col: 0
811
+ });
812
+ const [confirmedSize, setConfirmedSize] = useState({
813
+ row: 0,
814
+ col: 0
815
+ });
816
+ const maxColRef = useRef(0);
817
+ const prevRowRef = useRef(0);
818
+ const timerIdRef = useRef(null);
819
+ const refreshTimerRef = useRef(null);
820
+ const commitRef = useRef(null);
821
+ const clearTimer = useCallback(() => {
822
+ if (timerIdRef.current) {
823
+ clearTimeout(timerIdRef.current);
824
+ timerIdRef.current = null;
825
+ }
826
+ }, []);
827
+ const clearFlash = useCallback(() => {
828
+ if (refreshTimerRef.current) {
829
+ clearInterval(refreshTimerRef.current);
830
+ refreshTimerRef.current = null;
831
+ }
832
+ }, []);
833
+ useEffect(() => () => {
834
+ if (timerIdRef.current) clearTimeout(timerIdRef.current);
835
+ if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
836
+ }, []);
837
+ /**
838
+ * 扩展选中区域至 (col, row)
839
+ */
840
+ function extend(col, row) {
841
+ if (row < prevRowRef.current || col < maxColRef.current) maxColRef.current = 0;
842
+ if (col > maxColRef.current) maxColRef.current = col;
843
+ setHoverSize({
844
+ row,
845
+ col
846
+ });
847
+ setConfirmedSize({
848
+ row,
849
+ col: maxColRef.current
850
+ });
851
+ if (row === 1) commitRef.current = {
852
+ cols: maxColRef.current,
853
+ rows: 1
854
+ };
855
+ }
856
+ /**
857
+ * 跳跃动画效果
858
+ */
859
+ function flash(col, dir, limit, finalCallback) {
860
+ let current = dir === 1 ? 1 : limit;
861
+ const step = dir === 1 ? 1 : -1;
862
+ refreshTimerRef.current = setInterval(() => {
863
+ setHoverSize({
864
+ row: current,
865
+ col
866
+ });
867
+ if (current > confirmedSize.row || dir === -1) maxColRef.current = Math.min(MAX_COL, col);
868
+ current += step;
869
+ if (dir === 1 && current > limit || dir === -1 && current < 1) {
870
+ clearInterval(refreshTimerRef.current);
871
+ refreshTimerRef.current = null;
872
+ const finalRow = current - step;
873
+ setHoverSize({
874
+ row: finalRow,
875
+ col
876
+ });
877
+ finalCallback(col, finalRow);
878
+ }
879
+ }, 50);
880
+ }
881
+ /**
882
+ * 核心逻辑:鼠标进入格子
883
+ */
884
+ function onCellEnterCheck(index) {
885
+ clearTimer();
886
+ clearFlash();
887
+ const row = Math.floor(index / MAX_COL) + 1;
888
+ const col = index % MAX_COL + 1;
889
+ const prevRow = prevRowRef.current;
890
+ const isJump = Math.abs(row - prevRow) > 1;
891
+ const isDown = row > prevRow;
892
+ prevRowRef.current = row;
893
+ if (!isJump) extend(col, row);
894
+ else if (isDown) {
895
+ if (col > maxColRef.current || maxColRef.current >= MAX_COL) flash(col, 1, Math.min(row, MAX_ROW), (c, r) => {
896
+ setConfirmedSize({
897
+ row: r,
898
+ col: c
899
+ });
900
+ });
901
+ else extend(col, row);
902
+ } else {
903
+ maxColRef.current = 0;
904
+ flash(col, -1, row, (c, r) => {
905
+ setConfirmedSize({
906
+ row: r,
907
+ col: c
908
+ });
909
+ if (r === 1) {
910
+ commitRef.current = {
911
+ cols: Math.min(MAX_COL, col),
912
+ rows: 1
913
+ };
914
+ setConfirmedSize({
915
+ row: 1,
916
+ col: Math.min(MAX_COL, col)
917
+ });
918
+ }
919
+ });
920
+ }
921
+ }
922
+ function onMouseDown() {
923
+ const size = commitRef.current || {
924
+ cols: maxColRef.current,
925
+ rows: confirmedSize.row
926
+ };
927
+ if (size.rows > 0 && size.cols > 0) onSelect(size);
928
+ }
929
+ return /* @__PURE__ */ jsxs("div", {
930
+ className: index_module_default.container,
931
+ onMouseDown,
932
+ children: [/* @__PURE__ */ jsx("div", {
933
+ className: index_module_default.grid,
934
+ style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
935
+ children: Array.from({ length: 64 }).map((_, i) => {
936
+ const row = Math.floor(i / MAX_COL) + 1;
937
+ const col = i % MAX_COL + 1;
938
+ const isSelected = row <= hoverSize.row && col <= maxColRef.current;
939
+ return /* @__PURE__ */ jsx("div", {
940
+ className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
941
+ onMouseEnter: () => onCellEnterCheck(i)
942
+ }, i);
943
+ })
944
+ }), /* @__PURE__ */ jsxs("div", {
945
+ className: index_module_default.label,
946
+ children: [
947
+ hoverSize.row,
948
+ " x ",
949
+ maxColRef.current || 1
950
+ ]
951
+ })]
952
+ });
867
953
  }
868
-
869
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
870
- import { jsx as jsx7 } from "react/jsx-runtime";
954
+ //#endregion
955
+ //#region src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
871
956
  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
- );
957
+ const [open, setOpen] = React.useState(false);
958
+ const openCountRef = useRef(0);
959
+ const handleTableSelect = useCallback((rows, cols) => {
960
+ if (!editorView || rows <= 0 || cols <= 0) return;
961
+ editorView.focus();
962
+ const headerCells = Array(cols).fill(null).map((_, i) => ` 列${i + 1} `).join("|");
963
+ const separator = Array(cols).fill(" --- ").join("|");
964
+ const dataCell = Array(cols).fill(" 内容 ").join("|");
965
+ const table = `\n|${headerCells}|\n|${separator}|\n${Array(rows).fill(`|${dataCell}|`).join("\n")}\n`;
966
+ const { from } = editorView.state.selection.main;
967
+ editorView.dispatch({ changes: {
968
+ from,
969
+ insert: table
970
+ } });
971
+ }, [editorView]);
972
+ return /* @__PURE__ */ jsx(Dropdown, {
973
+ trigger: ["click"],
974
+ open,
975
+ onOpenChange: setOpen,
976
+ popupRender: () => {
977
+ openCountRef.current++;
978
+ return /* @__PURE__ */ jsx("div", {
979
+ style: { padding: 4 },
980
+ onClick: (e) => e.stopPropagation(),
981
+ children: /* @__PURE__ */ jsx(TablePicker, { onSelect: (size) => {
982
+ handleTableSelect(size.rows, size.cols);
983
+ setOpen(false);
984
+ } }, openCountRef.current)
985
+ });
986
+ },
987
+ placement: "bottomLeft",
988
+ children: /* @__PURE__ */ jsx(Button, {
989
+ className: "toolbar-button",
990
+ type: "text",
991
+ size: "small",
992
+ title: "表格",
993
+ children: /* @__PURE__ */ jsx(TableOutlined, {})
994
+ })
995
+ });
912
996
  }
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
- ] });
997
+ //#endregion
998
+ //#region src/components/Toolbar/index.tsx
999
+ function Toolbar({ editorView, fullscreen = true, onToggleFullscreen, extraItems, uploadConfig }) {
1000
+ const { config, extraItemsConfig, handleClick } = useToolbar(editorView, {
1001
+ extraItems,
1002
+ uploadConfig
1003
+ });
1004
+ const renderFullscreen = useCallback(() => {
1005
+ if (!onToggleFullscreen) return null;
1006
+ return /* @__PURE__ */ jsx(Button, {
1007
+ className: "toolbar-button",
1008
+ type: "text",
1009
+ size: "small",
1010
+ title: fullscreen ? "退出全屏" : "全屏",
1011
+ onClick: () => {
1012
+ onToggleFullscreen();
1013
+ },
1014
+ children: /* @__PURE__ */ jsx(fullscreen ? FullscreenExitOutlined : FullscreenOutlined, {})
1015
+ });
1016
+ }, [fullscreen, onToggleFullscreen]);
1017
+ function isSeparator(item) {
1018
+ return !!(!item.id && item.separatorBefore);
1019
+ }
1020
+ return /* @__PURE__ */ jsxs("div", {
1021
+ className: "toolbar",
1022
+ children: [
1023
+ config.map((item, idx) => {
1024
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `sep-${idx}`);
1025
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1026
+ item,
1027
+ onClick: handleClick
1028
+ }, item.id);
1029
+ }),
1030
+ /* @__PURE__ */ jsx(TableDropdown, { editorView }),
1031
+ extraItemsConfig?.map((item, idx) => {
1032
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `extra-sep-${idx}`);
1033
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1034
+ item,
1035
+ onClick: handleClick
1036
+ }, item.id);
1037
+ }),
1038
+ renderFullscreen()
1039
+ ]
1040
+ });
959
1041
  }
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";
1042
+ //#endregion
1043
+ //#region src/components/Dialog/EditDialog.tsx
1044
+ /**
1045
+ * 组件类型到 antd 表单组件的映射
1046
+ */
1047
+ var FormComponents = {
1048
+ input: Input,
1049
+ textarea: Input.TextArea,
1050
+ number: InputNumber,
1051
+ select: Select,
1052
+ switch: Switch,
1053
+ slider: Slider,
1054
+ rate: Rate,
1055
+ color: ColorPicker,
1056
+ date: DatePicker,
1057
+ dateRange: DatePicker.RangePicker
1058
+ };
1059
+ /**
1060
+ * 渲染对应类型的 antd 表单组件
1061
+ */
1062
+ function renderFormItemComponent(field) {
1063
+ const props = field.componentProps || {};
1064
+ const Component = field.componentType ? FormComponents[field.componentType] : Input;
1065
+ return /* @__PURE__ */ jsx(Component, { ...props });
1066
+ }
1067
+ /**
1068
+ * 根据 fields 自动生成 Form.Item 列表
1069
+ */
1070
+ function renderAutoForm(form, fields) {
1071
+ if (!fields || fields.length === 0) return null;
1072
+ return /* @__PURE__ */ jsx(Form, {
1073
+ form,
1074
+ layout: "vertical",
1075
+ children: fields.map((field) => {
1076
+ if (!field.componentType) return null;
1077
+ const formItemProps = {
1078
+ name: field.key,
1079
+ label: field.name || field.key,
1080
+ valuePropName: field.componentType === "switch" ? "checked" : void 0,
1081
+ ...field.componentProps
1082
+ };
1083
+ return /* @__PURE__ */ jsx(Form.Item, {
1084
+ ...formItemProps,
1085
+ children: renderFormItemComponent(field)
1086
+ }, field.key);
1087
+ })
1088
+ });
1089
+ }
1090
+ /**
1091
+ * 属性编辑弹窗组件
1092
+ * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
1093
+ */
965
1094
  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
- );
1095
+ const schema = componentRegistry.get(typeName);
1096
+ const [formProps, setFormProps] = useState({});
1097
+ const [form] = Form.useForm();
1098
+ const mergedProps = useMemo(() => {
1099
+ if (!schema) return currentProps;
1100
+ const defaults = {};
1101
+ for (const field of schema.fields || []) defaults[field.key] = field.defaultValue ?? "";
1102
+ return {
1103
+ ...defaults,
1104
+ ...currentProps
1105
+ };
1106
+ }, [schema, currentProps]);
1107
+ useEffect(() => {
1108
+ if (visible) {
1109
+ form.setFieldsValue(mergedProps);
1110
+ setFormProps({});
1111
+ }
1112
+ }, [
1113
+ visible,
1114
+ mergedProps,
1115
+ form
1116
+ ]);
1117
+ const handleDialogChange = (key, value) => {
1118
+ setFormProps((prev) => ({
1119
+ ...prev,
1120
+ [key]: value
1121
+ }));
1122
+ };
1123
+ const handleOk = async () => {
1124
+ let finalProps = visible ? {
1125
+ ...mergedProps,
1126
+ ...formProps
1127
+ } : mergedProps;
1128
+ try {
1129
+ const formValues = await form.validateFields();
1130
+ finalProps = {
1131
+ ...finalProps,
1132
+ ...formValues
1133
+ };
1134
+ } catch {}
1135
+ onSave(finalProps);
1136
+ };
1137
+ const handleClose = () => {
1138
+ setFormProps({});
1139
+ onClose();
1140
+ };
1141
+ if (!schema) return null;
1142
+ const context = {
1143
+ props: mergedProps,
1144
+ form,
1145
+ onChange: handleDialogChange,
1146
+ onSave
1147
+ };
1148
+ const renderDialogContent = () => {
1149
+ if (schema.renderDialog) return schema.renderDialog(context);
1150
+ if (schema.fields?.some((f) => f.componentType) && schema.fields) return renderAutoForm(form, schema.fields);
1151
+ return null;
1152
+ };
1153
+ return /* @__PURE__ */ jsx(Modal, {
1154
+ title: `${schema.label} 属性编辑`,
1155
+ open: visible,
1156
+ onOk: handleOk,
1157
+ onCancel: handleClose,
1158
+ okText: "保存",
1159
+ cancelText: "取消",
1160
+ children: renderDialogContent()
1161
+ });
1004
1162
  }
1005
-
1006
- // src/utils/writeContainer.ts
1163
+ //#endregion
1164
+ //#region src/utils/writeContainer.ts
1165
+ /**
1166
+ * 从属性对象生成 JSON 属性行。
1167
+ * 格式: { key: 'value', key2: 'value2' }
1168
+ */
1007
1169
  function writeContainerProps(props) {
1008
- const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1009
- return entries ? `{ ${entries} }` : "{}";
1170
+ const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1171
+ return entries ? `{ ${entries} }` : "{}";
1010
1172
  }
1173
+ /**
1174
+ * 更新编辑器文档中的容器属性。
1175
+ * 替换 JSON 属性行(::: 打开行之后的一行)为新属性。
1176
+ */
1011
1177
  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
- });
1178
+ const newPropsLine = writeContainerProps(newProps);
1179
+ view.dispatch({ changes: {
1180
+ from: propsLineFrom,
1181
+ to: propsLineTo,
1182
+ insert: newPropsLine
1183
+ } });
1020
1184
  }
1185
+ /**
1186
+ * 在当前光标位置插入新容器。
1187
+ * 创建 :::type 打开行 + JSON 属性 + ::: 关闭行的结构。
1188
+ */
1021
1189
  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 } });
1190
+ const { from } = view.state.selection.main;
1191
+ const container = `:::${typeName}\n${writeContainerProps(defaultProps)}\n:::\n`;
1192
+ view.dispatch({ changes: {
1193
+ from,
1194
+ insert: container
1195
+ } });
1029
1196
  }
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
- ] });
1197
+ //#endregion
1198
+ //#region src/hooks/useFullscreen.ts
1199
+ function useFullscreen() {
1200
+ const [fullscreen, setFullscreen] = useState(false);
1201
+ const toggleFullscreen = useCallback(() => {
1202
+ if (document.fullscreenElement) document.exitFullscreen().catch((e) => {
1203
+ console.error("退出全屏失败:", e);
1204
+ });
1205
+ else {
1206
+ const el = document.documentElement;
1207
+ const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1208
+ if (requestMethod) requestMethod.call(el).catch((e) => {
1209
+ console.error("进入全屏失败:", e);
1210
+ });
1211
+ }
1212
+ }, []);
1213
+ useEffect(() => {
1214
+ const handleFullscreenChange = () => {
1215
+ setFullscreen(!!document.fullscreenElement);
1216
+ };
1217
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1218
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1219
+ return () => {
1220
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1221
+ document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1222
+ };
1223
+ }, []);
1224
+ return {
1225
+ fullscreen,
1226
+ toggleFullscreen
1227
+ };
1228
+ }
1229
+ //#endregion
1230
+ //#region src/hooks/useScrollSync.ts
1231
+ /**
1232
+ * 双向滚动同步 Hook
1233
+ * 编辑器与预览区互相同步滚动位置(基于比例)
1234
+ * 使用 síncing 锁防止双向循环触发
1235
+ */
1236
+ function useScrollSync(sourceRef, targetRef) {
1237
+ const rafIdRef = useRef(null);
1238
+ const syncingRef = useRef(null);
1239
+ const syncScroll = (from, fromEl, toEl) => {
1240
+ if (rafIdRef.current) return;
1241
+ rafIdRef.current = requestAnimationFrame(() => {
1242
+ const fromScrollHeight = fromEl.scrollHeight - fromEl.clientHeight;
1243
+ if (fromScrollHeight <= 0) {
1244
+ rafIdRef.current = null;
1245
+ syncingRef.current = null;
1246
+ return;
1247
+ }
1248
+ const ratio = fromEl.scrollTop / fromScrollHeight;
1249
+ const toScrollHeight = toEl.scrollHeight - toEl.clientHeight;
1250
+ if (toScrollHeight > 0) {
1251
+ syncingRef.current = from;
1252
+ toEl.scrollTop = ratio * toScrollHeight;
1253
+ requestAnimationFrame(() => {
1254
+ syncingRef.current = null;
1255
+ });
1256
+ }
1257
+ rafIdRef.current = null;
1258
+ });
1259
+ };
1260
+ useEffect(() => {
1261
+ const source = sourceRef.current;
1262
+ const target = targetRef.current;
1263
+ if (!source || !target) return;
1264
+ const onSourceScroll = () => {
1265
+ if (syncingRef.current !== null) return;
1266
+ const s = sourceRef.current;
1267
+ const t = targetRef.current;
1268
+ syncScroll("source", s, t);
1269
+ };
1270
+ const onTargetScroll = () => {
1271
+ if (syncingRef.current !== null) return;
1272
+ const s = sourceRef.current;
1273
+ const t = targetRef.current;
1274
+ syncScroll("target", t, s);
1275
+ };
1276
+ source.addEventListener("scroll", onSourceScroll, { passive: true });
1277
+ target.addEventListener("scroll", onTargetScroll, { passive: true });
1278
+ return () => {
1279
+ source.removeEventListener("scroll", onSourceScroll);
1280
+ target.removeEventListener("scroll", onTargetScroll);
1281
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
1282
+ };
1283
+ }, [sourceRef, targetRef]);
1284
+ }
1285
+ //#endregion
1286
+ //#region src/MarkdownEditor.tsx
1287
+ var MarkdownEditor_default = forwardRef(function MarkdownEditor({ defaultValue = "", value, onChange, showPreview = true, fullscreen: fullscreenProp = true, previewOptions, uploadConfig }, ref) {
1288
+ const [content, setContent] = useState(value ?? defaultValue);
1289
+ const [editorView, setEditorView] = useState(null);
1290
+ const scrollDOMRef = useRef(null);
1291
+ const previewElRef = useRef(null);
1292
+ const { fullscreen, toggleFullscreen } = useFullscreen();
1293
+ useScrollSync(scrollDOMRef, previewElRef);
1294
+ const [dialogVisible, setDialogVisible] = useState(false);
1295
+ const [dialogTypeName, setDialogTypeName] = useState("");
1296
+ const [dialogProps, setDialogProps] = useState({});
1297
+ const [dialogPropsRange, setDialogPropsRange] = useState([0, 0]);
1298
+ useEffect(() => {
1299
+ if (value !== void 0) setContent(value);
1300
+ }, [value]);
1301
+ const internalSetContent = useCallback((newContent) => {
1302
+ setContent(newContent);
1303
+ onChange?.(newContent);
1304
+ }, [onChange]);
1305
+ useImperativeHandle(ref, () => ({ getContent: () => content }), [content]);
1306
+ const extraToolbarItems = componentRegistry.getAll().flatMap((schema) => {
1307
+ if (schema.wrapTags) return {
1308
+ id: `wrap-${schema.name}`,
1309
+ label: schema.label,
1310
+ icon: () => schema.icon ?? /* @__PURE__ */ jsx(ReadOutlined, {}),
1311
+ wrapTags: schema.wrapTags
1312
+ };
1313
+ const defaultProps = {};
1314
+ for (const field of schema?.fields ?? []) defaultProps[field.key] = field.defaultValue ?? "";
1315
+ return {
1316
+ id: `container-${schema.name}`,
1317
+ label: schema.label,
1318
+ icon: () => schema.icon ?? /* @__PURE__ */ jsx(ReadOutlined, {}),
1319
+ handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1320
+ };
1321
+ });
1322
+ const handleScrollReady = useCallback((scrollDOM) => {
1323
+ scrollDOMRef.current = scrollDOM;
1324
+ }, []);
1325
+ const handlePreviewRef = useCallback((node) => {
1326
+ previewElRef.current = node;
1327
+ }, []);
1328
+ const handleViewReady = useCallback((view) => {
1329
+ setEditorView(view);
1330
+ }, []);
1331
+ const handleEditContainer = useCallback((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1332
+ setDialogTypeName(typeName);
1333
+ setDialogProps({ ...currentProps });
1334
+ setDialogPropsRange([propsLineFrom, propsLineTo]);
1335
+ setDialogVisible(true);
1336
+ }, []);
1337
+ const handleDialogSave = useCallback((newProps) => {
1338
+ setDialogVisible(false);
1339
+ if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1340
+ }, [editorView, dialogPropsRange]);
1341
+ const handleDialogClose = useCallback(() => {
1342
+ setDialogVisible(false);
1343
+ }, []);
1344
+ return /* @__PURE__ */ jsxs("div", {
1345
+ className: fullscreen ? "markdown-editor-app fullscreen" : "markdown-editor-app",
1346
+ children: [
1347
+ /* @__PURE__ */ jsx(Toolbar, {
1348
+ editorView,
1349
+ fullscreen,
1350
+ onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1351
+ extraItems: extraToolbarItems,
1352
+ uploadConfig
1353
+ }),
1354
+ showPreview ? /* @__PURE__ */ jsx(SplitPanels, {
1355
+ leftPanel: /* @__PURE__ */ jsx(Editor, {
1356
+ value: content,
1357
+ onChange: internalSetContent,
1358
+ onScrollReady: handleScrollReady,
1359
+ onViewReady: handleViewReady,
1360
+ onEditContainer: handleEditContainer,
1361
+ uploadConfig,
1362
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1363
+ }),
1364
+ rightPanel: /* @__PURE__ */ jsx(Preview, {
1365
+ content,
1366
+ ref: handlePreviewRef,
1367
+ previewOptions
1368
+ })
1369
+ }) : /* @__PURE__ */ jsx(Editor, {
1370
+ value: content,
1371
+ onChange: internalSetContent,
1372
+ onScrollReady: handleScrollReady,
1373
+ onViewReady: handleViewReady,
1374
+ onEditContainer: handleEditContainer,
1375
+ uploadConfig,
1376
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1377
+ }),
1378
+ /* @__PURE__ */ jsx(EditDialog, {
1379
+ visible: dialogVisible,
1380
+ typeName: dialogTypeName,
1381
+ currentProps: dialogProps,
1382
+ onSave: handleDialogSave,
1383
+ onClose: handleDialogClose
1384
+ })
1385
+ ]
1386
+ });
1211
1387
  });
1212
-
1213
- // src/hooks/useRegisterToolbar.ts
1214
- import { useEffect as useEffect4 } from "react";
1215
- function useRegisterToolbar(schema) {
1216
- useEffect4(() => {
1217
- componentRegistry_default.register(schema.name, schema);
1218
- return () => {
1219
- componentRegistry_default.unregister(schema.name);
1220
- };
1221
- }, [schema]);
1388
+ //#endregion
1389
+ //#region src/hooks/useRegisterToolbar.ts
1390
+ /**
1391
+ * 注册自定义容器组件到工具栏。支持单个或批量注册。
1392
+ * 支持两种模式:
1393
+ *
1394
+ * 1. 容器模式(linkCard):name + fields + renderDialog
1395
+ * 插入 :::name 格式的容器,支持弹窗编辑属性
1396
+ *
1397
+ * 2. 标签包裹模式:name + wrapTags
1398
+ * 插入简单的标签包裹文本,如 <plugin-container>...</plugin-container>
1399
+ *
1400
+ * @example 容器模式 - 使用 fields 自动生成表单(最简)
1401
+ * ```tsx
1402
+ * const schema: ComponentSchema = {
1403
+ * name: 'alert',
1404
+ * label: '警告框',
1405
+ * icon: <AlertOutlined />,
1406
+ * fields: [
1407
+ * { key: 'type', name: '类型', componentType: 'select',
1408
+ * componentProps: { options: [{ value: 'price', label: '价格' }, { value: 'info', label: '信息' }] } },
1409
+ * { key: 'content', name: '内容', componentType: 'textarea',
1410
+ * componentProps: { rows: 3 } },
1411
+ * { key: 'enabled', name: '启用', componentType: 'switch' },
1412
+ * ],
1413
+ * }
1414
+ * // 支持的 componentType: 'input' | 'textarea' | 'number' | 'select' | 'switch' | 'slider' | 'rate' | 'color' | 'date' | 'dateRange'
1415
+ * ```
1416
+ *
1417
+ * @example 容器模式 - 使用 Form.Item 自动绑定(推荐)
1418
+ * ```tsx
1419
+ * const schema: ComponentSchema = {
1420
+ * name: 'alert',
1421
+ * label: '警告框',
1422
+ * icon: <AlertOutlined />,
1423
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1424
+ * renderDialog: ({ form }) => (
1425
+ * <Form form={form} layout="vertical">
1426
+ * <Form.Item name="level" label="级别">
1427
+ * <Input />
1428
+ * </Form.Item>
1429
+ * </Form>
1430
+ * ),
1431
+ * }
1432
+ *
1433
+ * function MyPage() {
1434
+ * useRegisterToolbar(schema)
1435
+ * return <MarkdownEditor />
1436
+ * }
1437
+ * ```
1438
+ *
1439
+ * @example 容器模式 - 手动控制
1440
+ * ```tsx
1441
+ * const schema: ComponentSchema = {
1442
+ * name: 'alert',
1443
+ * label: '警告框',
1444
+ * icon: <AlertOutlined />,
1445
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1446
+ * renderDialog: ({ props, onChange }) => (
1447
+ * <Input value={props.level} onChange={e => onChange('level', e.target.value)} />
1448
+ * ),
1449
+ * }
1450
+ * ```
1451
+ *
1452
+ * @example 容器模式 - 完全自定义弹窗
1453
+ * ```tsx
1454
+ * const schema: ComponentSchema = {
1455
+ * name: 'alert',
1456
+ * label: '警告框',
1457
+ * icon: <AlertOutlined />,
1458
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1459
+ * renderDialog: ({ props, onSave }) => (
1460
+ * <MyCustomEditor
1461
+ * value={props.level}
1462
+ * onSave={({ level }) => onSave({ level })}
1463
+ * />
1464
+ * ),
1465
+ * }
1466
+ * ```
1467
+ *
1468
+ * @example 标签包裹模式
1469
+ * ```tsx
1470
+ * function MyPage() {
1471
+ * useRegisterToolbar({
1472
+ * name: 'plugin-container',
1473
+ * label: '自定义容器',
1474
+ * icon: <AlertOutlined />,
1475
+ * wrapTags: ['<plugin-container>', '</plugin-container>'],
1476
+ * })
1477
+ * return <MarkdownEditor />
1478
+ * }
1479
+ * ```
1480
+ *
1481
+ * @example 批量注册
1482
+ * ```tsx
1483
+ * function MyPage() {
1484
+ * useRegisterToolbar([schema1, schema2, schema3])
1485
+ * return <MarkdownEditor />
1486
+ * }
1487
+ * ```
1488
+ */
1489
+ function useRegisterToolbar(schemas) {
1490
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1491
+ useEffect(() => {
1492
+ schemaList.forEach((schema) => {
1493
+ componentRegistry.register(schema.name, schema);
1494
+ });
1495
+ return () => {
1496
+ schemaList.forEach((schema) => {
1497
+ componentRegistry.unregister(schema.name);
1498
+ });
1499
+ };
1500
+ }, [schemaList]);
1222
1501
  }
1223
- export {
1224
- MarkdownEditor_default as MarkdownEditor,
1225
- useRegisterToolbar
1226
- };
1502
+ //#endregion
1503
+ export { MarkdownEditor_default as MarkdownEditor, useRegisterToolbar };
1504
+
1227
1505
  //# sourceMappingURL=index.js.map