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.
@@ -9,6 +9,8 @@ interface EditDialogProps {
9
9
  typeName: string;
10
10
  /** 当前属性值 */
11
11
  currentProps: ContainerProps;
12
+ /** 每次打开递增的序号:作为 Form 的 key,切换容器时触发重挂载重新取 initialValues */
13
+ initKey: number;
12
14
  /** 保存回调 */
13
15
  onSave: (props: ContainerProps) => void;
14
16
  /** 关闭回调 */
@@ -18,5 +20,5 @@ interface EditDialogProps {
18
20
  * 属性编辑弹窗组件
19
21
  * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
20
22
  */
21
- export default function EditDialog({ visible, typeName, currentProps, onSave, onClose }: EditDialogProps): import('react').JSX.Element | null;
23
+ export default function EditDialog({ visible, typeName, currentProps, initKey, onSave, onClose }: EditDialogProps): import('react').JSX.Element | null;
22
24
  export {};
@@ -1,6 +1,14 @@
1
1
  import { ViewPlugin, EditorView, DecorationSet } from '@codemirror/view';
2
- import { EditorState, Facet } from '@codemirror/state';
2
+ import { EditorState, Facet, Transaction } from '@codemirror/state';
3
3
  import { ContainerBlock } from '../../utils/parseContainer';
4
+ /**
5
+ * 唤醒容器装饰重建的状态 effect(不改变文档)。
6
+ * 编辑按钮是否展示取决于容器类型是否已注册,而宿主常在编辑器挂载之后才
7
+ * 通过 useRegisterToolbar 异步注册 schema;注册不是 CodeMirror 事务,默认不会触发
8
+ * ViewPlugin.update,导致初始化时烘焙的 registered=false 无法刷新(需手动改一个字才出现)。
9
+ * 宿主在注册中心版本变化时 dispatch 本 effect,驱动插件重新求值 registered。
10
+ */
11
+ export declare const refreshContainers: import('@codemirror/state').StateEffectType<null>;
4
12
  /**
5
13
  * 编辑容器属性的回调函数类型
6
14
  * @param view 当前编辑器实例
@@ -18,8 +26,11 @@ export declare const containerEditCallback: Facet<OnEditContainer, OnEditContain
18
26
  */
19
27
  export declare const containerWidgetPlugin: ViewPlugin<{
20
28
  decorations: DecorationSet;
29
+ /** 上次构建装饰时的注册中心版本,用于感知异步注册(不能标 private,否则导出匿名类的 dts 报 TS4094) */
30
+ registryVersion: number;
21
31
  update(update: {
22
32
  docChanged: boolean;
23
33
  state: EditorState;
34
+ transactions: readonly Transaction[];
24
35
  }): void;
25
36
  }, undefined>;
package/dist/index.cjs CHANGED
@@ -1278,8 +1278,25 @@ function parseContainerProps(line) {
1278
1278
  }
1279
1279
  }
1280
1280
  /**
1281
+ * 解析行首的引用(blockquote)嵌套前缀。
1282
+ * markdown-it 解析引用内部内容时会剥掉一层 "> ",因此 "> :::badge" 在渲染侧
1283
+ * 等同于 ":::badge";编辑器侧需同样识别,否则嵌套引用的容器匹配不到标签。
1284
+ * 返回 [前缀文本(含 "> " 及尾随空格), 去掉前缀后的内容],无前缀时返回 ['', 原文本]。
1285
+ */
1286
+ function splitQuotePrefix(line) {
1287
+ const m = line.match(/^((?:>\s*)+)(\S.*)$/);
1288
+ if (!m) return ["", line];
1289
+ return [m[1], m[2]];
1290
+ }
1291
+ /** 引用嵌套深度("> > x" 为 2,无前缀为 0) */
1292
+ function quoteDepth(prefix) {
1293
+ return (prefix.match(/>/g) ?? []).length;
1294
+ }
1295
+ /**
1281
1296
  * 在编辑器文档中查找容器块。
1282
1297
  * 单次遍历 + 预累计行偏移,复杂度 O(行数)。
1298
+ * 支持整体嵌套在引用中的容器:头行、属性行、闭合行需具有相同的 "> " 前缀深度,
1299
+ * 避免跨引用层级错误配对。闭合 ::: 改为按栈配对,嵌套容器不会被内层闭合误截断。
1283
1300
  */
1284
1301
  function findContainers(docText) {
1285
1302
  const containers = [];
@@ -1290,34 +1307,53 @@ function findContainers(docText) {
1290
1307
  offsets[i] = pos;
1291
1308
  pos += lines[i].length + 1;
1292
1309
  }
1310
+ const stack = [];
1293
1311
  for (let i = 0; i < lines.length; i++) {
1294
- const match = lines[i].match(/^:::(\w+)$/);
1312
+ const [prefix, body] = splitQuotePrefix(lines[i]);
1313
+ const depth = quoteDepth(prefix);
1314
+ if (/^:::$/.test(body)) {
1315
+ let top = stack.length - 1;
1316
+ while (top >= 0 && stack[top].depth !== depth) top--;
1317
+ if (top >= 0) {
1318
+ stack[top].block.endPos = offsets[i];
1319
+ stack.length = top;
1320
+ }
1321
+ continue;
1322
+ }
1323
+ const match = body.match(/^:::(\w+)$/);
1295
1324
  if (!match) continue;
1296
1325
  const typeName = match[1];
1297
1326
  const headerPos = offsets[i];
1298
1327
  let props = {};
1299
1328
  let propsLineFrom = 0;
1300
1329
  let propsLineTo = 0;
1301
- const propsLine = lines[i + 1];
1302
- if (propsLine && propsLine.trim().startsWith("{") && propsLine.trim().endsWith("}")) {
1303
- props = parseContainerProps(propsLine);
1304
- propsLineFrom = offsets[i + 1];
1305
- propsLineTo = propsLineFrom + propsLine.length;
1306
- }
1307
- let endPos = 0;
1308
- for (let j = i + 1; j < lines.length; j++) if (lines[j].trim() === ":::") {
1309
- endPos = offsets[j];
1310
- i = j;
1311
- break;
1330
+ let propsQuotePrefix = "";
1331
+ const nextLine = lines[i + 1];
1332
+ if (nextLine) {
1333
+ const [nextPrefix, nextBody] = splitQuotePrefix(nextLine);
1334
+ if ((quoteDepth(nextPrefix) === depth || nextPrefix === "") && nextBody.trimStart().startsWith("{") && nextBody.trimEnd().endsWith("}")) {
1335
+ props = parseContainerProps(nextBody);
1336
+ propsLineFrom = offsets[i + 1];
1337
+ propsLineTo = propsLineFrom + nextLine.length;
1338
+ propsQuotePrefix = nextPrefix;
1339
+ i += 1;
1340
+ }
1312
1341
  }
1313
- containers.push({
1342
+ const block = {
1314
1343
  startPos: headerPos,
1315
1344
  typeName,
1316
1345
  propsLineFrom,
1317
1346
  propsLineTo,
1318
1347
  props,
1319
- endPos,
1320
- registered: componentRegistry.has(typeName)
1348
+ endPos: 0,
1349
+ registered: componentRegistry.has(typeName),
1350
+ quotePrefix: prefix,
1351
+ propsQuotePrefix
1352
+ };
1353
+ containers.push(block);
1354
+ stack.push({
1355
+ block,
1356
+ depth
1321
1357
  });
1322
1358
  }
1323
1359
  return containers;
@@ -1325,6 +1361,14 @@ function findContainers(docText) {
1325
1361
  //#endregion
1326
1362
  //#region src/components/Editor/containerWidget.ts
1327
1363
  /**
1364
+ * 唤醒容器装饰重建的状态 effect(不改变文档)。
1365
+ * 编辑按钮是否展示取决于容器类型是否已注册,而宿主常在编辑器挂载之后才
1366
+ * 通过 useRegisterToolbar 异步注册 schema;注册不是 CodeMirror 事务,默认不会触发
1367
+ * ViewPlugin.update,导致初始化时烘焙的 registered=false 无法刷新(需手动改一个字才出现)。
1368
+ * 宿主在注册中心版本变化时 dispatch 本 effect,驱动插件重新求值 registered。
1369
+ */
1370
+ var refreshContainers = _codemirror_state.StateEffect.define();
1371
+ /**
1328
1372
  * 自定义 facet 用于传递编辑回调
1329
1373
  */
1330
1374
  var containerEditCallback = _codemirror_state.Facet.define({ combine: (values) => values.length ? values[values.length - 1] : null });
@@ -1337,18 +1381,20 @@ var ContainerHeaderWidget = class extends _codemirror_view.WidgetType {
1337
1381
  typeName;
1338
1382
  containerIndex;
1339
1383
  registered;
1340
- constructor(typeName, containerIndex, registered) {
1384
+ quotePrefix;
1385
+ constructor(typeName, containerIndex, registered, quotePrefix) {
1341
1386
  super();
1342
1387
  this.typeName = typeName;
1343
1388
  this.containerIndex = containerIndex;
1344
1389
  this.registered = registered;
1390
+ this.quotePrefix = quotePrefix;
1345
1391
  }
1346
1392
  toDOM() {
1347
1393
  const container = document.createElement("span");
1348
1394
  container.className = "container-widget-header";
1349
1395
  const text = document.createElement("span");
1350
1396
  text.className = "container-widget-text";
1351
- text.textContent = `:::${this.typeName}`;
1397
+ text.textContent = `${this.quotePrefix}:::${this.typeName}`;
1352
1398
  container.appendChild(text);
1353
1399
  if (this.registered) {
1354
1400
  const button = document.createElement("button");
@@ -1362,7 +1408,7 @@ var ContainerHeaderWidget = class extends _codemirror_view.WidgetType {
1362
1408
  return container;
1363
1409
  }
1364
1410
  eq(other) {
1365
- return this.typeName === other.typeName && this.containerIndex === other.containerIndex && this.registered === other.registered;
1411
+ return this.typeName === other.typeName && this.containerIndex === other.containerIndex && this.registered === other.registered && this.quotePrefix === other.quotePrefix;
1366
1412
  }
1367
1413
  ignoreEvent() {
1368
1414
  return false;
@@ -1370,38 +1416,46 @@ var ContainerHeaderWidget = class extends _codemirror_view.WidgetType {
1370
1416
  };
1371
1417
  /**
1372
1418
  * 只读的容器属性行 Widget(显示 { key: value, ... })
1419
+ * 嵌套在引用中时原始行带 "> " 前缀,展示时剔除,与头行 widget 风格一致
1373
1420
  */
1374
1421
  var ContainerPropsWidget = class extends _codemirror_view.WidgetType {
1375
1422
  propsText;
1376
- constructor(propsText) {
1423
+ quotePrefix;
1424
+ constructor(propsText, quotePrefix) {
1377
1425
  super();
1378
1426
  this.propsText = propsText;
1427
+ this.quotePrefix = quotePrefix;
1379
1428
  }
1380
1429
  toDOM() {
1381
1430
  const span = document.createElement("span");
1382
1431
  span.className = "container-widget-props";
1383
- span.textContent = this.propsText;
1432
+ span.textContent = this.propsText.startsWith(this.quotePrefix) ? this.propsText.slice(this.quotePrefix.length) : this.propsText;
1384
1433
  return span;
1385
1434
  }
1386
1435
  eq(other) {
1387
- return this.propsText === other.propsText;
1436
+ return this.propsText === other.propsText && this.quotePrefix === other.quotePrefix;
1388
1437
  }
1389
1438
  ignoreEvent() {
1390
1439
  return false;
1391
1440
  }
1392
1441
  };
1393
1442
  /**
1394
- * 只读的容器结束行 Widget(显示 :::)
1443
+ * 只读的容器结束行 Widget(显示 :::,嵌套引用时保留 "> " 前缀)
1395
1444
  */
1396
1445
  var ContainerCloseWidget = class extends _codemirror_view.WidgetType {
1446
+ text;
1447
+ constructor(text) {
1448
+ super();
1449
+ this.text = text;
1450
+ }
1397
1451
  toDOM() {
1398
1452
  const span = document.createElement("span");
1399
1453
  span.className = "container-widget-text";
1400
- span.textContent = ":::";
1454
+ span.textContent = this.text;
1401
1455
  return span;
1402
1456
  }
1403
- eq() {
1404
- return true;
1457
+ eq(other) {
1458
+ return this.text === other.text;
1405
1459
  }
1406
1460
  ignoreEvent() {
1407
1461
  return false;
@@ -1419,14 +1473,14 @@ function findContainerDecorations(state) {
1419
1473
  decorations.push({
1420
1474
  from: headerLine.from,
1421
1475
  to: headerLine.to,
1422
- value: _codemirror_view.Decoration.replace({ widget: new ContainerHeaderWidget(container.typeName, index, container.registered) })
1476
+ value: _codemirror_view.Decoration.replace({ widget: new ContainerHeaderWidget(container.typeName, index, container.registered, container.quotePrefix ?? "") })
1423
1477
  });
1424
1478
  if (container.propsLineFrom > 0 && container.propsLineTo > 0) {
1425
1479
  const propsLine = state.doc.lineAt(container.propsLineFrom);
1426
1480
  decorations.push({
1427
1481
  from: propsLine.from,
1428
1482
  to: propsLine.to,
1429
- value: _codemirror_view.Decoration.replace({ widget: new ContainerPropsWidget(propsLine.text) })
1483
+ value: _codemirror_view.Decoration.replace({ widget: new ContainerPropsWidget(propsLine.text, container.quotePrefix ?? "") })
1430
1484
  });
1431
1485
  }
1432
1486
  if (container.endPos > 0) {
@@ -1434,10 +1488,11 @@ function findContainerDecorations(state) {
1434
1488
  decorations.push({
1435
1489
  from: endLine.from,
1436
1490
  to: endLine.to,
1437
- value: _codemirror_view.Decoration.replace({ widget: new ContainerCloseWidget() })
1491
+ value: _codemirror_view.Decoration.replace({ widget: new ContainerCloseWidget(endLine.text) })
1438
1492
  });
1439
1493
  }
1440
1494
  });
1495
+ decorations.sort((a, b) => a.from - b.from || b.to - a.to);
1441
1496
  return decorations;
1442
1497
  }
1443
1498
  /**
@@ -1466,11 +1521,19 @@ function handleEditButtonClick(event, view) {
1466
1521
  */
1467
1522
  var containerWidgetPlugin = _codemirror_view.ViewPlugin.fromClass(class {
1468
1523
  decorations;
1524
+ /** 上次构建装饰时的注册中心版本,用于感知异步注册(不能标 private,否则导出匿名类的 dts 报 TS4094) */
1525
+ registryVersion;
1469
1526
  constructor(view) {
1527
+ this.registryVersion = componentRegistry.getVersion();
1470
1528
  this.decorations = _codemirror_view.Decoration.set(findContainerDecorations(view.state), true);
1471
1529
  }
1472
1530
  update(update) {
1473
- if (update.docChanged) this.decorations = _codemirror_view.Decoration.set(findContainerDecorations(update.state), true);
1531
+ const registryChanged = componentRegistry.getVersion() !== this.registryVersion;
1532
+ const woken = update.transactions.some((tr) => tr.effects.some((e) => e.is(refreshContainers)));
1533
+ if (update.docChanged || registryChanged || woken) {
1534
+ this.registryVersion = componentRegistry.getVersion();
1535
+ this.decorations = _codemirror_view.Decoration.set(findContainerDecorations(update.state), true);
1536
+ }
1474
1537
  }
1475
1538
  }, {
1476
1539
  decorations: (value) => value.decorations,
@@ -2861,13 +2924,17 @@ function renderFormItemComponent(field) {
2861
2924
  }
2862
2925
  /**
2863
2926
  * 根据 fields 自动生成 Form.Item 列表
2927
+ * 初始值走 <Form initialValues> + 按次打开的 key:
2928
+ * initialValues 在字段实体注册时消费,天然避开“Modal 首次挂载前 setFieldsValue 落空”的时序竞态;
2929
+ * key 每次打开变化触发 Form 重挂载,确保切换到不同容器时重新取值。
2864
2930
  */
2865
- function renderAutoForm(form, fields) {
2931
+ function renderAutoForm(form, fields, initialValues, initKey) {
2866
2932
  if (!fields || fields.length === 0) return null;
2867
2933
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Form, {
2868
2934
  form,
2869
2935
  layout: "vertical",
2870
2936
  preserve: false,
2937
+ initialValues,
2871
2938
  children: fields.map((field) => {
2872
2939
  if (!field.componentType) return null;
2873
2940
  const formItemProps = {
@@ -2881,7 +2948,7 @@ function renderAutoForm(form, fields) {
2881
2948
  children: renderFormItemComponent(field)
2882
2949
  }, field.key);
2883
2950
  })
2884
- });
2951
+ }, initKey);
2885
2952
  }
2886
2953
  /** 字符串形式的历史属性值('true'/'false')还原为布尔,供 Switch 初始化 */
2887
2954
  function toFormInitialValue(field, value) {
@@ -2920,7 +2987,7 @@ function normalizeValue(field, value) {
2920
2987
  * 属性编辑弹窗组件
2921
2988
  * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
2922
2989
  */
2923
- function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
2990
+ function EditDialog({ visible, typeName, currentProps, initKey, onSave, onClose }) {
2924
2991
  const schema = componentRegistry.get(typeName);
2925
2992
  const [formProps, setFormProps] = (0, react.useState)({});
2926
2993
  const [form] = antd.Form.useForm();
@@ -2936,16 +3003,19 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
2936
3003
  ...currentProps
2937
3004
  };
2938
3005
  }, [schema, currentProps]);
3006
+ const initialValues = (0, react.useMemo)(() => {
3007
+ const initial = {};
3008
+ for (const [key, value] of Object.entries(mergedProps)) {
3009
+ const field = schema?.fields?.find((f) => f.key === key);
3010
+ const formValue = toFormInitialValue(field ?? { key }, value);
3011
+ if (formValue !== void 0) initial[key] = formValue;
3012
+ }
3013
+ return initial;
3014
+ }, [mergedProps, schema]);
2939
3015
  (0, react.useEffect)(() => {
2940
3016
  if (visible) {
2941
3017
  setFormProps({});
2942
- const initial = {};
2943
- for (const [key, value] of Object.entries(mergedProps)) {
2944
- const field = schema?.fields?.find((f) => f.key === key);
2945
- const formValue = toFormInitialValue(field ?? { key }, value);
2946
- if (formValue !== void 0) initial[key] = formValue;
2947
- }
2948
- form.setFieldsValue(initial);
3018
+ form.setFieldsValue(initialValues);
2949
3019
  dirtyRef.current = false;
2950
3020
  }
2951
3021
  }, [visible, form]);
@@ -3000,7 +3070,7 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
3000
3070
  };
3001
3071
  const renderDialogContent = () => {
3002
3072
  if (schema.renderDialog) return schema.renderDialog(context);
3003
- if (schema.fields?.some((f) => f.componentType) && schema.fields) return renderAutoForm(form, schema.fields);
3073
+ if (schema.fields?.some((f) => f.componentType) && schema.fields) return renderAutoForm(form, schema.fields, initialValues, initKey);
3004
3074
  return null;
3005
3075
  };
3006
3076
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Modal, {
@@ -3027,19 +3097,20 @@ function writeContainerProps(props) {
3027
3097
  * 更新编辑器文档中的容器属性。
3028
3098
  * 已有属性行则整行替换;没有属性行则在容器头行后插入一行。
3029
3099
  * @param headerPos 容器头行(:::type)任意位置,用于无属性行时定位插入点
3100
+ * @param quotePrefix 容器所处的引用前缀(如 "> "),嵌套在引用中时插入的属性行需保持一致
3030
3101
  */
3031
- function updateContainerProps(view, headerPos, propsLineFrom, propsLineTo, newProps) {
3102
+ function updateContainerProps(view, headerPos, propsLineFrom, propsLineTo, newProps, quotePrefix = "") {
3032
3103
  const newPropsLine = writeContainerProps(newProps);
3033
3104
  if (propsLineTo > propsLineFrom) view.dispatch({ changes: {
3034
3105
  from: propsLineFrom,
3035
3106
  to: propsLineTo,
3036
- insert: newPropsLine
3107
+ insert: `${quotePrefix}${newPropsLine}`
3037
3108
  } });
3038
3109
  else {
3039
3110
  const headerLine = view.state.doc.lineAt(headerPos);
3040
3111
  view.dispatch({ changes: {
3041
3112
  from: headerLine.to,
3042
- insert: `\n${newPropsLine}`
3113
+ insert: `\n${quotePrefix}${newPropsLine}`
3043
3114
  } });
3044
3115
  }
3045
3116
  }
@@ -3164,6 +3235,7 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3164
3235
  const [dialogVisible, setDialogVisible] = (0, react.useState)(false);
3165
3236
  const [dialogTypeName, setDialogTypeName] = (0, react.useState)("");
3166
3237
  const [dialogContainer, setDialogContainer] = (0, react.useState)(null);
3238
+ const [dialogSeq, setDialogSeq] = (0, react.useState)(0);
3167
3239
  const app = antd.App.useApp();
3168
3240
  const notify = (0, react.useCallback)((type, text) => {
3169
3241
  if (onNotify) {
@@ -3183,6 +3255,9 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3183
3255
  }, [onChange]);
3184
3256
  (0, react.useImperativeHandle)(ref, () => ({ getContent: () => content }), [content]);
3185
3257
  const registryVersion = useRegistryVersion();
3258
+ (0, react.useEffect)(() => {
3259
+ editorView?.dispatch({ effects: refreshContainers.of(null) });
3260
+ }, [registryVersion, editorView]);
3186
3261
  const extraToolbarItems = (0, react.useMemo)(() => {
3187
3262
  const merged = /* @__PURE__ */ new Map();
3188
3263
  for (const schema of componentRegistry.getAll()) merged.set(schema.name, schema);
@@ -3216,10 +3291,11 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3216
3291
  const handleEditContainer = (0, react.useCallback)((_view, container) => {
3217
3292
  setDialogTypeName(container.typeName);
3218
3293
  setDialogContainer(container);
3294
+ setDialogSeq((seq) => seq + 1);
3219
3295
  setDialogVisible(true);
3220
3296
  }, []);
3221
3297
  const handleDialogSave = (0, react.useCallback)((newProps) => {
3222
- if (editorView && dialogContainer) updateContainerProps(editorView, dialogContainer.startPos, dialogContainer.propsLineFrom, dialogContainer.propsLineTo, newProps);
3298
+ if (editorView && dialogContainer) updateContainerProps(editorView, dialogContainer.startPos, dialogContainer.propsLineFrom, dialogContainer.propsLineTo, newProps, dialogContainer.quotePrefix);
3223
3299
  }, [editorView, dialogContainer]);
3224
3300
  const handleDialogClose = (0, react.useCallback)(() => {
3225
3301
  setDialogVisible(false);
@@ -3256,6 +3332,7 @@ var MarkdownEditorInner = ({ defaultValue = "", value, onChange, showPreview = t
3256
3332
  visible: dialogVisible,
3257
3333
  typeName: dialogTypeName,
3258
3334
  currentProps: dialogContainer?.props ?? {},
3335
+ initKey: dialogSeq,
3259
3336
  onSave: handleDialogSave,
3260
3337
  onClose: handleDialogClose
3261
3338
  })