remons-markdown-editor 1.0.0-beta.3 → 1.0.0-beta.5

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 +13 -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 +51 -0
  18. package/dist/hooks/useScrollSync.d.ts +5 -0
  19. package/dist/index.cjs +1325 -1207
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.css +108 -88
  22. package/dist/index.d.ts +5 -105
  23. package/dist/index.js +1301 -1187
  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 +76 -0
  28. package/dist/utils/parseContainer.d.ts +18 -0
  29. package/dist/utils/writeContainer.d.ts +16 -0
  30. package/package.json +7 -7
  31. package/readme.md +103 -4
  32. package/dist/index.css.map +0 -1
  33. package/dist/index.d.cts +0 -105
package/dist/index.js CHANGED
@@ -1,1237 +1,1351 @@
1
- // src/MarkdownEditor.tsx
2
- import { useState as useState3, useRef as useRef4, useCallback as useCallback5, useEffect as useEffect4, 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, Dropdown, Modal, Splitter, 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, useEffect as useEffect2 } from "react";
455
- import RenderMarkdown, { languagesCommon, initHighlighter } from "remons-render-markdown";
456
- import "remons-render-markdown/dist/index.css";
457
- import { jsx as jsx3 } from "react/jsx-runtime";
458
- var Preview = forwardRef(({ content, previewOptions }, ref) => {
459
- useEffect2(() => {
460
- initHighlighter({
461
- ...languagesCommon,
462
- ...(previewOptions == null ? void 0 : previewOptions.languages) ? previewOptions == null ? void 0 : previewOptions.languages : {}
463
- });
464
- }, [previewOptions == null ? void 0 : previewOptions.languages]);
465
- return /* @__PURE__ */ jsx3("div", { ref, className: "preview", children: /* @__PURE__ */ jsx3(RenderMarkdown, { content, ...previewOptions }) });
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
+ } }
466
269
  });
467
- Preview.displayName = "Preview";
468
- var Preview_default = Preview;
469
-
470
- // src/components/Panel/SplitPanels.tsx
471
- import { Splitter } from "antd";
472
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
473
- function SplitPanels({ leftPanel, rightPanel }) {
474
- return /* @__PURE__ */ jsxs2(Splitter, { className: "split-panels", children: [
475
- /* @__PURE__ */ jsx4(Splitter.Panel, { defaultSize: "50%", min: "20%", max: "80%", children: leftPanel }),
476
- /* @__PURE__ */ jsx4(Splitter.Panel, { children: rightPanel })
477
- ] });
478
- }
479
-
480
- // src/components/Toolbar/index.tsx
481
- import { useCallback as useCallback4 } from "react";
482
- import { Button as Button3 } from "antd";
483
- import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons";
484
-
485
- // src/components/Toolbar/hooks/useToolbar.ts
486
- import { useCallback, useMemo } from "react";
487
-
488
- // src/components/Toolbar/config.ts
489
- import {
490
- BoldOutlined,
491
- ItalicOutlined,
492
- StrikethroughOutlined,
493
- UnderlineOutlined,
494
- UnorderedListOutlined,
495
- OrderedListOutlined,
496
- LinkOutlined,
497
- PictureOutlined,
498
- CodeOutlined,
499
- BlockOutlined,
500
- MinusOutlined,
501
- FieldStringOutlined,
502
- FontSizeOutlined,
503
- TagsOutlined
504
- } from "@ant-design/icons";
505
-
506
- // src/components/Toolbar/buttons.tsx
270
+ //#endregion
271
+ //#region src/components/Toolbar/buttons.tsx
272
+ /**
273
+ * 在光标位置插入文本
274
+ */
507
275
  function insertAtCursor(view, before, after = "", placeholder = "") {
508
- const { from, to } = view.state.selection.main;
509
- const selectedText = view.state.doc.sliceString(from, to);
510
- const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
511
- view.dispatch({ changes: { from, to, insert } });
512
- if (!selectedText && placeholder) {
513
- view.dispatch({ selection: { anchor: from + before.length } });
514
- }
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 } });
515
285
  }
286
+ /**
287
+ * 在当前行行首插入文本
288
+ */
516
289
  function insertAtLineStart(view, text) {
517
- const { from } = view.state.selection.main;
518
- const line = view.state.doc.lineAt(from);
519
- view.dispatch({ changes: { from: line.from, to: line.from, insert: text } });
520
- 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 } });
521
298
  }
522
- async function uploadFileToServer(file) {
523
- var _a;
524
- const formData = new FormData();
525
- formData.append("file", file);
526
- const response = await fetch("https://remons.cn:3008/content/uploadMarkdownImg", {
527
- method: "POST",
528
- body: formData
529
- });
530
- if (!response.ok) return null;
531
- const result = await response.json();
532
- if (result.code === 200 && result.success && ((_a = result.data) == null ? void 0 : _a.path)) {
533
- return `https://remons.cn:3008${result.data.path}`;
534
- }
535
- return null;
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;
536
319
  }
537
- function uploadImage(view) {
538
- const input = document.createElement("input");
539
- input.type = "file";
540
- input.accept = "image/*";
541
- input.style.display = "none";
542
- input.addEventListener("change", async () => {
543
- if (!input.files || input.files.length === 0) return;
544
- const file = input.files[0];
545
- const { from, to } = view.state.selection.main;
546
- const selectedText = view.state.doc.sliceString(from, to) || "\u63CF\u8FF0";
547
- const url = await uploadFileToServer(file);
548
- if (url) {
549
- const insert = `![${selectedText}](${url})`;
550
- view.dispatch({ changes: { from, to, insert } });
551
- }
552
- input.remove();
553
- });
554
- document.body.appendChild(input);
555
- input.click();
320
+ /**
321
+ * 判断是否配置了图片上传
322
+ */
323
+ function isUploadConfigured(uploadConfig) {
324
+ return !!(uploadConfig?.uploadUrl || uploadConfig?.onUploadImage);
556
325
  }
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();
354
+ }
355
+ /**
356
+ * 用前缀和后缀包裹选中文本
357
+ */
557
358
  function wrapSelection(view, prefix, suffix) {
558
- const { from, to, empty } = view.state.selection.main;
559
- if (empty) return;
560
- const selected = view.state.doc.sliceString(from, to);
561
- 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
+ } });
562
367
  }
368
+ /**
369
+ * 切换包裹:选中的文本已包含标签则移除,否则添加
370
+ */
563
371
  function toggleWrap(view, openTag, closeTag) {
564
- const { from, to, empty } = view.state.selection.main;
565
- const doc = view.state.doc;
566
- if (!empty) {
567
- const selected = doc.sliceString(from, to);
568
- if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
569
- const inner = selected.slice(openTag.length, -closeTag.length);
570
- view.dispatch({ changes: { from, to, insert: inner } });
571
- return;
572
- }
573
- }
574
- {
575
- const cursorPos = empty ? from : to;
576
- const line = doc.lineAt(cursorPos);
577
- const lineText = doc.sliceString(line.from, line.to);
578
- const openIdx = lineText.indexOf(openTag);
579
- const closeIdx = lineText.lastIndexOf(closeTag);
580
- if (openIdx >= 0 && closeIdx > openIdx) {
581
- const absOpen = line.from + openIdx;
582
- const absClose = line.from + closeIdx;
583
- if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
584
- const inner = doc.sliceString(absOpen + openTag.length, absClose);
585
- view.dispatch({
586
- changes: {
587
- from: absOpen,
588
- to: absClose + closeTag.length,
589
- insert: inner
590
- }
591
- });
592
- return;
593
- }
594
- }
595
- }
596
- 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);
597
407
  }
