remons-markdown-editor 2.0.0-beta.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { App, Button, ColorPicker, DatePicker, Dropdown, Form, Input, InputNumber, Modal, Rate, Select, Slider, Splitter, Switch, Tooltip, message } from "antd";
3
3
  import { EditorView, basicSetup } from "codemirror";
4
- import { EditorState, Facet } from "@codemirror/state";
4
+ import { EditorState, Facet, StateEffect } from "@codemirror/state";
5
5
  import { Decoration, EditorView as EditorView$1, ViewPlugin, WidgetType, keymap } from "@codemirror/view";
6
6
  import { defaultKeymap, indentWithTab } from "@codemirror/commands";
7
7
  import { markdown } from "@codemirror/lang-markdown";
@@ -1274,8 +1274,25 @@ function parseContainerProps(line) {
1274
1274
  }
1275
1275
  }
1276
1276
  /**
1277
+ * 解析行首的引用(blockquote)嵌套前缀。
1278
+ * markdown-it 解析引用内部内容时会剥掉一层 "> ",因此 "> :::badge" 在渲染侧
1279
+ * 等同于 ":::badge";编辑器侧需同样识别,否则嵌套引用的容器匹配不到标签。
1280
+ * 返回 [前缀文本(含 "> " 及尾随空格), 去掉前缀后的内容],无前缀时返回 ['', 原文本]。
1281
+ */
1282
+ function splitQuotePrefix(line) {
1283
+ const m = line.match(/^((?:>\s*)+)(\S.*)$/);
1284
+ if (!m) return ["", line];
1285
+ return [m[1], m[2]];
1286
+ }
1287
+ /** 引用嵌套深度("> > x" 为 2,无前缀为 0) */
1288
+ function quoteDepth(prefix) {
1289
+ return (prefix.match(/>/g) ?? []).length;
1290
+ }
1291
+ /**
1277
1292
  * 在编辑器文档中查找容器块。
1278
1293
  * 单次遍历 + 预累计行偏移,复杂度 O(行数)。
1294
+ * 支持整体嵌套在引用中的容器:头行、属性行、闭合行需具有相同的 "> " 前缀深度,
1295
+ * 避免跨引用层级错误配对。闭合 ::: 改为按栈配对,嵌套容器不会被内层闭合误截断。
1279
1296
  */
