@incremark/core 0.3.1 → 0.3.2

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.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { I as IncrementalUpdate, P as ParsedBlock, D as DefinitionMap, F as FootnoteDefinitionMap, a as ParserState, B as BlockStatus } from './index-CWuosVAK.js';
2
2
  export { A as AstNode, M as MathOptions, b as ParserOptions } from './index-CWuosVAK.js';
3
- import { E as EngineParserOptions, I as IAstBuilder } from './types-N1b99kYB.js';
4
- export { a as EngineType, b as IncremarkPlugin, M as MarkedEngineExtension, c as MicromarkEngineExtension } from './types-N1b99kYB.js';
3
+ import { E as EngineParserOptions, I as IAstBuilder } from './types-B7GTGJc2.js';
4
+ export { a as EngineType, b as IncremarkPlugin, M as MarkedEngineExtension, c as MicromarkEngineExtension } from './types-B7GTGJc2.js';
5
5
  import { Root, RootContent, Text } from 'mdast';
6
6
  export { Root, RootContent } from 'mdast';
7
- export { M as MarkedAstBuilder } from './MarkedAstBuildter-DDP1An5M.js';
7
+ export { M as MarkedAstBuilder } from './MarkedAstBuildter-B2QhLKKy.js';
8
+ export { collectFootnoteReferences, traverseAst } from './utils/index.js';
8
9
  import 'micromark-util-types';
9
10
  import 'mdast-util-from-markdown';
10
11
  import 'marked';