598
-
599
- // src/components/Toolbar/config.ts
600
- function createToolbarConfig() {
601
- return [
602
- {
603
- id: "heading",
604
- icon: FontSizeOutlined,
605
- label: "\u6807\u9898",
606
- children: [
607
- { id: "heading1", label: "\u6807\u9898 1", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "# ") },
608
- { id: "heading2", label: "\u6807\u9898 2", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "## ") },
609
- { id: "heading3", label: "\u6807\u9898 3", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "### ") },
610
- { id: "heading4", label: "\u6807\u9898 4", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "#### ") },
611
- { id: "heading5", label: "\u6807\u9898 5", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "##### ") },
612
- { id: "heading6", label: "\u6807\u9898 6", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "###### ") }
613
- ]
614
- },
615
- { separatorBefore: true },
616
- { id: "bold", label: "\u52A0\u7C97", icon: BoldOutlined, handler: (v) => wrapSelection(v, "**", "**") },
617
- { id: "italic", label: "\u659C\u4F53", icon: ItalicOutlined, handler: (v) => wrapSelection(v, "*", "*") },
618
- { id: "strikethrough", label: "\u5220\u9664\u7EBF", icon: StrikethroughOutlined, handler: (v) => wrapSelection(v, "~~", "~~") },
619
- { id: "underline", label: "\u4E0B\u5212\u7EBF", icon: UnderlineOutlined, handler: (v) => wrapSelection(v, "<u>", "</u>") },
620
- { separatorBefore: true },
621
- { id: "ul", label: "\u65E0\u5E8F\u5217\u8868", icon: UnorderedListOutlined, handler: (v) => insertAtLineStart(v, "- ") },
622
- { id: "ol", label: "\u6709\u5E8F\u5217\u8868", icon: OrderedListOutlined, handler: (v) => insertAtLineStart(v, "1. ") },
623
- { separatorBefore: true },
624
- { id: "link", label: "\u94FE\u63A5", icon: LinkOutlined, handler: (v) => insertAtCursor(v, "[", "](url)", "\u6587\u672C") },
625
- { id: "image", label: "\u56FE\u7247", icon: PictureOutlined, handler: (v) => uploadImage(v) },
626
- { separatorBefore: true },
627
- { id: "code", label: "\u884C\u5185\u4EE3\u7801", icon: FieldStringOutlined, handler: (v) => wrapSelection(v, "`", "`") },
628
- {
629
- id: "codeblock",
630
- label: "\u4EE3\u7801\u5757",
631
- icon: CodeOutlined,
632
- handler: (v) => {
633
- const { from, to } = v.state.selection.main;
634
- const selected = v.state.doc.sliceString(from, to);
635
- const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
636
- v.dispatch({ changes: { from, to, insert } });
637
- }
638
- },
639
- { id: "quote", label: "\u5F15\u7528", icon: BlockOutlined, handler: (v) => insertAtLineStart(v, "> ") },
640
- { id: "textContainer", label: "\u6587\u5B57\u5BB9\u5668", icon: TagsOutlined, handler: (v) => toggleWrap(v, "<text-container>", "</text-container>") },
641
- {
642
- id: "hr",
643
- label: "\u5206\u5272\u7EBF",
644
- icon: MinusOutlined,
645
- handler: (v) => {
646
- const { from } = v.state.selection.main;
647
- v.dispatch({ changes: { from, insert: "\n---\n" } });
648
- }
649
- }
650
- ];
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
+ });
651
533
  }