1280
1297
  function findContainers(docText) {
1281
1298
  const containers = [];
@@ -1286,34 +1303,53 @@ function findContainers(docText) {
1286
1303
  offsets[i] = pos;
1287
1304
  pos += lines[i].length + 1;
1288
1305
  }
1306
+ const stack = [];
1289
1307
  for (let i = 0; i < lines.length; i++) {
1290
- const match = lines[i].match(/^:::(\w+)$/);
1308
+ const [prefix, body] = splitQuotePrefix(lines[i]);
1309
+ const depth = quoteDepth(prefix);
1310
+ if (/^:::$/.test(body)) {
1311
+ let top = stack.length - 1;
1312
+ while (top >= 0 && stack[top].depth !== depth) top--;
1313
+ if (top >= 0) {
1314
+ stack[top].block.endPos = offsets[i];
1315
+ stack.length = top;
1316
+ }
1317
+ continue;
1318
+ }
1319
+ const match = body.match(/^:::(\w+)$/);
1291
1320
  if (!match) continue;
1292
1321
  const typeName = match[1];
1293
1322
  const headerPos = offsets[i];
1294
1323
  let props = {};
1295
1324
  let propsLineFrom = 0;
1296
1325
  let propsLineTo = 0;
1297
- const propsLine = lines[i + 1];
1298
- if (propsLine && propsLine.trim().startsWith("{") && propsLine.trim().endsWith("}")) {
1299
- props = parseContainerProps(propsLine);
1300
- propsLineFrom = offsets[i + 1];
1301
- propsLineTo = propsLineFrom + propsLine.length;
1302
- }
1303
- let endPos = 0;
1304
- for (let j = i + 1; j < lines.length; j++) if (lines[j].trim() === ":::") {
1305
- endPos = offsets[j];
1306
- i = j;
1307
- break;
1326
+ let propsQuotePrefix = "";
1327
+ const nextLine = lines[i + 1];
1328
+ if (nextLine) {
1329
+ const [nextPrefix, nextBody] = splitQuotePrefix(nextLine);
1330
+ if ((quoteDepth(nextPrefix) === depth || nextPrefix === "") && nextBody.trimStart().startsWith("{") && nextBody.trimEnd().endsWith("}")) {
1331
+ props = parseContainerProps(nextBody);
1332
+ propsLineFrom = offsets[i + 1];
1333
+ propsLineTo = propsLineFrom + nextLine.length;
1334
+ propsQuotePrefix = nextPrefix;
1335
+ i += 1;
1336
+ }
1308
1337
  }
1309
- containers.push({
1338
+ const block = {
1310
1339
  startPos: headerPos,
1311
1340
  typeName,
1312
1341
  propsLineFrom,
1313
1342
  propsLineTo,
1314
1343
  props,
1315
- endPos,
1316
- registered: componentRegistry.has(typeName)
1344
+ endPos: 0,
1345
+ registered: componentRegistry.has(typeName),
1346
+ quotePrefix: prefix,
1347
+ propsQuotePrefix
1348
+ };
1349
+ containers.push(block);
1350
+ stack.push({
1351
+ block,
1352
+ depth
1317
1353
  });
1318
1354
  }
1319
1355
  return containers;
@@ -1321,6 +1357,14 @@ function findContainers(docText) {
1321
1357
  //#endregion
1322
1358
  //#region src/components/Editor/containerWidget.ts
1323
1359
  /**
1360
+ * 唤醒容器装饰重建的状态 effect(不改变文档)。
1361
+ * 编辑按钮是否展示取决于容器类型是否已注册,而宿主常在编辑器挂载之后才
1362
+ * 通过 useRegisterToolbar 异步注册 schema;注册不是 CodeMirror 事务,默认不会触发
1363
+ * ViewPlugin.update,导致初始化时烘焙的 registered=false 无法刷新(需手动改一个字才出现)。
1364
+ * 宿主在注册中心版本变化时 dispatch 本 effect,驱动插件重新求值 registered。
1365
+ */
1366
+ var refreshContainers = StateEffect.define();
1367
+ /**
1324
1368
  * 自定义 facet 用于传递编辑回调
1325
1369
  */
1326
1370
  var containerEditCallback = Facet.define({ combine: (values) => values.length ? values[values.length - 1] : null });
@@ -1333,18 +1377,20 @@ var ContainerHeaderWidget = class extends WidgetType {
1333
1377
  typeName;
1334
1378
  containerIndex;
1335
1379
  registered;
1336
- constructor(typeName, containerIndex, registered) {
1380
+ quotePrefix;
1381
+ constructor(typeName, containerIndex, registered, quotePrefix) {
1337
1382
  super();
1338
1383
  this.typeName = typeName;
1339
1384
  this.containerIndex = containerIndex;
1340
1385
  this.registered = registered;
1386
+ this.quotePrefix = quotePrefix;
1341
1387
  }
1342
1388
  toDOM() {
1343
1389
  const container = document.createElement("span");
1344
1390
  container.className = "container-widget-header";
1345
1391
  const text = document.createElement("span");
1346
1392
  text.className = "container-widget-text";
1347
- text.textContent = `:::${this.typeName}`;
1393
+ text.textContent = `${this.quotePrefix}:::${this.typeName}`;
1348
1394
  container.appendChild(text);
1349
1395
  if (this.registered) {
1350
1396
  const button = document.createElement("button");
@@ -1358,7 +1404,7 @@ var ContainerHeaderWidget = class extends WidgetType {
1358
1404
  return container;
1359
1405
  }
1360
1406
  eq(other) {
1361
- return this.typeName === other.typeName && this.containerIndex === other.containerIndex && this.registered === other.registered;
1407
+ return this.typeName === other.typeName && this.containerIndex === other.containerIndex && this.registered === other.registered && this.quotePrefix === other.quotePrefix;
1362
1408
  }
1363
1409
  ignoreEvent() {
1364
1410
  return false;
@@ -1366,38 +1412,46 @@ var ContainerHeaderWidget = class extends WidgetType {
1366
1412
  };
1367
1413
  /**
1368
1414
  * 只读的容器属性行 Widget(显示 { key: value, ... })
1415
+ * 嵌套在引用中时原始行带 "> " 前缀,展示时剔除,与头行 widget 风格一致
1369
1416
  */
1370
1417
  var ContainerPropsWidget = class extends WidgetType {
1371
1418
  propsText;
1372
- constructor(propsText) {
1419
+ quotePrefix;
1420
+ constructor(propsText, quotePrefix) {
1373
1421
  super();
1374
1422
  this.propsText = propsText;
1423
+ this.quotePrefix = quotePrefix;
1375
1424
  }
1376
1425
  toDOM() {
1377
1426
  const span = document.createElement("span");
1378
1427
  span.className = "container-widget-props";
1379
- span.textContent = this.propsText;
1428
+ span.textContent = this.propsText.startsWith(this.quotePrefix) ? this.propsText.slice(this.quotePrefix.length) : this.propsText;
1380
1429
  return span;
1381
1430
  }
1382
1431
  eq(other) {
1383
- return this.propsText === other.propsText;
1432
+ return this.propsText === other.propsText && this.quotePrefix === other.quotePrefix;
1384
1433
  }
1385
1434
  ignoreEvent() {
1386
1435
  return false;
1387
1436
  }
1388
1437
  };
1389
1438
  /**
1390
- * 只读的容器结束行 Widget(显示 :::)
1439
+ * 只读的容器结束行 Widget(显示 :::,嵌套引用时保留 "> " 前缀)
1391
1440
  */
1392
1441
  var ContainerCloseWidget = class extends WidgetType {
1442
+ text;
1443
+ constructor(text) {
1444
+ super();
1445
+ this.text = text;
1446
+ }
1393
1447
  toDOM() {
1394
1448
  const span = document.createElement("span");
1395
1449
  span.className = "container-widget-text";
1396
- span.textContent = ":::";
1450
+ span.textContent = this.text;
1397
1451
  return span;
1398
1452
  }
1399
- eq() {
1400
- return true;
1453
+ eq(other) {
1454
+ return this.text === other.text;
1401
1455
  }
1402
1456
  ignoreEvent() {
1403
1457
  return false;
@@ -1415,14 +1469,14 @@ function findContainerDecorations(state) {
1415
1469
  decorations.push({
1416
1470
  from: headerLine.from,
1417
1471
  to: headerLine.to,
1418
- value: Decoration.replace({ widget: new ContainerHeaderWidget(container.typeName, index, container.registered) })
1472
+ value: Decoration.replace({ widget: new ContainerHeaderWidget(container.typeName, index, container.registered, container.quotePrefix ?? "") })
1419
1473
  });
1420
1474
  if (container.propsLineFrom > 0 && container.propsLineTo > 0) {
1421
1475
  const propsLine = state.doc.lineAt(container.propsLineFrom);
1422
1476
  decorations.push({
1423
1477
  from: propsLine.from,
1424
1478
  to: propsLine.to,
1425
- value: Decoration.replace({ widget: new ContainerPropsWidget(propsLine.text) })
1479
+ value: Decoration.replace({ widget: new ContainerPropsWidget(propsLine.text, container.quotePrefix ?? "") })
1426
1480
  });
1427
1481
  }
1428
1482
  if (container.endPos > 0) {
@@ -1430,10 +1484,11 @@ function findContainerDecorations(state) {
1430
1484
  decorations.push({
1431
1485
  from: endLine.from,
1432
1486
  to: endLine.to,
1433
- value: Decoration.replace({ widget: new ContainerCloseWidget() })
1487
+ value: Decoration.replace({ widget: new ContainerCloseWidget(endLine.text) })
1434
1488
  });
1435
1489
  }
1436
1490
  });
1491
+ decorations.sort((a, b) => a.from - b.from || b.to - a.to);
1437
1492
  return decorations;
1438
1493
  }
1439
1494
  /**
@@ -1462,11 +1517,19 @@ function handleEditButtonClick(event, view) {
1462
1517
  */
1463
1518
  var containerWidgetPlugin = ViewPlugin.fromClass(class {
1464
1519
  decorations;
1520
+ /** 上次构建装饰时的注册中心版本,用于感知异步注册(不能标 private,否则导出匿名类的 dts 报 TS4094) */
1521
+ registryVersion;
1465
1522
  constructor(view) {
1523
+ this.registryVersion = componentRegistry.getVersion();
1466
1524
  this.decorations = Decoration.set(findContainerDecorations(view.state), true);
1467
1525
  }
1468
1526
  update(update) {
1469
- if (update.docChanged) this.decorations = Decoration.set(findContainerDecorations(update.state), true);
1527
+ const registryChanged = componentRegistry.getVersion() !== this.registryVersion;
1528
+ const woken = update.transactions.some((tr) => tr.effects.some((e) => e.is(refreshContainers)));
1529
+ if (update.docChanged || registryChanged || woken) {
1530
+ this.registryVersion = componentRegistry.getVersion();
1531
+ this.decorations = Decoration.set(findContainerDecorations(update.state), true);
1532
+ }
1470
1533
  }
1471
1534
  }, {
1472
1535
  decorations: (value) => value.decorations,
@@ -2857,13 +2920,17 @@ function renderFormItemComponent(field) {
2857
2920
  }
2858
2921
  /**
2859
2922
  * 根据 fields 自动生成 Form.Item 列表
2923
+ * 初始值走 <Form initialValues> + 按次打开的 key:
2924
+ * initialValues 在字段实体注册时消费,天然避开“Modal 首次挂载前 setFieldsValue 落空”的时序竞态;
2925
+ * key 每次打开变化触发 Form 重挂载,确保切换到不同容器时重新取值。
2860
2926
  */
2861
- function renderAutoForm(form, fields) {
2927
+ function renderAutoForm(form, fields, initialValues, initKey) {
2862
2928
  if (!fields || fields.length === 0) return null;
2863
2929
  return /* @__PURE__ */ jsx(Form, {
2864
2930
  form,
2865
2931
  layout: "vertical",
2866
2932
  preserve: false,
2933
+ initialValues,
2867
2934
  children: fields.map((field) => {
2868
2935
  if (!field.componentType) return null;
2869
2936
  const formItemProps = {
@@ -2877,7 +2944,7 @@ function renderAutoForm(form, fields) {
2877
2944
  children: renderFormItemComponent(field)
2878
2945
  }, field.key);
2879
2946
  })
2880
- });
2947
+ }, initKey);
2881
2948
  }
2882
2949
  /** 字符串形式的历史属性值('true'/'false')还原为布尔,供 Switch 初始化 */
2883
2950
  function toFormInitialValue(field, value) {
@@ -2916,7 +2983,7 @@ function normalizeValue(field, value) {
2916
2983
  * 属性编辑弹窗组件
2917
2984
  * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
2918
2985
  */
2919
- function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
2986
+ function EditDialog({ visible, typeName, currentProps, initKey, onSave, onClose }) {
2920
2987
  const schema = componentRegistry.get(typeName);
2921
2988
  const [formProps, setFormProps] = useState({});
2922
2989
  const [form] = Form.useForm();
@@ -2932,16 +2999,19 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
2932
2999
  ...currentProps
2933
3000
  };
2934
3001
  }, [schema, currentProps]);
3002
+ const initialValues = useMemo(() => {
3003
+ const initial = {};
3004
+ for (const [key, value] of Object.entries(mergedProps)) {
3005
+ const field = schema?.fields?.find((f) => f.key === key);
3006
+ const formValue = toFormInitialValue(field ?? { key }, value);
3007
+ if (formValue !== void 0) initial[key] = formValue;
3008
+ }
3009
+ return initial;
3010
+ }, [mergedProps, schema]);
2935
3011
  useEffect(() => {
2936
3012
  if (visible) {
2937
3013
  setFormProps({});
2938
- const initial = {};
2939
- for (const [key, value] of Object.entries(mergedProps)) {
2940
- const field = schema?.fields?.find((f) => f.key === key);
2941
- const formValue = toFormInitialValue(field ?? { key }, value);
2942
- if (formValue !== void 0) initial[key] = formValue;
2943
- }
2944
- form.setFieldsValue(initial);
3014
+ form.setFieldsValue(initialValues);
2945
3015
  dirtyRef.current = false;
2946
3016
  }
2947
3017
  }, [visible, form]);
@@ -2996,7 +3066,7 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
2996
3066
  };
2997
3067
  const renderDialogContent = () => {
2998
3068
  if (schema.renderDialog) return schema.renderDialog(context);
2999
- if (schema.fields?.some((f) => f.componentType) && schema.fields) return renderAutoForm(form, schema.fields);
3069
+ if (schema.fields?.some((f) => f.componentType) && schema.fields) return renderAutoForm(form, schema.fields, initialValues, initKey);
3000
3070
  return null;
3001
3071
  };
3002
3072
  return /* @__PURE__ */ jsx(Modal, {
@@ -3023,19 +3093,20 @@ function writeContainerProps(props) {
3023
3093
  * 更新编辑器文档中的容器属性。
3024
3094
  * 已有属性行则整行替换;没有属性行则在容器头行后插入一行。
3025
3095
  * @param headerPos 容器头行(:::type)任意位置,用于无属性行时定位插入点
3096
+ * @param quotePrefix 容器所处的引用前缀(如 "> "),嵌套在引用中时插入的属性行需保持一致
3026
3097
  */
3027
- function updateContainerProps(view, headerPos, propsLineFrom, propsLineTo, newProps) {
3098
+ function updateContainerProps(view, headerPos, propsLineFrom, propsLineTo, newProps, quotePrefix = "") {
3028
3099
  const newPropsLine = writeContainerProps(newProps);
3029
3100
  if (propsLineTo > propsLineFrom) view.dispatch({ changes: {
3030
3101
  from: propsLineFrom,
3031
3102
  to: propsLineTo,
3032
- insert: newPropsLine
3103
+ insert: `${quotePrefix}${newPropsLine}`
3033
3104
  } });
3034
3105
  else {
3035
3106
  const headerLine = view.state.doc.lineAt(headerPos);
3036
3107
  view.dispatch({ changes: {
3037
3108
  from: headerLine.to,
3038
- insert: `\n${newPropsLine}`
3109
+ insert: `\n${quotePrefix}${newPropsLine}`
3039
3110
  } });
3040
3111
  }
3041
3112
  }
@@ -3160,6 +3231,7 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3160
3231
  const [dialogVisible, setDialogVisible] = useState(false);
3161
3232
  const [dialogTypeName, setDialogTypeName] = useState("");
3162
3233
  const [dialogContainer, setDialogContainer] = useState(null);
3234
+ const [dialogSeq, setDialogSeq] = useState(0);
3163
3235
  const app = App.useApp();
3164
3236
  const notify = useCallback((type, text) => {
3165
3237
  if (onNotify) {
@@ -3179,6 +3251,9 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3179
3251
  }, [onChange]);
3180
3252
  useImperativeHandle(ref, () => ({ getContent: () => content }), [content]);
3181
3253
  const registryVersion = useRegistryVersion();
3254
+ useEffect(() => {
3255
+ editorView?.dispatch({ effects: refreshContainers.of(null) });
3256
+ }, [registryVersion, editorView]);
3182
3257
  const extraToolbarItems = useMemo(() => {
3183
3258
  const merged = /* @__PURE__ */ new Map();
3184
3259
  for (const schema of componentRegistry.getAll()) merged.set(schema.name, schema);
@@ -3212,10 +3287,11 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3212
3287
  const handleEditContainer = useCallback((_view, container) => {
3213
3288
  setDialogTypeName(container.typeName);
3214
3289
  setDialogContainer(container);
3290
+ setDialogSeq((seq) => seq + 1);
3215
3291
  setDialogVisible(true);
3216
3292
  }, []);
3217
3293
  const handleDialogSave = useCallback((newProps) => {
3218
- if (editorView && dialogContainer) updateContainerProps(editorView, dialogContainer.startPos, dialogContainer.propsLineFrom, dialogContainer.propsLineTo, newProps);
3294
+ if (editorView && dialogContainer) updateContainerProps(editorView, dialogContainer.startPos, dialogContainer.propsLineFrom, dialogContainer.propsLineTo, newProps, dialogContainer.quotePrefix);
3219
3295
  }, [editorView, dialogContainer]);
3220
3296
  const handleDialogClose = useCallback(() => {
3221
3297
  setDialogVisible(false);
@@ -3252,6 +3328,7 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3252
3328
  visible: dialogVisible,
3253
3329
  typeName: dialogTypeName,
3254
3330
  currentProps: dialogContainer?.props ?? {},
3331
+ initKey: dialogSeq,
3255
3332
  onSave: handleDialogSave,
3256
3333
  onClose: handleDialogClose
3257
3334
  })