@incremark/core 0.3.0 → 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.
@@ -140,6 +140,30 @@ interface ParserState {
140
140
  definitions: DefinitionMap;
141
141
  footnoteDefinitions: FootnoteDefinitionMap;
142
142
  }
143
+ /**
144
+ * 数学公式配置选项
145
+ */
146
+ interface MathOptions {
147
+ /**
148
+ * 启用 TeX 风格的公式分隔符 (default: `false`)
149
+ *
150
+ * 开启后同时支持:
151
+ * - 行内公式:\(...\)
152
+ * - 块级公式:\[...\]
153
+ *
154
+ * 这是 LaTeX/TeX 原生语法,MathJax 和 KaTeX 都支持。
155
+ * 开启此选项可以兼容从 LaTeX 文档或学术 AI 工具输出的内容。
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * // 启用 TeX 风格分隔符
160
+ * const parser = createIncremarkParser({
161
+ * math: { tex: true }
162
+ * })
163
+ * ```
164
+ */
165
+ tex?: boolean;
166
+ }
143
167
  /**
144
168
  * 解析器配置
145
169
  */
@@ -147,11 +171,12 @@ interface ParserOptions {
147
171
  /** 启用 GFM 扩展(表格、任务列表等) */
148
172
  gfm?: boolean;
149
173
  /**
150
- * 启用数学公式支持($..$ 行内公式和 $$...$$ 块级公式)
174
+ * 启用数学公式支持
151
175
  * - false/undefined: 禁用(默认)
152
- * - true: 启用数学公式解析
176
+ * - true: 启用数学公式解析(仅支持 $...$ 和 $$...$$ 语法)
177
+ * - MathOptions: 启用并配置额外选项(如 LaTeX 风格的 \(...\) 和 \[...\] 语法)
153
178
  */
154
- math?: boolean;
179
+ math?: boolean | MathOptions;
155
180
  /**
156
181
  * 启用 ::: 容器语法支持(用于边界检测)
157
182
  * - false: 禁用(默认)
@@ -222,4 +247,4 @@ interface ContainerMatch {
222
247
  isEnd: boolean;
223
248
  }
224
249
 
225
- export type { AstNode as A, BlockStatus as B, ContainerConfig as C, DefinitionMap as D, FootnoteDefinitionMap as F, HtmlTreeExtensionOptions as H, IncrementalUpdate as I, ParsedBlock as P, ParserState as a, ParserOptions as b, BlockContext as c, ContainerMatch as d };
250
+ export type { AstNode as A, BlockStatus as B, ContainerConfig as C, DefinitionMap as D, FootnoteDefinitionMap as F, HtmlTreeExtensionOptions as H, IncrementalUpdate as I, MathOptions as M, ParsedBlock as P, ParserState as a, ParserOptions as b, BlockContext as c, ContainerMatch as d };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { I as IncrementalUpdate, P as ParsedBlock, D as DefinitionMap, F as FootnoteDefinitionMap, a as ParserState, B as BlockStatus } from './index-mZ7yCqNH.js';
2
- export { A as AstNode, b as ParserOptions } from './index-mZ7yCqNH.js';
3
- import { E as EngineParserOptions, I as IAstBuilder } from './types-C_EW5vfp.js';
4
- export { a as EngineType, b as IncremarkPlugin, M as MarkedEngineExtension, c as MicromarkEngineExtension } from './types-C_EW5vfp.js';
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
+ export { A as AstNode, M as MathOptions, b as ParserOptions } from './index-CWuosVAK.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-BsjxZko_.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,13 +40,12 @@ 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
  /** 边界检测器 */
46
46
  private readonly boundaryDetector;
47
47
  /** AST 构建器 */
48
- private readonly astBuilder;
48
+ private astBuilder;
49
49
  /** Definition 管理器 */
50
50
  private readonly definitionManager;
51
51
  /** Footnote 管理器 */
@@ -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
  */
@@ -129,6 +139,27 @@ declare class IncremarkParser {
129
139
  * @returns 解析结果
130
140
  */
131
141
  render(content: string): IncrementalUpdate;
142
+ /**
143
+ * 更新解析器配置(动态更新,不需要重建 parser 实例)
144
+ *
145
+ * 注意:更新配置后会自动调用 reset() 重置状态
146
+ *
147
+ * @param options 部分配置选项
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * // 动态启用 TeX 数学公式语法
152
+ * parser.updateOptions({ math: { tex: true } })
153
+ *
154
+ * // 禁用 GFM
155
+ * parser.updateOptions({ gfm: false })
156
+ *
157
+ * // 切换引擎
158
+ * import { MicromarkAstBuilder } from '@incremark/core/engines/micromark'
159
+ * parser.updateOptions({ astBuilder: MicromarkAstBuilder })
160
+ * ```
161
+ */
162
+ updateOptions(options: Partial<IncremarkParserOptions>): void;
132
163
  }
133
164
  /**
134
165
  * 创建 Incremark 解析器实例
@@ -167,13 +198,22 @@ interface SourceBlock<T = unknown> {
167
198
  }
168
199
  /**
169
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'
170
210
  */
171
211
  interface DisplayBlock<T = unknown> extends SourceBlock<T> {
172
212
  /** 用于显示的 AST 节点(可能是截断的) */
173
213
  displayNode: RootContent;
174
214
  /** 显示进度 0-1 */
175
215
  progress: number;
176
- /** 是否已完成显示 */
216
+ /** 是否已完成显示(打字机动画是否完成) */
177
217
  isDisplayComplete: boolean;
178
218
  }
179
219
  /**
@@ -305,7 +345,11 @@ declare class BlockTransformer<T = unknown> {
305
345
  constructor(options?: TransformerOptions);
306
346
  /**
307
347
  * 推入新的 blocks
308
- * 会自动过滤已存在的 blocks
348
+ *
349
+ * 逻辑:
350
+ * 1. 移除不在传入列表中的旧 blocks(处理容器增量解析等场景)
351
+ * 2. 如果 block ID 不存在,添加到 pending
352
+ * 3. 如果 block ID 已存在且内容变化,更新对应位置的 block
309
353
  */
310
354
  push(blocks: SourceBlock<T>[]): void;
311
355
  /**
@@ -331,6 +375,10 @@ declare class BlockTransformer<T = unknown> {
331
375
  /**
332
376
  * 获取用于渲染的 display blocks
333
377
  * 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
378
+ *
379
+ * 注意:DisplayBlock 的 status 表示的是**打字机动画状态**,而不是解析器的状态:
380
+ * - 'completed': 打字机动画已完成,内容已完全显示
381
+ * - 'pending': 打字机动画还在进行中,内容还在逐字显示
334
382
  */
335
383
  getDisplayBlocks(): DisplayBlock<T>[];
336
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) {
@@ -1351,24 +1375,48 @@ function createOptimisticReferenceExtension() {
1351
1375
  }
1352
1376
 
1353
1377
  // src/extensions/marked-extensions/mathExtension.ts
1354
- function createBlockMathExtension() {
1378
+ function resolveOptions(options) {
1379
+ return {
1380
+ tex: options?.tex ?? false
1381
+ };
1382
+ }
1383
+ function createBlockMathExtension(options) {
1384
+ const resolved = resolveOptions(options);
1355
1385
  return {
1356
1386
  name: "blockMath",
1357
1387
  level: "block",
1358
1388
  start(src) {
1359
- const match = src.match(/^ {0,3}\$\$/m);
1360
- return match?.index;
1389
+ const dollarMatch = src.match(/^ {0,3}\$\$/m);
1390
+ let bracketMatch = null;
1391
+ if (resolved.tex) {
1392
+ bracketMatch = src.match(/^ {0,3}\\\[/m);
1393
+ }
1394
+ if (dollarMatch && bracketMatch) {
1395
+ return Math.min(dollarMatch.index, bracketMatch.index);
1396
+ }
1397
+ return dollarMatch?.index ?? bracketMatch?.index;
1361
1398
  },
1362
1399
  tokenizer(src) {
1363
- const rule = /^ {0,3}\$\$([\s\S]*?)\$\$ *(?:\n+|$)/;
1364
- const match = rule.exec(src);
1365
- if (match) {
1400
+ const dollarRule = /^ {0,3}\$\$([\s\S]*?)\$\$ *(?:\n+|$)/;
1401
+ const dollarMatch = dollarRule.exec(src);
1402
+ if (dollarMatch) {
1366
1403
  return {
1367
1404
  type: "blockMath",
1368
- raw: match[0],
1369
- text: match[1].trim()
1405
+ raw: dollarMatch[0],
1406
+ text: dollarMatch[1].trim()
1370
1407
  };
1371
1408
  }
1409
+ if (resolved.tex) {
1410
+ const bracketRule = /^ {0,3}\\\[([\s\S]*?)\\\] *(?:\n+|$)/;
1411
+ const bracketMatch = bracketRule.exec(src);
1412
+ if (bracketMatch) {
1413
+ return {
1414
+ type: "blockMath",
1415
+ raw: bracketMatch[0],
1416
+ text: bracketMatch[1].trim()
1417
+ };
1418
+ }
1419
+ }
1372
1420
  return void 0;
1373
1421
  },
1374
1422
  renderer() {
@@ -1376,26 +1424,46 @@ function createBlockMathExtension() {
1376
1424
  }
1377
1425
  };
1378
1426
  }
1379
- function createInlineMathExtension() {
1427
+ function createInlineMathExtension(options) {
1428
+ const resolved = resolveOptions(options);
1380
1429
  return {
1381
1430
  name: "inlineMath",
1382
1431
  level: "inline",
1383
1432
  start(src) {
1384
- const index = src.indexOf("$");
1385
- if (index === -1) return void 0;
1386
- if (src[index + 1] === "$") return void 0;
1387
- return index;
1433
+ const dollarIndex = src.indexOf("$");
1434
+ const validDollarIndex = dollarIndex !== -1 && src[dollarIndex + 1] !== "$" ? dollarIndex : -1;
1435
+ let parenIndex = -1;
1436
+ if (resolved.tex) {
1437
+ parenIndex = src.indexOf("\\(");
1438
+ }
1439
+ if (validDollarIndex !== -1 && parenIndex !== -1) {
1440
+ return Math.min(validDollarIndex, parenIndex);
1441
+ }
1442
+ if (validDollarIndex !== -1) return validDollarIndex;
1443
+ if (parenIndex !== -1) return parenIndex;
1444
+ return void 0;
1388
1445
  },
1389
1446
  tokenizer(src) {
1390
- const rule = /^\$(?!\$)((?:\\.|[^\\\n$])+?)\$(?!\d)/;
1391
- const match = rule.exec(src);
1392
- if (match) {
1447
+ const dollarRule = /^\$(?!\$)((?:\\.|[^\\\n$])+?)\$(?!\d)/;
1448
+ const dollarMatch = dollarRule.exec(src);
1449
+ if (dollarMatch) {
1393
1450
  return {
1394
1451
  type: "inlineMath",
1395
- raw: match[0],
1396
- text: match[1].trim()
1452
+ raw: dollarMatch[0],
1453
+ text: dollarMatch[1].trim()
1397
1454
  };
1398
1455
  }
1456
+ if (resolved.tex) {
1457
+ const parenRule = /^\\\(([\s\S]*?)\\\)/;
1458
+ const parenMatch = parenRule.exec(src);
1459
+ if (parenMatch) {
1460
+ return {
1461
+ type: "inlineMath",
1462
+ raw: parenMatch[0],
1463
+ text: parenMatch[1].trim()
1464
+ };
1465
+ }
1466
+ }
1399
1467
  return void 0;
1400
1468
  },
1401
1469
  renderer() {
@@ -2053,8 +2121,9 @@ var MarkedAstBuilder = class {
2053
2121
  const inlineExts = [optimisticRefExt.tokenizer, ...userInlineExts];
2054
2122
  const inlineStartExts = [optimisticRefExt.start, ...userInlineStartExts];
2055
2123
  if (this.options.math) {
2056
- const blockMathExt = createBlockMathExtension();
2057
- const inlineMathExt = createInlineMathExtension();
2124
+ const mathOptions = typeof this.options.math === "object" ? this.options.math : {};
2125
+ const blockMathExt = createBlockMathExtension(mathOptions);
2126
+ const inlineMathExt = createInlineMathExtension(mathOptions);
2058
2127
  blockExts.unshift(blockMathExt.tokenizer);
2059
2128
  blockStartExts.unshift(blockMathExt.start);
2060
2129
  inlineExts.unshift(inlineMathExt.tokenizer);
@@ -2270,7 +2339,7 @@ var MarkedAstBuilder = class {
2270
2339
  const absoluteStart = startOffset + relativeStart;
2271
2340
  const absoluteEnd = startOffset + relativeEnd;
2272
2341
  blocks.push({
2273
- id: generateBlockId(),
2342
+ id: generateBlockId(absoluteStart),
2274
2343
  status,
2275
2344
  node,
2276
2345
  startOffset: absoluteStart,
@@ -2280,6 +2349,28 @@ var MarkedAstBuilder = class {
2280
2349
  }
2281
2350
  return blocks;
2282
2351
  }
2352
+ /**
2353
+ * 更新配置选项
2354
+ * @param options 部分配置选项
2355
+ */
2356
+ updateOptions(options) {
2357
+ Object.assign(this.options, options);
2358
+ if ("containers" in options) {
2359
+ this.containerConfig = typeof options.containers === "object" ? options.containers : options.containers === true ? {} : void 0;
2360
+ }
2361
+ if ("htmlTree" in options) {
2362
+ this.htmlTreeOptions = typeof options.htmlTree === "object" ? options.htmlTree : options.htmlTree === true ? {} : void 0;
2363
+ }
2364
+ if (options.plugins || options.markedExtensions) {
2365
+ this.userExtensions.length = 0;
2366
+ if (options.plugins) {
2367
+ this.userExtensions.push(...extractMarkedExtensions(options.plugins));
2368
+ }
2369
+ if (options.markedExtensions) {
2370
+ this.userExtensions.push(...options.markedExtensions);
2371
+ }
2372
+ }
2373
+ }
2283
2374
  };
2284
2375
 
2285
2376
  // src/parser/IncremarkParser.ts
@@ -2289,7 +2380,6 @@ var IncremarkParser = class {
2289
2380
  lineOffsets = [0];
2290
2381
  completedBlocks = [];
2291
2382
  pendingStartLine = 0;
2292
- blockIdCounter = 0;
2293
2383
  context;
2294
2384
  options;
2295
2385
  /** 边界检测器 */
@@ -2314,8 +2404,20 @@ var IncremarkParser = class {
2314
2404
  this.definitionManager = new DefinitionManager();
2315
2405
  this.footnoteManager = new FootnoteManager();
2316
2406
  }
2317
- generateBlockId() {
2318
- 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);
2319
2421
  }
2320
2422
  /**
2321
2423
  * 更新已完成的 blocks 中的 definitions 和 footnote definitions
@@ -2387,9 +2489,28 @@ var IncremarkParser = class {
2387
2489
  const stableText = this.lines.slice(this.pendingStartLine, stableBoundary + 1).join("\n");
2388
2490
  const stableOffset = this.getLineOffset(this.pendingStartLine);
2389
2491
  const ast = this.astBuilder.parse(stableText);
2390
- 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
+ }
2391
2509
  this.completedBlocks.push(...newBlocks);
2392
2510
  update.completed = newBlocks;
2511
+ if (blocksToRemove.length > 0) {
2512
+ update.updated = blocksToRemove;
2513
+ }
2393
2514
  this.updateDefinitionsFromCompletedBlocks(newBlocks);
2394
2515
  this.footnoteManager.collectReferencesFromCompletedBlocks(newBlocks);
2395
2516
  this.boundaryDetector.clearContextCache(this.pendingStartLine);
@@ -2401,7 +2522,13 @@ var IncremarkParser = class {
2401
2522
  if (pendingText.trim()) {
2402
2523
  const pendingOffset = this.getLineOffset(this.pendingStartLine);
2403
2524
  const ast = this.astBuilder.parse(pendingText);
2404
- 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
+ );
2405
2532
  }
2406
2533
  }
2407
2534
  this.lastPendingBlocks = update.pending;
@@ -2461,7 +2588,7 @@ var IncremarkParser = class {
2461
2588
  remainingOffset,
2462
2589
  remainingText,
2463
2590
  "completed",
2464
- () => this.generateBlockId()
2591
+ (offset) => this.generateBlockId(offset)
2465
2592
  );
2466
2593
  this.completedBlocks.push(...finalBlocks);
2467
2594
  update.completed = finalBlocks;
@@ -2552,7 +2679,6 @@ var IncremarkParser = class {
2552
2679
  this.lineOffsets = [0];
2553
2680
  this.completedBlocks = [];
2554
2681
  this.pendingStartLine = 0;
2555
- this.blockIdCounter = 0;
2556
2682
  this.context = createInitialContext();
2557
2683
  this.lastPendingBlocks = [];
2558
2684
  this.definitionManager.clear();
@@ -2569,6 +2695,40 @@ var IncremarkParser = class {
2569
2695
  this.append(content);
2570
2696
  return this.finalize();
2571
2697
  }
2698
+ /**
2699
+ * 更新解析器配置(动态更新,不需要重建 parser 实例)
2700
+ *
2701
+ * 注意:更新配置后会自动调用 reset() 重置状态
2702
+ *
2703
+ * @param options 部分配置选项
2704
+ *
2705
+ * @example
2706
+ * ```ts
2707
+ * // 动态启用 TeX 数学公式语法
2708
+ * parser.updateOptions({ math: { tex: true } })
2709
+ *
2710
+ * // 禁用 GFM
2711
+ * parser.updateOptions({ gfm: false })
2712
+ *
2713
+ * // 切换引擎
2714
+ * import { MicromarkAstBuilder } from '@incremark/core/engines/micromark'
2715
+ * parser.updateOptions({ astBuilder: MicromarkAstBuilder })
2716
+ * ```
2717
+ */
2718
+ updateOptions(options) {
2719
+ this.reset();
2720
+ Object.assign(this.options, options);
2721
+ if (options.astBuilder) {
2722
+ const BuilderClass = options.astBuilder;
2723
+ if (!(this.astBuilder instanceof BuilderClass)) {
2724
+ this.astBuilder = new BuilderClass(this.options);
2725
+ } else {
2726
+ this.astBuilder.updateOptions(options);
2727
+ }
2728
+ } else {
2729
+ this.astBuilder.updateOptions(options);
2730
+ }
2731
+ }
2572
2732
  };
2573
2733
  function createIncremarkParser(options) {
2574
2734
  return new IncremarkParser(options);
@@ -2784,21 +2944,77 @@ var BlockTransformer = class {
2784
2944
  }
2785
2945
  /**
2786
2946
  * 推入新的 blocks
2787
- * 会自动过滤已存在的 blocks
2947
+ *
2948
+ * 逻辑:
2949
+ * 1. 移除不在传入列表中的旧 blocks(处理容器增量解析等场景)
2950
+ * 2. 如果 block ID 不存在,添加到 pending
2951
+ * 3. 如果 block ID 已存在且内容变化,更新对应位置的 block
2788
2952
  */
2789
2953
  push(blocks) {
2954
+ const inputIds = new Set(blocks.map((b) => b.id));
2790
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
+ }
2791
2977
  const newBlocks = blocks.filter((b) => !existingIds.has(b.id));
2792
2978
  if (newBlocks.length > 0) {
2793
2979
  this.state.pendingBlocks.push(...newBlocks);
2794
2980
  this.startIfNeeded();
2795
2981
  }
2796
- if (this.state.currentBlock) {
2797
- const updated = blocks.find((b) => b.id === this.state.currentBlock.id);
2798
- if (updated && updated.node !== this.state.currentBlock.node) {
2799
- this.handleContentChange(this.state.currentBlock.node, updated.node, true);
2800
- 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;
2801
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;
3014
+ }
3015
+ }
3016
+ if (hasRemovals) {
3017
+ this.emit();
2802
3018
  }
2803
3019
  }
2804
3020
  /**
@@ -2808,6 +3024,18 @@ var BlockTransformer = class {
2808
3024
  if (this.state.currentBlock?.id === block.id) {
2809
3025
  this.handleContentChange(this.state.currentBlock.node, block.node, false);
2810
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;
2811
3039
  }
2812
3040
  }
2813
3041
  /**
@@ -2865,12 +3093,18 @@ var BlockTransformer = class {
2865
3093
  /**
2866
3094
  * 获取用于渲染的 display blocks
2867
3095
  * 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
3096
+ *
3097
+ * 注意:DisplayBlock 的 status 表示的是**打字机动画状态**,而不是解析器的状态:
3098
+ * - 'completed': 打字机动画已完成,内容已完全显示
3099
+ * - 'pending': 打字机动画还在进行中,内容还在逐字显示
2868
3100
  */
2869
3101
  getDisplayBlocks() {
2870
3102
  const result = [];
2871
3103
  for (const block of this.state.completedBlocks) {
2872
3104
  result.push({
2873
3105
  ...block,
3106
+ // 打字机动画已完成,状态为 completed
3107
+ status: "completed",
2874
3108
  displayNode: block.node,
2875
3109
  progress: 1,
2876
3110
  isDisplayComplete: true
@@ -2883,6 +3117,8 @@ var BlockTransformer = class {
2883
3117
  }
2884
3118
  result.push({
2885
3119
  ...this.state.currentBlock,
3120
+ // 打字机动画进行中,状态为 pending
3121
+ status: "pending",
2886
3122
  displayNode: this.cachedDisplayNode || { type: "paragraph", children: [] },
2887
3123
  progress: total > 0 ? this.state.currentProgress / total : 1,
2888
3124
  isDisplayComplete: false
@@ -3309,6 +3545,6 @@ function createPlugin(name, matcher, options = {}) {
3309
3545
  };
3310
3546
  }
3311
3547
 
3312
- 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 };
3313
3549
  //# sourceMappingURL=index.js.map
3314
3550
  //# sourceMappingURL=index.js.map