652
-
653
- // src/components/Toolbar/hooks/useToolbar.ts
654
- function useToolbar(editorView, extraItems) {
655
- const config = useMemo(() => {
656
- const base = createToolbarConfig();
657
- return extraItems ? [...base, { separatorBefore: true }, ...extraItems] : base;
658
- }, [extraItems]);
659
- const handleClick = useCallback((button) => {
660
- if (button.handler && editorView) {
661
- editorView.focus();
662
- button.handler(editorView, editorView.state);
663
- }
664
- }, [editorView]);
665
- 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
+ function SplitPanels({ leftPanel, rightPanel }) {
556
+ return /* @__PURE__ */ jsxs(Splitter, {
557
+ className: "split-panels",
558
+ children: [/* @__PURE__ */ jsx(Splitter.Panel, {
559
+ defaultSize: "50%",
560
+ min: "20%",
561
+ max: "80%",
562
+ children: leftPanel
563
+ }), /* @__PURE__ */ jsx(Splitter.Panel, { children: rightPanel })]
564
+ });
666
565
  }
667
-
668
- // src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
669
- import { Button, Dropdown } from "antd";
670
- import { DownOutlined } from "@ant-design/icons";
671
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
566
+ //#endregion
567
+ //#region src/components/Toolbar/config.ts
568
+ /**
569
+ * 创建工具栏配置
570
+ * @param options - 配置选项
571
+ */
572
+ function createToolbarConfig(options) {
573
+ const hasUploadConfig = isUploadConfigured(options);
574
+ return [
575
+ {
576
+ id: "heading",
577
+ icon: FontSizeOutlined,
578
+ label: "标题",
579
+ children: [
580
+ {
581
+ id: "heading1",
582
+ label: "标题 1",
583
+ icon: FontSizeOutlined,
584
+ handler: (v) => insertAtLineStart(v, "# ")
585
+ },
586
+ {
587
+ id: "heading2",
588
+ label: "标题 2",
589
+ icon: FontSizeOutlined,
590
+ handler: (v) => insertAtLineStart(v, "## ")
591
+ },
592
+ {
593
+ id: "heading3",
594
+ label: "标题 3",
595
+ icon: FontSizeOutlined,
596
+ handler: (v) => insertAtLineStart(v, "### ")
597
+ },
598
+ {
599
+ id: "heading4",
600
+ label: "标题 4",
601
+ icon: FontSizeOutlined,
602
+ handler: (v) => insertAtLineStart(v, "#### ")
603
+ },
604
+ {
605
+ id: "heading5",
606
+ label: "标题 5",
607
+ icon: FontSizeOutlined,
608
+ handler: (v) => insertAtLineStart(v, "##### ")
609
+ },
610
+ {
611
+ id: "heading6",
612
+ label: "标题 6",
613
+ icon: FontSizeOutlined,
614
+ handler: (v) => insertAtLineStart(v, "###### ")
615
+ }
616
+ ]
617
+ },
618
+ { separatorBefore: true },
619
+ {
620
+ id: "bold",
621
+ label: "加粗",
622
+ icon: BoldOutlined,
623
+ handler: (v) => wrapSelection(v, "**", "**")
624
+ },
625
+ {
626
+ id: "italic",
627
+ label: "斜体",
628
+ icon: ItalicOutlined,
629
+ handler: (v) => wrapSelection(v, "*", "*")
630
+ },
631
+ {
632
+ id: "strikethrough",
633
+ label: "删除线",
634
+ icon: StrikethroughOutlined,
635
+ handler: (v) => wrapSelection(v, "~~", "~~")
636
+ },
637
+ {
638
+ id: "underline",
639
+ label: "下划线",
640
+ icon: UnderlineOutlined,
641
+ handler: (v) => wrapSelection(v, "<u>", "</u>")
642
+ },
643
+ { separatorBefore: true },
644
+ {
645
+ id: "ul",
646
+ label: "无序列表",
647
+ icon: UnorderedListOutlined,
648
+ handler: (v) => insertAtLineStart(v, "- ")
649
+ },
650
+ {
651
+ id: "ol",
652
+ label: "有序列表",
653
+ icon: OrderedListOutlined,
654
+ handler: (v) => insertAtLineStart(v, "1. ")
655
+ },
656
+ { separatorBefore: true },
657
+ {
658
+ id: "link",
659
+ label: "链接",
660
+ icon: LinkOutlined,
661
+ handler: (v) => insertAtCursor(v, "[", "](url)", "文本")
662
+ },
663
+ {
664
+ id: "image",
665
+ label: hasUploadConfig ? "图片" : "请配置图片上传地址",
666
+ icon: PictureOutlined,
667
+ disabled: !hasUploadConfig,
668
+ handler: hasUploadConfig ? (v) => uploadImage(v, {
669
+ uploadUrl: options?.uploadUrl,
670
+ onUploadImage: options?.onUploadImage
671
+ }) : void 0
672
+ },
673
+ { separatorBefore: true },
674
+ {
675
+ id: "code",
676
+ label: "行内代码",
677
+ icon: FieldStringOutlined,
678
+ handler: (v) => wrapSelection(v, "`", "`")
679
+ },
680
+ {
681
+ id: "codeblock",
682
+ label: "代码块",
683
+ icon: CodeOutlined,
684
+ handler: (v) => {
685
+ const { from, to } = v.state.selection.main;
686
+ const selected = v.state.doc.sliceString(from, to);
687
+ const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
688
+ v.dispatch({ changes: {
689
+ from,
690
+ to,
691
+ insert
692
+ } });
693
+ }
694
+ },
695
+ {
696
+ id: "quote",
697
+ label: "引用",
698
+ icon: BlockOutlined,
699
+ handler: (v) => insertAtLineStart(v, "> ")
700
+ },
701
+ {
702
+ id: "hr",
703
+ label: "分割线",
704
+ icon: MinusOutlined,
705
+ handler: (v) => {
706
+ const { from } = v.state.selection.main;
707
+ v.dispatch({ changes: {
708
+ from,
709
+ insert: "\n---\n"
710
+ } });
711
+ }
712
+ }
713
+ ];
714
+ }
715
+ //#endregion
716
+ //#region src/components/Toolbar/hooks/useToolbar.ts
717
+ function useToolbar(editorView, options) {
718
+ const { extraItems, uploadConfig } = options || {};
719
+ return {
720
+ config: useMemo(() => {
721
+ const base = createToolbarConfig(uploadConfig);
722
+ return extraItems ? [...base, ...extraItems] : base;
723
+ }, [extraItems, uploadConfig]),
724
+ handleClick: useCallback((button) => {
725
+ if (!editorView) return;
726
+ if (button.wrapTags) {
727
+ const [openTag, closeTag] = button.wrapTags;
728
+ toggleWrap(editorView, openTag, closeTag);
729
+ return;
730
+ }
731
+ if (button.handler) {
732
+ editorView.focus();
733
+ button.handler(editorView, editorView.state);
734
+ }
735
+ }, [editorView])
736
+ };
737
+ }
738
+ //#endregion
739
+ //#region src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
672
740
  function ToolbarItemRenderer({ item, onClick }) {
673
- const Icon = item.icon;
674
- if (item.children && item.children.length > 0) {
675
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
676
- item.separatorBefore && /* @__PURE__ */ jsx5("div", { className: "toolbar-separator" }),
677
- /* @__PURE__ */ jsx5("div", { className: "toolbar-group", children: /* @__PURE__ */ jsx5(
678
- Dropdown,
679
- {
680
- menu: {
681
- items: item.children.map((child) => {
682
- var _a;
683
- return {
684
- type: "item",
685
- key: (_a = child.id) != null ? _a : "",
686
- label: /* @__PURE__ */ jsxs3("span", { onClick: () => onClick(child), children: [
687
- child.icon && (() => {
688
- const ChildIcon = child.icon;
689
- return /* @__PURE__ */ jsx5(ChildIcon, {});
690
- })(),
691
- " ",
692
- child.label
693
- ] })
694
- };
695
- })
696
- },
697
- placement: "bottomLeft",
698
- children: /* @__PURE__ */ jsxs3(Button, { className: "toolbar-button", type: "text", size: "small", title: item.label, children: [
699
- /* @__PURE__ */ jsx5(Icon, {}),
700
- /* @__PURE__ */ jsx5(DownOutlined, { style: { marginLeft: 2, fontSize: 10 } })
701
- ] })
702
- }
703
- ) })
704
- ] });
705
- }
706
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
707
- item.separatorBefore && /* @__PURE__ */ jsx5("div", { className: "toolbar-separator" }),
708
- /* @__PURE__ */ jsx5("div", { className: "toolbar-group", children: /* @__PURE__ */ jsx5(
709
- Button,
710
- {
711
- id: item.id,
712
- className: "toolbar-button",
713
- type: "text",
714
- size: "small",
715
- title: item.label,
716
- onClick: () => onClick(item),
717
- children: /* @__PURE__ */ jsx5(Icon, {})
718
- }
719
- ) })
720
- ] });
741
+ const Icon = item.icon;
742
+ if (item.children && item.children.length > 0) return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
743
+ className: "toolbar-group",
744
+ children: /* @__PURE__ */ jsx(Tooltip, {
745
+ title: item.label,
746
+ children: /* @__PURE__ */ jsx(Dropdown, {
747
+ menu: { items: item.children.map((child) => ({
748
+ type: "item",
749
+ key: child.id ?? "",
750
+ label: /* @__PURE__ */ jsxs("span", {
751
+ onClick: () => onClick(child),
752
+ children: [
753
+ child.icon && (() => {
754
+ const ChildIcon = child.icon;
755
+ return /* @__PURE__ */ jsx(ChildIcon, {});
756
+ })(),
757
+ " ",
758
+ child.label
759
+ ]
760
+ })
761
+ })) },
762
+ placement: "bottomLeft",
763
+ children: /* @__PURE__ */ jsxs(Button, {
764
+ className: "toolbar-button",
765
+ type: "text",
766
+ size: "small",
767
+ children: [/* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsx(DownOutlined, { style: {
768
+ marginLeft: 2,
769
+ fontSize: 10
770
+ } })]
771
+ })
772
+ })
773
+ })
774
+ })] });
775
+ const isDisabled = item.disabled;
776
+ return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
777
+ className: "toolbar-group",
778
+ children: /* @__PURE__ */ jsx(Tooltip, {
779
+ title: item.label,
780
+ children: /* @__PURE__ */ jsx(Button, {
781
+ id: item.id,
782
+ className: "toolbar-button",
783
+ type: "text",
784
+ size: "small",
785
+ disabled: isDisabled,
786
+ onClick: () => !isDisabled && onClick(item),
787
+ children: /* @__PURE__ */ jsx(Icon, {})
788
+ })
789
+ })
790
+ })] });
721
791
  }
722
-
723
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
724
- import React, { useCallback as useCallback3, useRef as useRef3 } from "react";
725
- import { Button as Button2, Dropdown as Dropdown2 } from "antd";
726
- import { TableOutlined } from "@ant-design/icons";
727
-
728
- // src/components/Toolbar/renderers/TablePicker/index.tsx
729
- import { useState, useRef as useRef2, useEffect as useEffect3, useCallback as useCallback2 } from "react";
730
-
731
- // src/components/Toolbar/renderers/TablePicker/index.module.less
732
- var index_module_default = {};
733
-
734
- // src/components/Toolbar/renderers/TablePicker/index.tsx
735
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
736
- var MAX_ROW = 12;
737
- var MAX_COL = 12;
738
- var REFRESH_TIME = 200;
792
+ var index_module_default = {
793
+ container: "index-module__container___l6BT-",
794
+ grid: "index-module__grid___tCZ-r",
795
+ cell: "index-module__cell___vTDQz",
796
+ cellActive: "index-module__cellActive___0EYvU",
797
+ hint: "index-module__hint___dTObk",
798
+ label: "index-module__label___2CmK1"
799
+ };
800
+ //#endregion
801
+ //#region src/components/Toolbar/renderers/TablePicker/index.tsx
802
+ var MAX_ROW = 8;
803
+ var MAX_COL = 8;
739
804
  function TablePicker({ onSelect }) {
740
- const [hoverSize, setHoverSize] = useState({ row: 0, col: 0 });
741
- const [prevHoverSize, setPrevHoverSize] = useState({ row: 0, col: 0 });
742
- const [confirmedSize, setConfirmedSize] = useState({ row: 0, col: 0 });
743
- const maxColRef = useRef2(0);
744
- const timerIdRef = useRef2(null);
745
- const refreshTimerRef = useRef2(null);
746
- const commitRef = useRef2(null);
747
- const clearTimer = useCallback2((flushFn) => {
748
- if (timerIdRef.current) {
749
- timerIdRef.current = null;
750
- }
751
- if (flushFn && maxColRef.current > 0) {
752
- timerIdRef.current = setTimeout(flushFn, REFRESH_TIME);
753
- }
754
- }, []);
755
- useEffect3(
756
- () => () => {
757
- if (timerIdRef.current) clearTimeout(timerIdRef.current);
758
- if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
759
- },
760
- []
761
- );
762
- function extend(col, row) {
763
- if (col > maxColRef.current) {
764
- maxColRef.current = col;
765
- setHoverSize({ row, col });
766
- setConfirmedSize({ row, col });
767
- } else {
768
- setHoverSize({ row, col });
769
- if (row !== confirmedSize.row) {
770
- setConfirmedSize({ row, col: maxColRef.current });
771
- }
772
- if (row === 1) {
773
- commitRef.current = { cols: col, rows: 1 };
774
- setConfirmedSize({ row: 1, col });
775
- }
776
- }
777
- }
778
- function flash(col, dir, limit, finalCallback) {
779
- let current = dir === 1 ? 1 : limit;
780
- const step = dir === 1 ? 1 : -1;
781
- let timer = 0;
782
- refreshTimerRef.current = setInterval(() => {
783
- setHoverSize({ row: current, col });
784
- if (current > confirmedSize.row || dir === -1) {
785
- maxColRef.current = Math.min(MAX_COL, col);
786
- }
787
- current += step;
788
- if (dir === 1 && current > limit || dir === -1 && current < 1) {
789
- clearInterval(refreshTimerRef.current);
790
- setHoverSize({ row: current - step, col });
791
- finalCallback(col, current - step);
792
- }
793
- }, 50);
794
- clearTimeout(timer);
795
- return () => {
796
- clearInterval(refreshTimerRef.current);
797
- };
798
- }
799
- function onCellEnterCheck(index) {
800
- clearTimer();
801
- const row = Math.floor(index / MAX_COL) + 1;
802
- const col = index % MAX_COL + 1;
803
- const isDown = row > hoverSize.row;
804
- if (Math.abs(row - hoverSize.row) <= 1) {
805
- extend(col, row);
806
- } else {
807
- if (isDown && (col > maxColRef.current || maxColRef.current >= MAX_COL)) {
808
- const finalRow = Math.min(row, MAX_ROW);
809
- flash(col, 1, finalRow, (c, r) => {
810
- setConfirmedSize({ row: r, col: c });
811
- });
812
- } else if (!isDown) {
813
- flash(col, -1, row, (c, r) => {
814
- setConfirmedSize({ row: r, col: c });
815
- if (r === 1) {
816
- commitRef.current = { cols: Math.min(MAX_COL, col), rows: 1 };
817
- setConfirmedSize({ row: 1, col: Math.min(MAX_COL, col) });
818
- }
819
- });
820
- }
821
- }
822
- if (col >= MAX_COL && row >= prevHoverSize.row) {
823
- if (row !== prevHoverSize.row) {
824
- maxColRef.current = 0;
825
- setPrevHoverSize({ row: 0, col: 0 });
826
- }
827
- }
828
- setPrevHoverSize({ row, col });
829
- }
830
- function onMouseDown() {
831
- const size = commitRef.current || { cols: maxColRef.current, rows: confirmedSize.row };
832
- if (size.rows > 0 && size.cols > 0) {
833
- onSelect(size);
834
- }
835
- }
836
- return /* @__PURE__ */ jsxs4(
837
- "div",
838
- {
839
- className: index_module_default.container,
840
- onMouseDown,
841
- children: [
842
- /* @__PURE__ */ jsx6(
843
- "div",
844
- {
845
- className: index_module_default.grid,
846
- style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
847
- children: Array.from({ length: MAX_ROW * MAX_COL }).map((_, i) => {
848
- const row = Math.floor(i / MAX_COL) + 1;
849
- const col = i % MAX_COL + 1;
850
- const isSelected = row <= hoverSize.row && col <= maxColRef.current;
851
- return /* @__PURE__ */ jsx6(
852
- "div",
853
- {
854
- className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
855
- onMouseEnter: () => onCellEnterCheck(i)
856
- },
857
- i
858
- );
859
- })
860
- }
861
- ),
862
- /* @__PURE__ */ jsxs4("div", { className: index_module_default.label, children: [
863
- hoverSize.row,
864
- " x ",
865
- maxColRef.current || 1
866
- ] })
867
- ]
868
- }
869
- );
805
+ const [hoverSize, setHoverSize] = useState({
806
+ row: 0,
807
+ col: 0
808
+ });
809
+ const [confirmedSize, setConfirmedSize] = useState({
810
+ row: 0,
811
+ col: 0
812
+ });
813
+ const maxColRef = useRef(0);
814
+ const prevRowRef = useRef(0);
815
+ const timerIdRef = useRef(null);
816
+ const refreshTimerRef = useRef(null);
817
+ const commitRef = useRef(null);
818
+ const clearTimer = useCallback(() => {
819
+ if (timerIdRef.current) {
820
+ clearTimeout(timerIdRef.current);
821
+ timerIdRef.current = null;
822
+ }
823
+ }, []);
824
+ const clearFlash = useCallback(() => {
825
+ if (refreshTimerRef.current) {
826
+ clearInterval(refreshTimerRef.current);
827
+ refreshTimerRef.current = null;
828
+ }
829
+ }, []);
830
+ useEffect(() => () => {
831
+ if (timerIdRef.current) clearTimeout(timerIdRef.current);
832
+ if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
833
+ }, []);
834
+ /**
835
+ * 扩展选中区域至 (col, row)
836
+ */
837
+ function extend(col, row) {
838
+ if (row < prevRowRef.current || col < maxColRef.current) maxColRef.current = 0;
839
+ if (col > maxColRef.current) maxColRef.current = col;
840
+ setHoverSize({
841
+ row,
842
+ col
843
+ });
844
+ setConfirmedSize({
845
+ row,
846
+ col: maxColRef.current
847
+ });
848
+ if (row === 1) commitRef.current = {
849
+ cols: maxColRef.current,
850
+ rows: 1
851
+ };
852
+ }
853
+ /**
854
+ * 跳跃动画效果
855
+ */
856
+ function flash(col, dir, limit, finalCallback) {
857
+ let current = dir === 1 ? 1 : limit;
858
+ const step = dir === 1 ? 1 : -1;
859
+ refreshTimerRef.current = setInterval(() => {
860
+ setHoverSize({
861
+ row: current,
862
+ col
863
+ });
864
+ if (current > confirmedSize.row || dir === -1) maxColRef.current = Math.min(MAX_COL, col);
865
+ current += step;
866
+ if (dir === 1 && current > limit || dir === -1 && current < 1) {
867
+ clearInterval(refreshTimerRef.current);
868
+ refreshTimerRef.current = null;
869
+ const finalRow = current - step;
870
+ setHoverSize({
871
+ row: finalRow,
872
+ col
873
+ });
874
+ finalCallback(col, finalRow);
875
+ }
876
+ }, 50);
877
+ }
878
+ /**
879
+ * 核心逻辑:鼠标进入格子
880
+ */
881
+ function onCellEnterCheck(index) {
882
+ clearTimer();
883
+ clearFlash();
884
+ const row = Math.floor(index / MAX_COL) + 1;
885
+ const col = index % MAX_COL + 1;
886
+ const prevRow = prevRowRef.current;
887
+ const isJump = Math.abs(row - prevRow) > 1;
888
+ const isDown = row > prevRow;
889
+ prevRowRef.current = row;
890
+ if (!isJump) extend(col, row);
891
+ else if (isDown) {
892
+ if (col > maxColRef.current || maxColRef.current >= MAX_COL) flash(col, 1, Math.min(row, MAX_ROW), (c, r) => {
893
+ setConfirmedSize({
894
+ row: r,
895
+ col: c
896
+ });
897
+ });
898
+ else extend(col, row);
899
+ } else {
900
+ maxColRef.current = 0;
901
+ flash(col, -1, row, (c, r) => {
902
+ setConfirmedSize({
903
+ row: r,
904
+ col: c
905
+ });
906
+ if (r === 1) {
907
+ commitRef.current = {
908
+ cols: Math.min(MAX_COL, col),
909
+ rows: 1
910
+ };
911
+ setConfirmedSize({
912
+ row: 1,
913
+ col: Math.min(MAX_COL, col)
914
+ });
915
+ }
916
+ });
917
+ }
918
+ }
919
+ function onMouseDown() {
920
+ const size = commitRef.current || {
921
+ cols: maxColRef.current,
922
+ rows: confirmedSize.row
923
+ };
924
+ if (size.rows > 0 && size.cols > 0) onSelect(size);
925
+ }
926
+ return /* @__PURE__ */ jsxs("div", {
927
+ className: index_module_default.container,
928
+ onMouseDown,
929
+ children: [/* @__PURE__ */ jsx("div", {
930
+ className: index_module_default.grid,
931
+ style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
932
+ children: Array.from({ length: 64 }).map((_, i) => {
933
+ const row = Math.floor(i / MAX_COL) + 1;
934
+ const col = i % MAX_COL + 1;
935
+ const isSelected = row <= hoverSize.row && col <= maxColRef.current;
936
+ return /* @__PURE__ */ jsx("div", {
937
+ className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
938
+ onMouseEnter: () => onCellEnterCheck(i)
939
+ }, i);
940
+ })
941
+ }), /* @__PURE__ */ jsxs("div", {
942
+ className: index_module_default.label,
943
+ children: [
944
+ hoverSize.row,
945
+ " x ",
946
+ maxColRef.current || 1
947
+ ]
948
+ })]
949
+ });
870
950
  }
871
-
872
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
873
- import { jsx as jsx7 } from "react/jsx-runtime";
951
+ //#endregion
952
+ //#region src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
874
953
  function TableDropdown({ editorView }) {
875
- const [open, setOpen] = React.useState(false);
876
- const openCountRef = useRef3(0);
877
- const handleTableSelect = useCallback3((rows, cols) => {
878
- if (!editorView || rows <= 0 || cols <= 0) return;
879
- editorView.focus();
880
- const headerCells = Array(cols).fill(null).map((_, i) => ` \u5217${i + 1} `).join("|");
881
- const separator = Array(cols).fill(" --- ").join("|");
882
- const dataCell = Array(cols).fill(" \u5185\u5BB9 ").join("|");
883
- const dataRows = Array(rows).fill(`|${dataCell}|`).join("\n");
884
- const table = `
885
- |${headerCells}|
886
- |${separator}|
887
- ${dataRows}
888
- `;
889
- const { from } = editorView.state.selection.main;
890
- editorView.dispatch({ changes: { from, insert: table } });
891
- }, [editorView]);
892
- return /* @__PURE__ */ jsx7(
893
- Dropdown2,
894
- {
895
- trigger: ["click"],
896
- open,
897
- onOpenChange: setOpen,
898
- popupRender: () => {
899
- openCountRef.current++;
900
- return /* @__PURE__ */ jsx7("div", { style: { padding: 4 }, onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx7(
901
- TablePicker,
902
- {
903
- onSelect: (size) => {
904
- handleTableSelect(size.rows, size.cols);
905
- setOpen(false);
906
- }
907
- },
908
- openCountRef.current
909
- ) });
910
- },
911
- placement: "bottomLeft",
912
- children: /* @__PURE__ */ jsx7(Button2, { className: "toolbar-button", type: "text", size: "small", title: "\u8868\u683C", children: /* @__PURE__ */ jsx7(TableOutlined, {}) })
913
- }
914
- );
954
+ const [open, setOpen] = React.useState(false);
955
+ const openCountRef = useRef(0);
956
+ const handleTableSelect = useCallback((rows, cols) => {
957
+ if (!editorView || rows <= 0 || cols <= 0) return;
958
+ editorView.focus();
959
+ const headerCells = Array(cols).fill(null).map((_, i) => ` 列${i + 1} `).join("|");
960
+ const separator = Array(cols).fill(" --- ").join("|");
961
+ const dataCell = Array(cols).fill(" 内容 ").join("|");
962
+ const table = `\n|${headerCells}|\n|${separator}|\n${Array(rows).fill(`|${dataCell}|`).join("\n")}\n`;
963
+ const { from } = editorView.state.selection.main;
964
+ editorView.dispatch({ changes: {
965
+ from,
966
+ insert: table
967
+ } });
968
+ }, [editorView]);
969
+ return /* @__PURE__ */ jsx(Dropdown, {
970
+ trigger: ["click"],
971
+ open,
972
+ onOpenChange: setOpen,
973
+ popupRender: () => {
974
+ openCountRef.current++;
975
+ return /* @__PURE__ */ jsx("div", {
976
+ style: { padding: 4 },
977
+ onClick: (e) => e.stopPropagation(),
978
+ children: /* @__PURE__ */ jsx(TablePicker, { onSelect: (size) => {
979
+ handleTableSelect(size.rows, size.cols);
980
+ setOpen(false);
981
+ } }, openCountRef.current)
982
+ });
983
+ },
984
+ placement: "bottomLeft",
985
+ children: /* @__PURE__ */ jsx(Button, {
986
+ className: "toolbar-button",
987
+ type: "text",
988
+ size: "small",
989
+ title: "表格",
990
+ children: /* @__PURE__ */ jsx(TableOutlined, {})
991
+ })
992
+ });
915
993
  }
916
-
917
- // src/components/Toolbar/index.tsx
918
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
919
- function Toolbar({ editorView, fullscreen, onToggleFullscreen, extraItems }) {
920
- const { config, handleClick } = useToolbar(editorView, extraItems);
921
- const renderFullscreen = useCallback4(() => {
922
- if (!onToggleFullscreen) return null;
923
- const Icon = fullscreen ? FullscreenExitOutlined : FullscreenOutlined;
924
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
925
- /* @__PURE__ */ jsx8("div", { className: "toolbar-separator" }),
926
- /* @__PURE__ */ jsx8(
927
- Button3,
928
- {
929
- className: "toolbar-button",
930
- type: "text",
931
- size: "small",
932
- title: fullscreen ? "\u9000\u51FA\u5168\u5C4F" : "\u5168\u5C4F",
933
- onClick: () => {
934
- onToggleFullscreen();
935
- },
936
- children: /* @__PURE__ */ jsx8(Icon, {})
937
- }
938
- )
939
- ] });
940
- }, [fullscreen, onToggleFullscreen]);
941
- function isSeparator(item) {
942
- return !!(!item.id && item.separatorBefore);
943
- }
944
- return /* @__PURE__ */ jsxs5("div", { className: "toolbar", children: [
945
- config.map((item, idx) => {
946
- if (isSeparator(item)) {
947
- return /* @__PURE__ */ jsx8("div", { className: "toolbar-separator" }, `sep-${idx}`);
948
- }
949
- return /* @__PURE__ */ jsx8(
950
- ToolbarItemRenderer,
951
- {
952
- item,
953
- onClick: handleClick
954
- },
955
- item.id
956
- );
957
- }),
958
- /* @__PURE__ */ jsx8("div", { className: "toolbar-separator" }),
959
- /* @__PURE__ */ jsx8(TableDropdown, { editorView }),
960
- renderFullscreen()
961
- ] });
994
+ //#endregion
995
+ //#region src/components/Toolbar/index.tsx
996
+ function Toolbar({ editorView, fullscreen = true, onToggleFullscreen, extraItems, uploadConfig }) {
997
+ const { config, handleClick } = useToolbar(editorView, {
998
+ extraItems,
999
+ uploadConfig
1000
+ });
1001
+ const renderFullscreen = useCallback(() => {
1002
+ if (!onToggleFullscreen) return null;
1003
+ return /* @__PURE__ */ jsx(Button, {
1004
+ className: "toolbar-button",
1005
+ type: "text",
1006
+ size: "small",
1007
+ title: fullscreen ? "退出全屏" : "全屏",
1008
+ onClick: () => {
1009
+ onToggleFullscreen();
1010
+ },
1011
+ children: /* @__PURE__ */ jsx(fullscreen ? FullscreenExitOutlined : FullscreenOutlined, {})
1012
+ });
1013
+ }, [fullscreen, onToggleFullscreen]);
1014
+ function isSeparator(item) {
1015
+ return !!(!item.id && item.separatorBefore);
1016
+ }
1017
+ return /* @__PURE__ */ jsxs("div", {
1018
+ className: "toolbar",
1019
+ children: [
1020
+ config.map((item, idx) => {
1021
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `sep-${idx}`);
1022
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1023
+ item,
1024
+ onClick: handleClick
1025
+ }, item.id);
1026
+ }),
1027
+ /* @__PURE__ */ jsx(TableDropdown, { editorView }),
1028
+ renderFullscreen()
1029
+ ]
1030
+ });
962
1031
  }