@@ -39,7 +40,6 @@ declare class IncremarkParser {
39
40
  private lineOffsets;
40
41
  private completedBlocks;
41
42
  private pendingStartLine;
42
- private blockIdCounter;
43
43
  private context;
44
44
  private options;
45
45
  /** 边界检测器 */
@@ -53,7 +53,17 @@ declare class IncremarkParser {
53
53
  /** 上次 append 返回的 pending blocks,用于 getAst 复用 */
54
54
  private lastPendingBlocks;
55
55
  constructor(options?: IncremarkParserOptions);
56
+ /**
57
+ * 生成 block 的 id(直接使用 offset)
58
+ * @param startOffset - block 的起始偏移量
59
+ */
56
60
  private generateBlockId;
61
+ /**
62
+ * 生成 pending block 的稳定 id(基于 startOffset)
63
+ * pending blocks 在每次 append 时都会重新生成,使用 startOffset 确保 id 稳定
64
+ * @param startOffset - block 的起始偏移量,用作稳定的 id
65
+ */
66
+ private generatePendingBlockId;
57
67
  /**
58
68
  * 更新已完成的 blocks 中的 definitions 和 footnote definitions
59
69
  */
@@ -188,13 +198,22 @@ interface SourceBlock<T = unknown> {
188
198
  }
189
199
  /**
190
200
  * 显示用的 Block(转换后)
201
+ *
202
+ * 注意:DisplayBlock 的 status 含义与 SourceBlock 不同:
203
+ * - SourceBlock.status: 表示**解析器**的状态(解析是否完成)
204
+ * - DisplayBlock.status: 表示**打字机动画**的状态(动画是否完成)
205
+ *
206
+ * 在打字机模式下:
207
+ * - 即使解析器已完成(SourceBlock.status = 'completed'),
208
+ * 如果打字机动画还在进行中,DisplayBlock.status 仍然是 'pending'
209
+ * - 只有打字机动画完成后,DisplayBlock.status 才变成 'completed'
191
210
  */
192
211
  interface DisplayBlock<T = unknown> extends SourceBlock<T> {
193
212
  /** 用于显示的 AST 节点(可能是截断的) */
194
213
  displayNode: RootContent;
195
214
  /** 显示进度 0-1 */
196
215
  progress: number;
197
- /** 是否已完成显示 */
216
+ /** 是否已完成显示(打字机动画是否完成) */
198
217
  isDisplayComplete: boolean;
199
218
  }
200
219
  /**
@@ -326,7 +345,11 @@ declare class BlockTransformer<T = unknown> {
326
345
  constructor(options?: TransformerOptions);
327
346
  /**
328
347
  * 推入新的 blocks
329
- * 会自动过滤已存在的 blocks
348
+ *
349
+ * 逻辑:
350
+ * 1. 移除不在传入列表中的旧 blocks(处理容器增量解析等场景)
351
+ * 2. 如果 block ID 不存在,添加到 pending
352
+ * 3. 如果 block ID 已存在且内容变化,更新对应位置的 block
330
353
  */
331
354
  push(blocks: SourceBlock<T>[]): void;
332
355
  /**
@@ -352,6 +375,10 @@ declare class BlockTransformer<T = unknown> {
352
375
  /**
353
376
  * 获取用于渲染的 display blocks
354
377
  * 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
378
+ *
379
+ * 注意:DisplayBlock 的 status 表示的是**打字机动画状态**,而不是解析器的状态:
380
+ * - 'completed': 打字机动画已完成,内容已完全显示
381
+ * - 'pending': 打字机动画还在进行中,内容还在逐字显示
355
382
  */
356
383
  getDisplayBlocks(): DisplayBlock<T>[];
357
384
  /**
package/dist/index.js CHANGED
@@ -484,6 +484,15 @@ var BoundaryDetector = class {
484
484
  const wasInFencedCode = tempContext.inFencedCode;
485
485
  const wasInContainer = tempContext.inContainer;
486
486
  const wasContainerDepth = tempContext.containerDepth;
487
+ const prevLine = i > 0 ? lines[i - 1] : "";
488
+ const isSetextUnderline = i > 0 && isSetextHeadingUnderline(line, prevLine);
489
+ const hasExplicitBlockBoundary = detectFenceStart(line) || // 代码块 fence 开始
490
+ isHeading(line) || // 新标题开始
491
+ isThematicBreak(line);
492
+ if (!wasInFencedCode && !wasInContainer && hasExplicitBlockBoundary && !isSetextUnderline) {
493
+ stableLine = i - 1;
494
+ stableContext = { ...tempContext };
495
+ }
487
496
  tempContext = updateContext(line, tempContext, this.containerConfig);
488
497
  this.contextCache.set(i, { ...tempContext });
489
498
  if (wasInFencedCode && !tempContext.inFencedCode) {
@@ -547,7 +556,8 @@ var BoundaryDetector = class {
547
556
  if (isSetextHeadingUnderline(prevLine, lines[lineIndex - 2])) {
548
557
  return lineIndex - 1;
549
558
  }
550
- if (lineIndex >= lines.length - 1) {
559
+ const isLastLine = lineIndex >= lines.length - 1;
560
+ if (isLastLine) {
551
561
  return -1;
552
562
  }
553
563
  for (const checker of this.checkers) {
@@ -567,6 +577,20 @@ function isDefinitionNode(node) {
567
577
  function isFootnoteDefinitionNode(node) {
568
578
  return node.type === "footnoteDefinition";
569
579
  }
580
+ function collectFootnoteReferences(node) {
581
+ const references = [];
582
+ const seen = /* @__PURE__ */ new Set();
583
+ traverseAst(node, (n) => {
584
+ if (n.type === "footnoteReference" && "identifier" in n) {
585
+ const identifier = n.identifier;
586
+ if (!seen.has(identifier)) {
587
+ seen.add(identifier);
588
+ references.push(identifier);
589
+ }
590
+ }
591
+ });
592
+ return references;
593
+ }
570
594
  function traverseAst(node, visitor) {
571
595
  const stopEarly = visitor(node);
572
596
  if (stopEarly === true) {
@@ -2315,7 +2339,7 @@ var MarkedAstBuilder = class {
2315
2339
  const absoluteStart = startOffset + relativeStart;
2316
2340
  const absoluteEnd = startOffset + relativeEnd;
2317
2341
  blocks.push({
2318
- id: generateBlockId(),
2342
+ id: generateBlockId(absoluteStart),
2319
2343
  status,
2320
2344
  node,
2321
2345
  startOffset: absoluteStart,
@@ -2356,7 +2380,6 @@ var IncremarkParser = class {
2356
2380
  lineOffsets = [0];
2357
2381
  completedBlocks = [];
2358
2382
  pendingStartLine = 0;
2359
- blockIdCounter = 0;
2360
2383
  context;
2361
2384
  options;
2362
2385
  /** 边界检测器 */
@@ -2381,8 +2404,20 @@ var IncremarkParser = class {
2381
2404
  this.definitionManager = new DefinitionManager();
2382
2405
  this.footnoteManager = new FootnoteManager();
2383
2406
  }
2384
- generateBlockId() {
2385
- return `block-${++this.blockIdCounter}`;
2407
+ /**
2408
+ * 生成 block 的 id(直接使用 offset)
2409
+ * @param startOffset - block 的起始偏移量
2410
+ */
2411
+ generateBlockId(startOffset) {
2412
+ return String(startOffset);
2413
+ }
2414
+ /**
2415
+ * 生成 pending block 的稳定 id(基于 startOffset)
2416
+ * pending blocks 在每次 append 时都会重新生成,使用 startOffset 确保 id 稳定
2417
+ * @param startOffset - block 的起始偏移量,用作稳定的 id
2418
+ */
2419
+ generatePendingBlockId(startOffset) {
2420
+ return String(startOffset);
2386
2421
  }
2387
2422
  /**
2388
2423
  * 更新已完成的 blocks 中的 definitions 和 footnote definitions
@@ -2454,9 +2489,28 @@ var IncremarkParser = class {
2454
2489
  const stableText = this.lines.slice(this.pendingStartLine, stableBoundary + 1).join("\n");
2455
2490
  const stableOffset = this.getLineOffset(this.pendingStartLine);
2456
2491
  const ast = this.astBuilder.parse(stableText);
2457
- const newBlocks = this.astBuilder.nodesToBlocks(ast.children, stableOffset, stableText, "completed", () => this.generateBlockId());
2492
+ const newBlocks = this.astBuilder.nodesToBlocks(ast.children, stableOffset, stableText, "completed", (offset) => this.generateBlockId(offset));
2493
+ const blocksToRemove = [];
2494
+ for (const newBlock of newBlocks) {
2495
+ for (const existingBlock of this.completedBlocks) {
2496
+ const isOverlapping = newBlock.startOffset >= existingBlock.startOffset && newBlock.startOffset < existingBlock.endOffset;
2497
+ const isOverlappingReverse = existingBlock.startOffset >= newBlock.startOffset && existingBlock.startOffset < newBlock.endOffset;
2498
+ if (isOverlapping || isOverlappingReverse) {
2499
+ if (newBlock.id !== existingBlock.id) {
2500
+ blocksToRemove.push(existingBlock);
2501
+ }
2502
+ }
2503
+ }
2504
+ }
2505
+ if (blocksToRemove.length > 0) {
2506
+ const idsToRemove = new Set(blocksToRemove.map((b) => b.id));
2507
+ this.completedBlocks = this.completedBlocks.filter((b) => !idsToRemove.has(b.id));
2508
+ }
2458
2509
  this.completedBlocks.push(...newBlocks);
2459
2510
  update.completed = newBlocks;
2511
+ if (blocksToRemove.length > 0) {
2512
+ update.updated = blocksToRemove;
2513
+ }
2460
2514
  this.updateDefinitionsFromCompletedBlocks(newBlocks);
2461
2515
  this.footnoteManager.collectReferencesFromCompletedBlocks(newBlocks);
2462
2516
  this.boundaryDetector.clearContextCache(this.pendingStartLine);
@@ -2468,7 +2522,13 @@ var IncremarkParser = class {
2468
2522
  if (pendingText.trim()) {
2469
2523
  const pendingOffset = this.getLineOffset(this.pendingStartLine);
2470
2524
  const ast = this.astBuilder.parse(pendingText);
2471
- update.pending = this.astBuilder.nodesToBlocks(ast.children, pendingOffset, pendingText, "pending", () => this.generateBlockId());
2525
+ update.pending = this.astBuilder.nodesToBlocks(
2526
+ ast.children,
2527
+ pendingOffset,
2528
+ pendingText,
2529
+ "pending",
2530
+ (offset) => this.generatePendingBlockId(offset)
2531
+ );
2472
2532
  }
2473
2533
  }
2474
2534
  this.lastPendingBlocks = update.pending;
@@ -2528,7 +2588,7 @@ var IncremarkParser = class {
2528
2588
  remainingOffset,
2529
2589
  remainingText,
2530
2590
  "completed",
2531
- () => this.generateBlockId()
2591
+ (offset) => this.generateBlockId(offset)
2532
2592
  );
2533
2593
  this.completedBlocks.push(...finalBlocks);
2534
2594
  update.completed = finalBlocks;
@@ -2619,7 +2679,6 @@ var IncremarkParser = class {
2619
2679
  this.lineOffsets = [0];
2620
2680
  this.completedBlocks = [];
2621
2681
  this.pendingStartLine = 0;
2622
- this.blockIdCounter = 0;
2623
2682
  this.context = createInitialContext();
2624
2683
  this.lastPendingBlocks = [];
2625
2684
  this.definitionManager.clear();
@@ -2885,22 +2944,78 @@ var BlockTransformer = class {
2885
2944
  }
2886
2945
  /**
2887
2946
  * 推入新的 blocks
2888
- * 会自动过滤已存在的 blocks
2947
+ *
2948
+ * 逻辑:
2949
+ * 1. 移除不在传入列表中的旧 blocks(处理容器增量解析等场景)
2950
+ * 2. 如果 block ID 不存在,添加到 pending
2951
+ * 3. 如果 block ID 已存在且内容变化,更新对应位置的 block
2889
2952
  */
2890
2953
  push(blocks) {
2954
+ const inputIds = new Set(blocks.map((b) => b.id));
2891
2955
  const existingIds = this.getAllBlockIds();
2956
+ let hasRemovals = false;
2957
+ const completedBefore = this.state.completedBlocks.length;
2958
+ this.state.completedBlocks = this.state.completedBlocks.filter((b) => inputIds.has(b.id));
2959
+ if (this.state.completedBlocks.length < completedBefore) {
2960
+ hasRemovals = true;
2961
+ }
2962
+ if (this.state.currentBlock && !inputIds.has(this.state.currentBlock.id)) {
2963
+ this.state.currentBlock = null;
2964
+ this.state.currentProgress = 0;
2965
+ this.chunks = [];
2966
+ this.clearCache();
2967
+ hasRemovals = true;
2968
+ }
2969
+ const pendingBefore = this.state.pendingBlocks.length;
2970
+ this.state.pendingBlocks = this.state.pendingBlocks.filter((b) => inputIds.has(b.id));
2971
+ if (this.state.pendingBlocks.length < pendingBefore) {
2972
+ hasRemovals = true;
2973
+ }
2974
+ if (hasRemovals && !this.state.currentBlock && this.state.pendingBlocks.length > 0) {
2975
+ this.startIfNeeded();
2976
+ }
2892
2977
  const newBlocks = blocks.filter((b) => !existingIds.has(b.id));
2893
2978
  if (newBlocks.length > 0) {
2894
2979
  this.state.pendingBlocks.push(...newBlocks);
2895
2980
  this.startIfNeeded();
2896
2981
  }
2897
- if (this.state.currentBlock) {
2898
- const updated = blocks.find((b) => b.id === this.state.currentBlock.id);
2899
- if (updated && updated.node !== this.state.currentBlock.node) {
2900
- this.handleContentChange(this.state.currentBlock.node, updated.node, true);
2901
- this.state.currentBlock = updated;
2982
+ for (const block of blocks) {
2983
+ if (!existingIds.has(block.id)) continue;
2984
+ if (this.state.currentBlock?.id === block.id) {
2985
+ const total = this.getTotalChars();
2986
+ const isComplete = this.state.currentProgress >= total;
2987
+ if (isComplete) {
2988
+ this.state.completedBlocks.push(block);
2989
+ this.state.currentBlock = null;
2990
+ this.state.currentProgress = 0;
2991
+ this.chunks = [];
2992
+ this.clearCache();
2993
+ this.processNext();
2994
+ } else if (this.state.currentBlock.node !== block.node) {
2995
+ this.handleContentChange(this.state.currentBlock.node, block.node, true);
2996
+ this.state.currentBlock = block;
2997
+ }
2998
+ continue;
2999
+ }
3000
+ const completedIndex = this.state.completedBlocks.findIndex((b) => b.id === block.id);
3001
+ if (completedIndex !== -1) {
3002
+ if (this.state.completedBlocks[completedIndex].node !== block.node) {
3003
+ this.state.completedBlocks[completedIndex] = block;
3004
+ this.emit();
3005
+ }
3006
+ continue;
3007
+ }
3008
+ const pendingIndex = this.state.pendingBlocks.findIndex((b) => b.id === block.id);
3009
+ if (pendingIndex !== -1) {
3010
+ if (this.state.pendingBlocks[pendingIndex].node !== block.node) {
3011
+ this.state.pendingBlocks[pendingIndex] = block;
3012
+ }
3013
+ continue;
2902
3014
  }
2903
3015
  }
3016
+ if (hasRemovals) {
3017
+ this.emit();
3018
+ }
2904
3019
  }
2905
3020
  /**
2906
3021
  * 更新指定 block(用于 pending block 内容增加时)
@@ -2909,6 +3024,18 @@ var BlockTransformer = class {
2909
3024
  if (this.state.currentBlock?.id === block.id) {
2910
3025
  this.handleContentChange(this.state.currentBlock.node, block.node, false);
2911
3026
  this.state.currentBlock = block;
3027
+ return;
3028
+ }
3029
+ const completedIndex = this.state.completedBlocks.findIndex((b) => b.id === block.id);
3030
+ if (completedIndex !== -1) {
3031
+ this.state.completedBlocks[completedIndex] = block;
3032
+ this.emit();
3033
+ return;
3034
+ }
3035
+ const pendingIndex = this.state.pendingBlocks.findIndex((b) => b.id === block.id);
3036
+ if (pendingIndex !== -1) {
3037
+ this.state.pendingBlocks[pendingIndex] = block;
3038
+ return;
2912
3039
  }
2913
3040
  }
2914
3041
  /**
@@ -2966,12 +3093,18 @@ var BlockTransformer = class {
2966
3093
  /**
2967
3094
  * 获取用于渲染的 display blocks
2968
3095
  * 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
3096
+ *
3097
+ * 注意:DisplayBlock 的 status 表示的是**打字机动画状态**,而不是解析器的状态:
3098
+ * - 'completed': 打字机动画已完成,内容已完全显示
3099
+ * - 'pending': 打字机动画还在进行中,内容还在逐字显示
2969
3100
  */
2970
3101
  getDisplayBlocks() {
2971
3102
  const result = [];
2972
3103
  for (const block of this.state.completedBlocks) {
2973
3104
  result.push({
2974
3105
  ...block,
3106
+ // 打字机动画已完成,状态为 completed
3107
+ status: "completed",
2975
3108
  displayNode: block.node,
2976
3109
  progress: 1,
2977
3110
  isDisplayComplete: true
@@ -2984,6 +3117,8 @@ var BlockTransformer = class {
2984
3117
  }
2985
3118
  result.push({
2986
3119
  ...this.state.currentBlock,
3120
+ // 打字机动画进行中,状态为 pending
3121
+ status: "pending",
2987
3122
  displayNode: this.cachedDisplayNode || { type: "paragraph", children: [] },
2988
3123
  progress: total > 0 ? this.state.currentProgress / total : 1,
2989
3124
  isDisplayComplete: false
@@ -3410,6 +3545,6 @@ function createPlugin(name, matcher, options = {}) {
3410
3545
  };
3411
3546
  }
3412
3547
 
3413
- export { BlockTransformer, IncremarkParser, MarkedAstBuilder, allPlugins, cloneNode, codeBlockPlugin, countChars, createBlockTransformer, createIncremarkParser, createPlugin, defaultPlugins, imagePlugin, mathPlugin, mermaidPlugin, sliceAst, thematicBreakPlugin };
3548
+ export { BlockTransformer, IncremarkParser, MarkedAstBuilder, allPlugins, cloneNode, codeBlockPlugin, collectFootnoteReferences, countChars, createBlockTransformer, createIncremarkParser, createPlugin, defaultPlugins, imagePlugin, mathPlugin, mermaidPlugin, sliceAst, thematicBreakPlugin, traverseAst };
3414
3549
  //# sourceMappingURL=index.js.map
3415
3550
  //# sourceMappingURL=index.js.map