963
-
964
- // src/components/Dialog/EditDialog.tsx
965
- import { useState as useState2, useMemo as useMemo2 } from "react";
966
- import { Modal } from "antd";
967
- import { jsx as jsx9 } from "react/jsx-runtime";
1032
+ //#endregion
1033
+ //#region src/components/Dialog/EditDialog.tsx
1034
+ /**
1035
+ * 属性编辑弹窗组件
1036
+ * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
1037
+ */
968
1038
  function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
969
- const schema = componentRegistry_default.get(typeName);
970
- const [formProps, setFormProps] = useState2({});
971
- const mergedProps = useMemo2(() => {
972
- var _a;
973
- if (!schema) return currentProps;
974
- const defaults = {};
975
- for (const field of schema.fields) {
976
- defaults[field.key] = (_a = field.defaultValue) != null ? _a : "";
977
- }
978
- return { ...defaults, ...currentProps };
979
- }, [schema, currentProps]);
980
- const handleDialogChange = (key, value) => {
981
- setFormProps((prev) => ({ ...prev, [key]: value }));
982
- };
983
- const handleOk = () => {
984
- const finalProps = visible ? { ...mergedProps, ...formProps } : mergedProps;
985
- onSave(finalProps);
986
- setFormProps({});
987
- };
988
- const handleClose = () => {
989
- setFormProps({});
990
- onClose();
991
- };
992
- if (!schema) {
993
- return null;
994
- }
995
- return /* @__PURE__ */ jsx9(
996
- Modal,
997
- {
998
- title: `${schema.label} \u5C5E\u6027\u7F16\u8F91`,
999
- open: visible,
1000
- onOk: handleOk,
1001
- onCancel: handleClose,
1002
- okText: "\u4FDD\u5B58",
1003
- cancelText: "\u53D6\u6D88",
1004
- children: schema.renderDialog(visible ? { ...mergedProps, ...formProps } : mergedProps, handleDialogChange)
1005
- }
1006
- );
1039
+ const schema = componentRegistry.get(typeName);
1040
+ const [formProps, setFormProps] = useState({});
1041
+ const mergedProps = useMemo(() => {
1042
+ if (!schema) return currentProps;
1043
+ const defaults = {};
1044
+ for (const field of schema.fields || []) defaults[field.key] = field.defaultValue ?? "";
1045
+ return {
1046
+ ...defaults,
1047
+ ...currentProps
1048
+ };
1049
+ }, [schema, currentProps]);
1050
+ const handleDialogChange = (key, value) => {
1051
+ setFormProps((prev) => ({
1052
+ ...prev,
1053
+ [key]: value
1054
+ }));
1055
+ };
1056
+ const handleOk = () => {
1057
+ onSave(visible ? {
1058
+ ...mergedProps,
1059
+ ...formProps
1060
+ } : mergedProps);
1061
+ setFormProps({});
1062
+ };
1063
+ const handleClose = () => {
1064
+ setFormProps({});
1065
+ onClose();
1066
+ };
1067
+ if (!schema || !schema.renderDialog) return null;
1068
+ return /* @__PURE__ */ jsx(Modal, {
1069
+ title: `${schema.label} 属性编辑`,
1070
+ open: visible,
1071
+ onOk: handleOk,
1072
+ onCancel: handleClose,
1073
+ okText: "保存",
1074
+ cancelText: "取消",
1075
+ children: schema.renderDialog(visible ? {
1076
+ ...mergedProps,
1077
+ ...formProps
1078
+ } : mergedProps, handleDialogChange)
1079
+ });
1007
1080
  }
1008
-
1009
- // src/utils/writeContainer.ts
1081
+ //#endregion
1082
+ //#region src/utils/writeContainer.ts
1083
+ /**
1084
+ * 从属性对象生成 JSON 属性行。
1085
+ * 格式: { key: 'value', key2: 'value2' }
1086
+ */
1010
1087
  function writeContainerProps(props) {
1011
- const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1012
- return entries ? `{ ${entries} }` : "{}";
1088
+ const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1089
+ return entries ? `{ ${entries} }` : "{}";
1013
1090
  }
1091
+ /**
1092
+ * 更新编辑器文档中的容器属性。
1093
+ * 替换 JSON 属性行(::: 打开行之后的一行)为新属性。
1094
+ */
1014
1095
  function updateContainerProps(view, propsLineFrom, propsLineTo, newProps) {
1015
- const newPropsLine = writeContainerProps(newProps);
1016
- view.dispatch({
1017
- changes: {
1018
- from: propsLineFrom,
1019
- to: propsLineTo,
1020
- insert: newPropsLine
1021
- }
1022
- });
1096
+ const newPropsLine = writeContainerProps(newProps);
1097
+ view.dispatch({ changes: {
1098
+ from: propsLineFrom,
1099
+ to: propsLineTo,
1100
+ insert: newPropsLine
1101
+ } });
1023
1102
  }
1103
+ /**
1104
+ * 在当前光标位置插入新容器。
1105
+ * 创建 :::type 打开行 + JSON 属性 + ::: 关闭行的结构。
1106
+ */
1024
1107
  function insertContainer(view, typeName, defaultProps) {
1025
- const { from } = view.state.selection.main;
1026
- const propsLine = writeContainerProps(defaultProps);
1027
- const container = `:::${typeName}
1028
- ${propsLine}
1029
- :::
1030
- `;
1031
- view.dispatch({ changes: { from, insert: container } });
1108
+ const { from } = view.state.selection.main;
1109
+ const container = `:::${typeName}\n${writeContainerProps(defaultProps)}\n:::\n`;
1110
+ view.dispatch({ changes: {
1111
+ from,
1112
+ insert: container
1113
+ } });
1032
1114
  }
1033
-
1034
- // src/MarkdownEditor.tsx
1035
- import { ReadOutlined } from "@ant-design/icons";
1036
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1037
- var MarkdownEditor_default = forwardRef2(function MarkdownEditor({
1038
- defaultValue = "",
1039
- value,
1040
- onChange,
1041
- showPreview = true,
1042
- fullscreen: fullscreenProp = false,
1043
- previewOptions
1044
- }, ref) {
1045
- const [content, setContent] = useState3(value != null ? value : defaultValue);
1046
- const [editorView, setEditorView] = useState3(null);
1047
- const scrollDOMRef = useRef4(null);
1048
- const previewElRef = useRef4(null);
1049
- const [fullscreen, setFullscreen] = useState3(false);
1050
- const [dialogVisible, setDialogVisible] = useState3(false);
1051
- const [dialogTypeName, setDialogTypeName] = useState3("");
1052
- const [dialogProps, setDialogProps] = useState3({});
1053
- const [dialogPropsRange, setDialogPropsRange] = useState3([0, 0]);
1054
- useEffect4(() => {
1055
- if (value !== void 0) {
1056
- setContent(value);
1057
- }
1058
- }, [value]);
1059
- const internalSetContent = useCallback5((newContent) => {
1060
- setContent(newContent);
1061
- onChange == null ? void 0 : onChange(newContent);
1062
- }, [onChange]);
1063
- useImperativeHandle(ref, () => ({
1064
- getContent: () => content
1065
- }), [content]);
1066
- const toggleFullscreen = useCallback5(() => {
1067
- if (document.fullscreenElement) {
1068
- document.exitFullscreen().catch((e) => {
1069
- console.error("\u9000\u51FA\u5168\u5C4F\u5931\u8D25:", e);
1070
- });
1071
- } else {
1072
- const el = document.documentElement;
1073
- const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1074
- if (requestMethod) {
1075
- requestMethod.call(el).catch((e) => {
1076
- console.error("\u8FDB\u5165\u5168\u5C4F\u5931\u8D25:", e);
1077
- });
1078
- }
1079
- }
1080
- }, []);
1081
- useEffect4(() => {
1082
- const handleFullscreenChange = () => {
1083
- setFullscreen(!!document.fullscreenElement);
1084
- };
1085
- document.addEventListener("fullscreenchange", handleFullscreenChange);
1086
- document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1087
- return () => {
1088
- document.removeEventListener("fullscreenchange", handleFullscreenChange);
1089
- document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1090
- };
1091
- }, []);
1092
- const extraToolbarItems = componentRegistry_default.getAll().map((schema) => {
1093
- var _a, _b;
1094
- const defaultProps = {};
1095
- for (const field of schema.fields) {
1096
- defaultProps[field.key] = (_a = field.defaultValue) != null ? _a : "";
1097
- }
1098
- const iconNode = (_b = schema.icon) != null ? _b : /* @__PURE__ */ jsx10(ReadOutlined, {});
1099
- return {
1100
- id: `container-${schema.name}`,
1101
- label: schema.label,
1102
- icon: () => iconNode,
1103
- handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1104
- };
1105
- });
1106
- const handleScrollReady = useCallback5((scrollDOM) => {
1107
- scrollDOMRef.current = scrollDOM;
1108
- }, []);
1109
- useEffect4(() => {
1110
- const source = scrollDOMRef.current;
1111
- const target = previewElRef.current;
1112
- if (!source || !target) return;
1113
- let rafId = null;
1114
- const listener = () => {
1115
- if (rafId) return;
1116
- rafId = requestAnimationFrame(() => {
1117
- const s = scrollDOMRef.current;
1118
- const t = previewElRef.current;
1119
- if (!s || !t) {
1120
- rafId = null;
1121
- return;
1122
- }
1123
- const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1124
- if (sourceScrollHeight <= 0) {
1125
- rafId = null;
1126
- return;
1127
- }
1128
- const ratio = s.scrollTop / sourceScrollHeight;
1129
- const targetScrollHeight = t.scrollHeight - t.clientHeight;
1130
- if (targetScrollHeight > 0) {
1131
- t.scrollTop = ratio * targetScrollHeight;
1132
- }
1133
- rafId = null;
1134
- });
1135
- };
1136
- source.addEventListener("scroll", listener, { passive: true });
1137
- return () => {
1138
- source.removeEventListener("scroll", listener);
1139
- if (rafId) cancelAnimationFrame(rafId);
1140
- };
1141
- }, [previewElRef.current]);
1142
- const handlePreviewRef = useCallback5((node) => {
1143
- previewElRef.current = node;
1144
- }, []);
1145
- const handleViewReady = useCallback5((view) => {
1146
- setEditorView(view);
1147
- }, []);
1148
- const handleEditContainer = useCallback5((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1149
- setDialogTypeName(typeName);
1150
- setDialogProps({ ...currentProps });
1151
- setDialogPropsRange([propsLineFrom, propsLineTo]);
1152
- setDialogVisible(true);
1153
- }, []);
1154
- const handleDialogSave = useCallback5((newProps) => {
1155
- setDialogVisible(false);
1156
- if (editorView) {
1157
- updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1158
- }
1159
- }, [editorView, dialogPropsRange]);
1160
- const handleDialogClose = useCallback5(() => {
1161
- setDialogVisible(false);
1162
- }, []);
1163
- return /* @__PURE__ */ jsxs6("div", { className: fullscreen ? "app fullscreen" : "app", children: [
1164
- /* @__PURE__ */ jsx10(
1165
- Toolbar,
1166
- {
1167
- editorView,
1168
- fullscreen,
1169
- onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1170
- extraItems: extraToolbarItems
1171
- }
1172
- ),
1173
- showPreview ? /* @__PURE__ */ jsx10(
1174
- SplitPanels,
1175
- {
1176
- leftPanel: /* @__PURE__ */ jsx10(
1177
- Editor,
1178
- {
1179
- value: content,
1180
- onChange: internalSetContent,
1181
- onScrollReady: handleScrollReady,
1182
- onViewReady: handleViewReady,
1183
- onEditContainer: handleEditContainer
1184
- }
1185
- ),
1186
- rightPanel: /* @__PURE__ */ jsx10(
1187
- Preview_default,
1188
- {
1189
- content,
1190
- ref: handlePreviewRef,
1191
- previewOptions
1192
- }
1193
- )
1194
- }
1195
- ) : /* @__PURE__ */ jsx10(
1196
- Editor,
1197
- {
1198
- value: content,
1199
- onChange: internalSetContent,
1200
- onScrollReady: handleScrollReady,
1201
- onViewReady: handleViewReady,
1202
- onEditContainer: handleEditContainer
1203
- }
1204
- ),
1205
- /* @__PURE__ */ jsx10(
1206
- EditDialog,
1207
- {
1208
- visible: dialogVisible,
1209
- typeName: dialogTypeName,
1210
- currentProps: dialogProps,
1211
- onSave: handleDialogSave,
1212
- onClose: handleDialogClose
1213
- }
1214
- )
1215
- ] });
1115
+ //#endregion
1116
+ //#region src/hooks/useFullscreen.ts
1117
+ function useFullscreen() {
1118
+ const [fullscreen, setFullscreen] = useState(false);
1119
+ const toggleFullscreen = useCallback(() => {
1120
+ if (document.fullscreenElement) document.exitFullscreen().catch((e) => {
1121
+ console.error("退出全屏失败:", e);
1122
+ });
1123
+ else {
1124
+ const el = document.documentElement;
1125
+ const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1126
+ if (requestMethod) requestMethod.call(el).catch((e) => {
1127
+ console.error("进入全屏失败:", e);
1128
+ });
1129
+ }
1130
+ }, []);
1131
+ useEffect(() => {
1132
+ const handleFullscreenChange = () => {
1133
+ setFullscreen(!!document.fullscreenElement);
1134
+ };
1135
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1136
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1137
+ return () => {
1138
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1139
+ document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1140
+ };
1141
+ }, []);
1142
+ return {
1143
+ fullscreen,
1144
+ toggleFullscreen
1145
+ };
1146
+ }
1147
+ //#endregion
1148
+ //#region src/hooks/useScrollSync.ts
1149
+ /**
1150
+ * 滚动同步 Hook
1151
+ * 将源滚动容器的滚动位置同步到目标预览容器
1152
+ */
1153
+ function useScrollSync(sourceRef, targetRef) {
1154
+ const rafIdRef = useRef(null);
1155
+ useEffect(() => {
1156
+ const source = sourceRef.current;
1157
+ const target = targetRef.current;
1158
+ if (!source || !target) return;
1159
+ const listener = () => {
1160
+ if (rafIdRef.current) return;
1161
+ rafIdRef.current = requestAnimationFrame(() => {
1162
+ const s = sourceRef.current;
1163
+ const t = targetRef.current;
1164
+ if (!s || !t) {
1165
+ rafIdRef.current = null;
1166
+ return;
1167
+ }
1168
+ const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1169
+ if (sourceScrollHeight <= 0) {
1170
+ rafIdRef.current = null;
1171
+ return;
1172
+ }
1173
+ const ratio = s.scrollTop / sourceScrollHeight;
1174
+ const targetScrollHeight = t.scrollHeight - t.clientHeight;
1175
+ if (targetScrollHeight > 0) t.scrollTop = ratio * targetScrollHeight;
1176
+ rafIdRef.current = null;
1177
+ });
1178
+ };
1179
+ source.addEventListener("scroll", listener, { passive: true });
1180
+ return () => {
1181
+ source.removeEventListener("scroll", listener);
1182
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
1183
+ };
1184
+ }, [sourceRef, targetRef]);
1185
+ }
1186
+ //#endregion
1187
+ //#region src/MarkdownEditor.tsx
1188
+ var MarkdownEditor_default = forwardRef(function MarkdownEditor({ defaultValue = "", value, onChange, showPreview = true, fullscreen: fullscreenProp = false, previewOptions, uploadConfig }, ref) {
1189
+ const [content, setContent] = useState(value ?? defaultValue);
1190
+ const [editorView, setEditorView] = useState(null);
1191
+ const scrollDOMRef = useRef(null);
1192
+ const previewElRef = useRef(null);
1193
+ const { fullscreen, toggleFullscreen } = useFullscreen();
1194
+ useScrollSync(scrollDOMRef, previewElRef);
1195
+ const [dialogVisible, setDialogVisible] = useState(false);
1196
+ const [dialogTypeName, setDialogTypeName] = useState("");
1197
+ const [dialogProps, setDialogProps] = useState({});
1198
+ const [dialogPropsRange, setDialogPropsRange] = useState([0, 0]);
1199
+ useEffect(() => {
1200
+ if (value !== void 0) setContent(value);
1201
+ }, [value]);
1202
+ const internalSetContent = useCallback((newContent) => {
1203
+ setContent(newContent);
1204
+ onChange?.(newContent);
1205
+ }, [onChange]);
1206
+ useImperativeHandle(ref, () => ({ getContent: () => content }), [content]);
1207
+ const extraToolbarItems = componentRegistry.getAll().map((schema) => {
1208
+ const defaultProps = {};
1209
+ for (const field of schema?.fields ?? []) defaultProps[field.key] = field.defaultValue ?? "";
1210
+ const iconNode = schema.icon ?? /* @__PURE__ */ jsx(ReadOutlined, {});
1211
+ return {
1212
+ id: `container-${schema.name}`,
1213
+ label: schema.label,
1214
+ icon: () => iconNode,
1215
+ handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1216
+ };
1217
+ });
1218
+ const handleScrollReady = useCallback((scrollDOM) => {
1219
+ scrollDOMRef.current = scrollDOM;
1220
+ }, []);
1221
+ const handlePreviewRef = useCallback((node) => {
1222
+ previewElRef.current = node;
1223
+ }, []);
1224
+ const handleViewReady = useCallback((view) => {
1225
+ setEditorView(view);
1226
+ }, []);
1227
+ const handleEditContainer = useCallback((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1228
+ setDialogTypeName(typeName);
1229
+ setDialogProps({ ...currentProps });
1230
+ setDialogPropsRange([propsLineFrom, propsLineTo]);
1231
+ setDialogVisible(true);
1232
+ }, []);
1233
+ const handleDialogSave = useCallback((newProps) => {
1234
+ setDialogVisible(false);
1235
+ if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1236
+ }, [editorView, dialogPropsRange]);
1237
+ const handleDialogClose = useCallback(() => {
1238
+ setDialogVisible(false);
1239
+ }, []);
1240
+ return /* @__PURE__ */ jsxs("div", {
1241
+ className: fullscreen ? "app fullscreen" : "app",
1242
+ children: [
1243
+ /* @__PURE__ */ jsx(Toolbar, {
1244
+ editorView,
1245
+ fullscreen,
1246
+ onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1247
+ extraItems: extraToolbarItems,
1248
+ uploadConfig
1249
+ }),
1250
+ showPreview ? /* @__PURE__ */ jsx(SplitPanels, {
1251
+ leftPanel: /* @__PURE__ */ jsx(Editor, {
1252
+ value: content,
1253
+ onChange: internalSetContent,
1254
+ onScrollReady: handleScrollReady,
1255
+ onViewReady: handleViewReady,
1256
+ onEditContainer: handleEditContainer,
1257
+ uploadConfig,
1258
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1259
+ }),
1260
+ rightPanel: /* @__PURE__ */ jsx(Preview, {
1261
+ content,
1262
+ ref: handlePreviewRef,
1263
+ previewOptions
1264
+ })
1265
+ }) : /* @__PURE__ */ jsx(Editor, {
1266
+ value: content,
1267
+ onChange: internalSetContent,
1268
+ onScrollReady: handleScrollReady,
1269
+ onViewReady: handleViewReady,
1270
+ onEditContainer: handleEditContainer,
1271
+ uploadConfig,
1272
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1273
+ }),
1274
+ /* @__PURE__ */ jsx(EditDialog, {
1275
+ visible: dialogVisible,
1276
+ typeName: dialogTypeName,
1277
+ currentProps: dialogProps,
1278
+ onSave: handleDialogSave,
1279
+ onClose: handleDialogClose
1280
+ })
1281
+ ]
1282
+ });
1216
1283
  });
1217
-
1218
- // src/hooks/useRegisterToolbar.ts
1219
- import { useEffect as useEffect5 } from "react";
1284
+ //#endregion
1285
+ //#region src/hooks/useRegisterToolbar.ts
1286
+ /**
1287
+ * 注册自定义容器组件到工具栏。支持单个或批量注册。
1288
+ * 支持两种模式:
1289
+ *
1290
+ * 1. 容器模式(linkCard):name + fields + renderDialog
1291
+ * 插入 :::name 格式的容器,支持弹窗编辑属性
1292
+ *
1293
+ * 2. 标签包裹模式:name + wrapTags
1294
+ * 插入简单的标签包裹文本,如 <plugin-container>...</plugin-container>
1295
+ *
1296
+ * @example 容器模式
1297
+ * ```tsx
1298
+ * const schema: ComponentSchema = {
1299
+ * name: 'alert',
1300
+ * label: '警告框',
1301
+ * icon: <AlertOutlined />,
1302
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1303
+ * renderDialog: (props, onChange) => (
1304
+ * <Input value={props.level} onChange={e => onChange('level', e.target.value)} />
1305
+ * ),
1306
+ * }
1307
+ *
1308
+ * function MyPage() {
1309
+ * useRegisterToolbar(schema)
1310
+ * return <MarkdownEditor />
1311
+ * }
1312
+ * ```
1313
+ *
1314
+ * @example 标签包裹模式
1315
+ * ```tsx
1316
+ * function MyPage() {
1317
+ * useRegisterToolbar({
1318
+ * name: 'plugin-container',
1319
+ * label: '自定义容器',
1320
+ * icon: <AlertOutlined />,
1321
+ * wrapTags: ['<plugin-container>', '</plugin-container>'],
1322
+ * })
1323
+ * return <MarkdownEditor />
1324
+ * }
1325
+ * ```
1326
+ *
1327
+ * @example 批量注册
1328
+ * ```tsx
1329
+ * function MyPage() {
1330
+ * useRegisterToolbar([schema1, schema2, schema3])
1331
+ * return <MarkdownEditor />
1332
+ * }
1333
+ * ```
1334
+ */
1220
1335
  function useRegisterToolbar(schemas) {
1221
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1222
- useEffect5(() => {
1223
- schemaList.forEach((schema) => {
1224
- componentRegistry_default.register(schema.name, schema);
1225
- });
1226
- return () => {
1227
- schemaList.forEach((schema) => {
1228
- componentRegistry_default.unregister(schema.name);
1229
- });
1230
- };
1231
- }, [schemaList]);
1336
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1337
+ useEffect(() => {
1338
+ schemaList.forEach((schema) => {
1339
+ componentRegistry.register(schema.name, schema);
1340
+ });
1341
+ return () => {
1342
+ schemaList.forEach((schema) => {
1343
+ componentRegistry.unregister(schema.name);
1344
+ });
1345
+ };
1346
+ }, [schemaList]);
1232
1347
  }
1233
- export {
1234
- MarkdownEditor_default as MarkdownEditor,
1235
- useRegisterToolbar
1236
- };
1348
+ //#endregion
1349
+ export { MarkdownEditor_default as MarkdownEditor, useRegisterToolbar };
1350
+
1237
1351
  //# sourceMappingURL=index.js.map