@incremark/core 0.1.2 → 0.2.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,9 +1,863 @@
1
1
  import { fromMarkdown } from 'mdast-util-from-markdown';
2
2
  import { gfmFromMarkdown } from 'mdast-util-gfm';
3
3
  import { gfm } from 'micromark-extension-gfm';
4
+ import { gfmFootnoteFromMarkdown } from 'mdast-util-gfm-footnote';
5
+ import { codes, constants, types } from 'micromark-util-symbol';
6
+ import { markdownLineEndingOrSpace } from 'micromark-util-character';
7
+ import { factoryDestination } from 'micromark-factory-destination';
8
+ import { factoryTitle } from 'micromark-factory-title';
9
+ import { factoryLabel } from 'micromark-factory-label';
10
+ import { factoryWhitespace } from 'micromark-factory-whitespace';
11
+ import { gfmFootnote } from 'micromark-extension-gfm-footnote';
12
+ import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
4
13
 
5
14
  // src/parser/IncremarkParser.ts
6
15
 
16
+ // src/extensions/html-extension/index.ts
17
+ var DEFAULT_TAG_BLACKLIST = [
18
+ "script",
19
+ "style",
20
+ "iframe",
21
+ "object",
22
+ "embed",
23
+ "form",
24
+ "input",
25
+ "button",
26
+ "textarea",
27
+ "select",
28
+ "meta",
29
+ "link",
30
+ "base",
31
+ "frame",
32
+ "frameset",
33
+ "applet",
34
+ "noscript",
35
+ "template"
36
+ ];
37
+ var DEFAULT_ATTR_BLACKLIST = [
38
+ // 事件属性通过正则匹配
39
+ "formaction",
40
+ "xlink:href",
41
+ "xmlns",
42
+ "srcdoc"
43
+ ];
44
+ var DEFAULT_PROTOCOL_BLACKLIST = [
45
+ "javascript:",
46
+ "vbscript:",
47
+ "data:"
48
+ // 注意:data:image/ 会被特殊处理允许
49
+ ];
50
+ var URL_ATTRS = ["href", "src", "action", "formaction", "poster", "background"];
51
+ var VOID_ELEMENTS = ["br", "hr", "img", "input", "meta", "link", "area", "base", "col", "embed", "source", "track", "wbr"];
52
+ function detectHtmlContentType(html) {
53
+ const trimmed = html.trim();
54
+ if (!trimmed) return "unknown";
55
+ if (!trimmed.startsWith("<")) return "unknown";
56
+ const closingMatch = trimmed.match(/^<\/([a-zA-Z][a-zA-Z0-9-]*)\s*>$/);
57
+ if (closingMatch) {
58
+ return "closing";
59
+ }
60
+ const singleTagMatch = trimmed.match(/^<([a-zA-Z][a-zA-Z0-9-]*)(\s[^]*?)?(\/?)>$/);
61
+ if (singleTagMatch) {
62
+ const [fullMatch, tagName, attrsString, selfClosingSlash] = singleTagMatch;
63
+ if (attrsString) {
64
+ let inQuote = "";
65
+ let hasUnquotedBracket = false;
66
+ for (let i = 0; i < attrsString.length; i++) {
67
+ const char = attrsString[i];
68
+ if (inQuote) {
69
+ if (char === inQuote) inQuote = "";
70
+ } else {
71
+ if (char === '"' || char === "'") inQuote = char;
72
+ else if (char === "<") {
73
+ hasUnquotedBracket = true;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ if (hasUnquotedBracket) {
79
+ return "fragment";
80
+ }
81
+ }
82
+ const isSelfClosing = selfClosingSlash === "/" || VOID_ELEMENTS.includes(tagName.toLowerCase());
83
+ return isSelfClosing ? "self-closing" : "opening";
84
+ }
85
+ let bracketCount = 0;
86
+ for (const char of trimmed) {
87
+ if (char === "<") bracketCount++;
88
+ }
89
+ if (bracketCount > 1) {
90
+ return "fragment";
91
+ }
92
+ return "unknown";
93
+ }
94
+ function parseHtmlTag(html) {
95
+ const trimmed = html.trim();
96
+ const contentType = detectHtmlContentType(trimmed);
97
+ if (contentType !== "opening" && contentType !== "closing" && contentType !== "self-closing") {
98
+ return null;
99
+ }
100
+ if (contentType === "closing") {
101
+ const match2 = trimmed.match(/^<\/([a-zA-Z][a-zA-Z0-9-]*)\s*>$/);
102
+ if (!match2) return null;
103
+ return {
104
+ tagName: match2[1].toLowerCase(),
105
+ attrs: {},
106
+ isClosing: true,
107
+ isSelfClosing: false,
108
+ rawHtml: html
109
+ };
110
+ }
111
+ const match = trimmed.match(/^<([a-zA-Z][a-zA-Z0-9-]*)(\s[^]*?)?(\/?)>$/);
112
+ if (!match) return null;
113
+ const [, tagName, attrsString, selfClosingSlash] = match;
114
+ const isSelfClosing = selfClosingSlash === "/" || VOID_ELEMENTS.includes(tagName.toLowerCase());
115
+ const attrs = {};
116
+ if (attrsString) {
117
+ const attrRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
118
+ let attrMatch;
119
+ while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
120
+ const [, name, doubleQuoted, singleQuoted, unquoted] = attrMatch;
121
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
122
+ attrs[name.toLowerCase()] = decodeHtmlEntities(value);
123
+ }
124
+ }
125
+ return {
126
+ tagName: tagName.toLowerCase(),
127
+ attrs,
128
+ isClosing: false,
129
+ isSelfClosing,
130
+ rawHtml: html
131
+ };
132
+ }
133
+ function decodeHtmlEntities(text) {
134
+ const entities = {
135
+ "&amp;": "&",
136
+ "&lt;": "<",
137
+ "&gt;": ">",
138
+ "&quot;": '"',
139
+ "&#39;": "'",
140
+ "&apos;": "'",
141
+ "&nbsp;": " "
142
+ };
143
+ return text.replace(/&(?:#(\d+)|#x([a-fA-F0-9]+)|([a-zA-Z]+));/g, (match, dec, hex, name) => {
144
+ if (dec) return String.fromCharCode(parseInt(dec, 10));
145
+ if (hex) return String.fromCharCode(parseInt(hex, 16));
146
+ return entities[`&${name};`] || match;
147
+ });
148
+ }
149
+ function parseTagDirect(tag) {
150
+ const trimmed = tag.trim();
151
+ const closingMatch = trimmed.match(/^<\/([a-zA-Z][a-zA-Z0-9-]*)\s*>$/);
152
+ if (closingMatch) {
153
+ return {
154
+ tagName: closingMatch[1].toLowerCase(),
155
+ attrs: {},
156
+ isClosing: true,
157
+ isSelfClosing: false,
158
+ rawHtml: tag
159
+ };
160
+ }
161
+ const openMatch = trimmed.match(/^<([a-zA-Z][a-zA-Z0-9-]*)([\s\S]*?)(\/?)>$/);
162
+ if (!openMatch) return null;
163
+ const [, tagName, attrsString, selfClosingSlash] = openMatch;
164
+ const isSelfClosing = selfClosingSlash === "/" || VOID_ELEMENTS.includes(tagName.toLowerCase());
165
+ const attrs = {};
166
+ if (attrsString) {
167
+ const attrRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
168
+ let attrMatch;
169
+ while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
170
+ const [, name, doubleQuoted, singleQuoted, unquoted] = attrMatch;
171
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
172
+ attrs[name.toLowerCase()] = decodeHtmlEntities(value);
173
+ }
174
+ }
175
+ return {
176
+ tagName: tagName.toLowerCase(),
177
+ attrs,
178
+ isClosing: false,
179
+ isSelfClosing,
180
+ rawHtml: tag
181
+ };
182
+ }
183
+ function parseHtmlFragment(html, options = {}) {
184
+ const result = [];
185
+ const stack = [];
186
+ const tokenRegex = /(<\/?[a-zA-Z][^>]*>)|([^<]+)/g;
187
+ let match;
188
+ while ((match = tokenRegex.exec(html)) !== null) {
189
+ const [, tag, text] = match;
190
+ if (tag) {
191
+ const parsed = parseTagDirect(tag);
192
+ if (!parsed) continue;
193
+ if (isTagBlacklisted(parsed.tagName, options)) {
194
+ continue;
195
+ }
196
+ if (parsed.isClosing) {
197
+ let found = false;
198
+ for (let i = stack.length - 1; i >= 0; i--) {
199
+ if (stack[i].tagName === parsed.tagName) {
200
+ const node = stack.pop();
201
+ if (stack.length > 0) {
202
+ stack[stack.length - 1].children.push(node);
203
+ } else {
204
+ result.push(node);
205
+ }
206
+ found = true;
207
+ break;
208
+ }
209
+ }
210
+ if (!found) continue;
211
+ } else {
212
+ const sanitizedAttrs = sanitizeAttrs(parsed.attrs, options);
213
+ const node = {
214
+ type: "htmlElement",
215
+ tagName: parsed.tagName,
216
+ attrs: sanitizedAttrs,
217
+ children: [],
218
+ data: options.preserveRawHtml !== false ? {
219
+ rawHtml: tag,
220
+ parsed: true
221
+ } : void 0
222
+ };
223
+ if (parsed.isSelfClosing) {
224
+ if (stack.length > 0) {
225
+ stack[stack.length - 1].children.push(node);
226
+ } else {
227
+ result.push(node);
228
+ }
229
+ } else {
230
+ stack.push(node);
231
+ }
232
+ }
233
+ } else if (text && text.trim()) {
234
+ const textNode = {
235
+ type: "text",
236
+ value: text
237
+ };
238
+ if (stack.length > 0) {
239
+ stack[stack.length - 1].children.push(textNode);
240
+ }
241
+ }
242
+ }
243
+ while (stack.length > 0) {
244
+ const node = stack.pop();
245
+ if (stack.length > 0) {
246
+ stack[stack.length - 1].children.push(node);
247
+ } else {
248
+ result.push(node);
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+ function isTagBlacklisted(tagName, options) {
254
+ const blacklist = options.tagBlacklist ?? DEFAULT_TAG_BLACKLIST;
255
+ return blacklist.includes(tagName.toLowerCase());
256
+ }
257
+ function isAttrBlacklisted(attrName, options) {
258
+ const name = attrName.toLowerCase();
259
+ const blacklist = options.attrBlacklist ?? DEFAULT_ATTR_BLACKLIST;
260
+ if (name.startsWith("on")) return true;
261
+ return blacklist.includes(name);
262
+ }
263
+ function isProtocolDangerous(url, options) {
264
+ const protocolBlacklist = options.protocolBlacklist ?? DEFAULT_PROTOCOL_BLACKLIST;
265
+ const normalizedUrl = url.trim().toLowerCase();
266
+ for (const protocol of protocolBlacklist) {
267
+ if (normalizedUrl.startsWith(protocol)) {
268
+ if (protocol === "data:" && normalizedUrl.startsWith("data:image/")) {
269
+ return false;
270
+ }
271
+ return true;
272
+ }
273
+ }
274
+ return false;
275
+ }
276
+ function sanitizeAttrs(attrs, options) {
277
+ const result = {};
278
+ for (const [name, value] of Object.entries(attrs)) {
279
+ if (isAttrBlacklisted(name, options)) continue;
280
+ if (URL_ATTRS.includes(name.toLowerCase())) {
281
+ if (isProtocolDangerous(value, options)) continue;
282
+ }
283
+ result[name] = value;
284
+ }
285
+ return result;
286
+ }
287
+ function isHtmlNode(node) {
288
+ return node.type === "html";
289
+ }
290
+ function hasChildren(node) {
291
+ return "children" in node && Array.isArray(node.children);
292
+ }
293
+ function processHtmlNodesInArray(nodes, options) {
294
+ const result = [];
295
+ let i = 0;
296
+ while (i < nodes.length) {
297
+ const node = nodes[i];
298
+ if (isHtmlNode(node)) {
299
+ const contentType = detectHtmlContentType(node.value);
300
+ if (contentType === "fragment") {
301
+ const fragmentNodes = parseHtmlFragment(node.value, options);
302
+ if (fragmentNodes.length > 0) {
303
+ result.push(...fragmentNodes);
304
+ } else {
305
+ result.push(node);
306
+ }
307
+ i++;
308
+ } else if (contentType === "self-closing") {
309
+ const parsed = parseHtmlTag(node.value);
310
+ if (parsed && !isTagBlacklisted(parsed.tagName, options)) {
311
+ const elementNode = {
312
+ type: "htmlElement",
313
+ tagName: parsed.tagName,
314
+ attrs: sanitizeAttrs(parsed.attrs, options),
315
+ children: [],
316
+ data: options.preserveRawHtml !== false ? {
317
+ rawHtml: node.value,
318
+ parsed: true,
319
+ originalType: "html"
320
+ } : void 0
321
+ };
322
+ result.push(elementNode);
323
+ }
324
+ i++;
325
+ } else if (contentType === "closing") {
326
+ i++;
327
+ } else if (contentType === "opening") {
328
+ const parsed = parseHtmlTag(node.value);
329
+ if (!parsed || isTagBlacklisted(parsed.tagName, options)) {
330
+ i++;
331
+ continue;
332
+ }
333
+ const tagName = parsed.tagName;
334
+ const contentNodes = [];
335
+ let depth = 1;
336
+ let j = i + 1;
337
+ let foundClosing = false;
338
+ while (j < nodes.length && depth > 0) {
339
+ const nextNode = nodes[j];
340
+ if (isHtmlNode(nextNode)) {
341
+ const nextType = detectHtmlContentType(nextNode.value);
342
+ if (nextType === "closing") {
343
+ const nextParsed = parseHtmlTag(nextNode.value);
344
+ if (nextParsed && nextParsed.tagName === tagName) {
345
+ depth--;
346
+ if (depth === 0) {
347
+ foundClosing = true;
348
+ break;
349
+ }
350
+ }
351
+ } else if (nextType === "opening") {
352
+ const nextParsed = parseHtmlTag(nextNode.value);
353
+ if (nextParsed && nextParsed.tagName === tagName) {
354
+ depth++;
355
+ }
356
+ }
357
+ }
358
+ contentNodes.push(nextNode);
359
+ j++;
360
+ }
361
+ const elementNode = {
362
+ type: "htmlElement",
363
+ tagName: parsed.tagName,
364
+ attrs: sanitizeAttrs(parsed.attrs, options),
365
+ children: processHtmlNodesInArray(contentNodes, options),
366
+ data: options.preserveRawHtml !== false ? {
367
+ rawHtml: node.value,
368
+ parsed: true,
369
+ originalType: "html"
370
+ } : void 0
371
+ };
372
+ result.push(elementNode);
373
+ i = foundClosing ? j + 1 : j;
374
+ } else {
375
+ result.push(node);
376
+ i++;
377
+ }
378
+ } else {
379
+ if (hasChildren(node)) {
380
+ const processed = processHtmlNodesInArray(
381
+ node.children,
382
+ options
383
+ );
384
+ result.push({
385
+ ...node,
386
+ children: processed
387
+ });
388
+ } else {
389
+ result.push(node);
390
+ }
391
+ i++;
392
+ }
393
+ }
394
+ return result;
395
+ }
396
+ function transformHtmlNodes(ast, options = {}) {
397
+ return {
398
+ ...ast,
399
+ children: processHtmlNodesInArray(ast.children, options)
400
+ };
401
+ }
402
+ function createHtmlTreeTransformer(options = {}) {
403
+ return function transformer(tree) {
404
+ return transformHtmlNodes(tree, options);
405
+ };
406
+ }
407
+ var htmlTreeExtension = {
408
+ enter: {},
409
+ exit: {}
410
+ };
411
+ function isHtmlElementNode(node) {
412
+ return node.type === "htmlElement";
413
+ }
414
+ function walkHtmlElements(node, callback, parent = null) {
415
+ if (isHtmlElementNode(node)) {
416
+ callback(node, parent);
417
+ }
418
+ if (hasChildren(node) || node.type === "root") {
419
+ const children = node.children;
420
+ for (const child of children) {
421
+ walkHtmlElements(child, callback, node);
422
+ }
423
+ }
424
+ }
425
+ function findHtmlElementsByTag(root, tagName) {
426
+ const result = [];
427
+ walkHtmlElements(root, (node) => {
428
+ if (node.tagName === tagName.toLowerCase()) {
429
+ result.push(node);
430
+ }
431
+ });
432
+ return result;
433
+ }
434
+ function htmlElementToString(node) {
435
+ const { tagName, attrs, children } = node;
436
+ const attrsStr = Object.entries(attrs).map(([name, value]) => {
437
+ if (value === "") return name;
438
+ return `${name}="${escapeHtml(value)}"`;
439
+ }).join(" ");
440
+ const openTag = attrsStr ? `<${tagName} ${attrsStr}>` : `<${tagName}>`;
441
+ if (children.length === 0 && isSelfClosingTag(tagName)) {
442
+ return attrsStr ? `<${tagName} ${attrsStr} />` : `<${tagName} />`;
443
+ }
444
+ const childrenStr = children.map((child) => {
445
+ if (child.type === "text") {
446
+ return child.value;
447
+ }
448
+ if (isHtmlElementNode(child)) {
449
+ return htmlElementToString(child);
450
+ }
451
+ return "";
452
+ }).join("");
453
+ return `${openTag}${childrenStr}</${tagName}>`;
454
+ }
455
+ function isSelfClosingTag(tagName) {
456
+ return ["br", "hr", "img", "input", "meta", "link", "area", "base", "col", "embed", "source", "track", "wbr"].includes(tagName.toLowerCase());
457
+ }
458
+ function escapeHtml(text) {
459
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
460
+ }
461
+ function micromarkReferenceExtension() {
462
+ return {
463
+ // 在 text 中使用 codes.rightSquareBracket 键覆盖 labelEnd
464
+ text: {
465
+ [codes.rightSquareBracket]: {
466
+ name: "labelEnd",
467
+ resolveAll: resolveAllLabelEnd,
468
+ resolveTo: resolveToLabelEnd,
469
+ tokenize: tokenizeLabelEnd,
470
+ // 添加 add: 'before' 确保先被尝试
471
+ add: "before"
472
+ }
473
+ }
474
+ };
475
+ }
476
+ function resolveAllLabelEnd(events) {
477
+ let index = -1;
478
+ const newEvents = [];
479
+ while (++index < events.length) {
480
+ const token = events[index][1];
481
+ newEvents.push(events[index]);
482
+ if (token.type === types.labelImage || token.type === types.labelLink || token.type === types.labelEnd) {
483
+ const offset = token.type === types.labelImage ? 4 : 2;
484
+ token.type = types.data;
485
+ index += offset;
486
+ }
487
+ }
488
+ if (events.length !== newEvents.length) {
489
+ events.length = 0;
490
+ events.push(...newEvents);
491
+ }
492
+ return events;
493
+ }
494
+ function resolveToLabelEnd(events, context) {
495
+ let index = events.length;
496
+ let offset = 0;
497
+ let token;
498
+ let open;
499
+ let close;
500
+ let media;
501
+ while (index--) {
502
+ token = events[index][1];
503
+ if (open !== void 0) {
504
+ if (token.type === types.link || token.type === types.labelLink && token._inactive) {
505
+ break;
506
+ }
507
+ if (events[index][0] === "enter" && token.type === types.labelLink) {
508
+ token._inactive = true;
509
+ }
510
+ } else if (close !== void 0) {
511
+ if (events[index][0] === "enter" && (token.type === types.labelImage || token.type === types.labelLink) && !token._balanced) {
512
+ open = index;
513
+ if (token.type !== types.labelLink) {
514
+ offset = 2;
515
+ break;
516
+ }
517
+ }
518
+ } else if (token.type === types.labelEnd) {
519
+ close = index;
520
+ }
521
+ }
522
+ if (open === void 0 || close === void 0) {
523
+ return events;
524
+ }
525
+ const group = {
526
+ type: events[open][1].type === types.labelLink ? types.link : types.image,
527
+ start: { ...events[open][1].start },
528
+ end: { ...events[events.length - 1][1].end }
529
+ };
530
+ const label = {
531
+ type: types.label,
532
+ start: { ...events[open][1].start },
533
+ end: { ...events[close][1].end }
534
+ };
535
+ const text = {
536
+ type: types.labelText,
537
+ start: { ...events[open + offset + 2][1].end },
538
+ end: { ...events[close - 2][1].start }
539
+ };
540
+ media = [
541
+ ["enter", group, context],
542
+ ["enter", label, context]
543
+ ];
544
+ media.push(...events.slice(open + 1, open + offset + 3));
545
+ media.push(["enter", text, context]);
546
+ media.push(...events.slice(open + offset + 4, close - 3));
547
+ media.push(
548
+ ["exit", text, context],
549
+ events[close - 2],
550
+ events[close - 1],
551
+ ["exit", label, context]
552
+ );
553
+ media.push(...events.slice(close + 1));
554
+ media.push(["exit", group, context]);
555
+ events.splice(open, events.length - open, ...media);
556
+ return events;
557
+ }
558
+ function tokenizeLabelEnd(effects, ok, nok) {
559
+ const self = this;
560
+ let index = self.events.length;
561
+ let labelStart;
562
+ while (index--) {
563
+ if ((self.events[index][1].type === types.labelImage || self.events[index][1].type === types.labelLink) && !self.events[index][1]._balanced) {
564
+ labelStart = self.events[index][1];
565
+ break;
566
+ }
567
+ }
568
+ return start;
569
+ function start(code) {
570
+ if (!labelStart) {
571
+ return nok(code);
572
+ }
573
+ if (labelStart._inactive) {
574
+ return labelEndNok(code);
575
+ }
576
+ if (labelStart.type === types.labelLink) {
577
+ const labelText = self.sliceSerialize({ start: labelStart.end, end: self.now() });
578
+ if (labelText.startsWith("^")) {
579
+ return nok(code);
580
+ }
581
+ }
582
+ effects.enter(types.labelEnd);
583
+ effects.enter(types.labelMarker);
584
+ effects.consume(code);
585
+ effects.exit(types.labelMarker);
586
+ effects.exit(types.labelEnd);
587
+ return after;
588
+ }
589
+ function after(code) {
590
+ if (code === codes.leftParenthesis) {
591
+ return effects.attempt(
592
+ {
593
+ tokenize: tokenizeResource,
594
+ partial: false
595
+ },
596
+ labelEndOk,
597
+ labelEndNok
598
+ // 修复:resource 解析失败时返回 nok
599
+ )(code);
600
+ }
601
+ if (code === codes.leftSquareBracket) {
602
+ return effects.attempt(
603
+ {
604
+ tokenize: tokenizeReferenceFull,
605
+ partial: false
606
+ },
607
+ labelEndOk,
608
+ referenceNotFull
609
+ // 修改:即使不是 full reference,也尝试 collapsed
610
+ )(code);
611
+ }
612
+ return labelEndOk(code);
613
+ }
614
+ function referenceNotFull(code) {
615
+ return effects.attempt(
616
+ {
617
+ tokenize: tokenizeReferenceCollapsed,
618
+ partial: false
619
+ },
620
+ labelEndOk,
621
+ labelEndOk
622
+ // 修改:即使失败也返回 ok
623
+ )(code);
624
+ }
625
+ function labelEndOk(code) {
626
+ return ok(code);
627
+ }
628
+ function labelEndNok(code) {
629
+ labelStart._balanced = true;
630
+ return nok(code);
631
+ }
632
+ }
633
+ function tokenizeResource(effects, ok, nok) {
634
+ return resourceStart;
635
+ function resourceStart(code) {
636
+ if (code !== codes.leftParenthesis) {
637
+ return nok(code);
638
+ }
639
+ effects.enter(types.resource);
640
+ effects.enter(types.resourceMarker);
641
+ effects.consume(code);
642
+ effects.exit(types.resourceMarker);
643
+ return resourceBefore;
644
+ }
645
+ function resourceBefore(code) {
646
+ return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceOpen)(code) : resourceOpen(code);
647
+ }
648
+ function resourceOpen(code) {
649
+ if (code === codes.rightParenthesis) {
650
+ return resourceEnd(code);
651
+ }
652
+ return factoryDestination(
653
+ effects,
654
+ resourceDestinationAfter,
655
+ resourceDestinationMissing,
656
+ types.resourceDestination,
657
+ types.resourceDestinationLiteral,
658
+ types.resourceDestinationLiteralMarker,
659
+ types.resourceDestinationRaw,
660
+ types.resourceDestinationString,
661
+ constants.linkResourceDestinationBalanceMax
662
+ )(code);
663
+ }
664
+ function resourceDestinationAfter(code) {
665
+ return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceBetween)(code) : resourceEnd(code);
666
+ }
667
+ function resourceDestinationMissing(code) {
668
+ return nok(code);
669
+ }
670
+ function resourceBetween(code) {
671
+ if (code === codes.quotationMark || code === codes.apostrophe || code === codes.leftParenthesis) {
672
+ return factoryTitle(
673
+ effects,
674
+ resourceTitleAfter,
675
+ nok,
676
+ types.resourceTitle,
677
+ types.resourceTitleMarker,
678
+ types.resourceTitleString
679
+ )(code);
680
+ }
681
+ return resourceEnd(code);
682
+ }
683
+ function resourceTitleAfter(code) {
684
+ return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceEnd)(code) : resourceEnd(code);
685
+ }
686
+ function resourceEnd(code) {
687
+ if (code === codes.rightParenthesis) {
688
+ effects.enter(types.resourceMarker);
689
+ effects.consume(code);
690
+ effects.exit(types.resourceMarker);
691
+ effects.exit(types.resource);
692
+ return ok;
693
+ }
694
+ return nok(code);
695
+ }
696
+ }
697
+ function tokenizeReferenceFull(effects, ok, nok) {
698
+ const self = this;
699
+ return referenceFull;
700
+ function referenceFull(code) {
701
+ if (code !== codes.leftSquareBracket) {
702
+ return nok(code);
703
+ }
704
+ return factoryLabel.call(
705
+ self,
706
+ effects,
707
+ referenceFullAfter,
708
+ referenceFullMissing,
709
+ types.reference,
710
+ types.referenceMarker,
711
+ types.referenceString
712
+ )(code);
713
+ }
714
+ function referenceFullAfter(code) {
715
+ return ok(code);
716
+ }
717
+ function referenceFullMissing(code) {
718
+ return nok(code);
719
+ }
720
+ }
721
+ function tokenizeReferenceCollapsed(effects, ok, nok) {
722
+ return referenceCollapsedStart;
723
+ function referenceCollapsedStart(code) {
724
+ if (code !== codes.leftSquareBracket) {
725
+ return nok(code);
726
+ }
727
+ effects.enter(types.reference);
728
+ effects.enter(types.referenceMarker);
729
+ effects.consume(code);
730
+ effects.exit(types.referenceMarker);
731
+ return referenceCollapsedOpen;
732
+ }
733
+ function referenceCollapsedOpen(code) {
734
+ if (code === codes.rightSquareBracket) {
735
+ effects.enter(types.referenceMarker);
736
+ effects.consume(code);
737
+ effects.exit(types.referenceMarker);
738
+ effects.exit(types.reference);
739
+ return ok;
740
+ }
741
+ return nok(code);
742
+ }
743
+ }
744
+ function gfmFootnoteIncremental() {
745
+ const original = gfmFootnote();
746
+ return {
747
+ ...original,
748
+ text: {
749
+ ...original.text,
750
+ // 覆盖 text[91] (`[` 的处理) - 这是脚注引用解析的起点
751
+ [codes.leftSquareBracket]: {
752
+ ...original.text[codes.leftSquareBracket],
753
+ tokenize: tokenizeGfmFootnoteCallIncremental
754
+ },
755
+ // 覆盖 text[93] (`]` 的处理) - 用于处理 ![^1] 这样的情况
756
+ [codes.rightSquareBracket]: {
757
+ ...original.text[codes.rightSquareBracket],
758
+ tokenize: tokenizePotentialGfmFootnoteCallIncremental
759
+ }
760
+ }
761
+ };
762
+ }
763
+ function tokenizeGfmFootnoteCallIncremental(effects, ok, nok) {
764
+ let size = 0;
765
+ let data = false;
766
+ return start;
767
+ function start(code) {
768
+ if (code !== codes.leftSquareBracket) {
769
+ return nok(code);
770
+ }
771
+ effects.enter("gfmFootnoteCall");
772
+ effects.enter("gfmFootnoteCallLabelMarker");
773
+ effects.consume(code);
774
+ effects.exit("gfmFootnoteCallLabelMarker");
775
+ return callStart;
776
+ }
777
+ function callStart(code) {
778
+ if (code !== codes.caret) {
779
+ return nok(code);
780
+ }
781
+ effects.enter("gfmFootnoteCallMarker");
782
+ effects.consume(code);
783
+ effects.exit("gfmFootnoteCallMarker");
784
+ effects.enter("gfmFootnoteCallString");
785
+ const token = effects.enter("chunkString");
786
+ token.contentType = "string";
787
+ return callData;
788
+ }
789
+ function callData(code) {
790
+ if (
791
+ // 太长
792
+ size > constants.linkReferenceSizeMax || // 右括号但没有数据
793
+ code === codes.rightSquareBracket && !data || // EOF、换行、空格、制表符、左括号不支持
794
+ code === codes.eof || code === codes.leftSquareBracket || markdownLineEndingOrSpace(code)
795
+ ) {
796
+ return nok(code);
797
+ }
798
+ if (code === codes.rightSquareBracket) {
799
+ effects.exit("chunkString");
800
+ effects.exit("gfmFootnoteCallString");
801
+ effects.enter("gfmFootnoteCallLabelMarker");
802
+ effects.consume(code);
803
+ effects.exit("gfmFootnoteCallLabelMarker");
804
+ effects.exit("gfmFootnoteCall");
805
+ return ok;
806
+ }
807
+ if (!markdownLineEndingOrSpace(code)) {
808
+ data = true;
809
+ }
810
+ size++;
811
+ effects.consume(code);
812
+ return code === codes.backslash ? callEscape : callData;
813
+ }
814
+ function callEscape(code) {
815
+ if (code === codes.leftSquareBracket || code === codes.backslash || code === codes.rightSquareBracket) {
816
+ effects.consume(code);
817
+ size++;
818
+ return callData;
819
+ }
820
+ return callData(code);
821
+ }
822
+ }
823
+ function tokenizePotentialGfmFootnoteCallIncremental(effects, ok, nok) {
824
+ const self = this;
825
+ let index = self.events.length;
826
+ let labelStart;
827
+ while (index--) {
828
+ const token = self.events[index][1];
829
+ if (token.type === "labelImage") {
830
+ labelStart = token;
831
+ break;
832
+ }
833
+ if (token.type === "gfmFootnoteCall" || token.type === "labelLink" || token.type === "label" || token.type === "image" || token.type === "link") {
834
+ break;
835
+ }
836
+ }
837
+ return start;
838
+ function start(code) {
839
+ if (code !== codes.rightSquareBracket) {
840
+ return nok(code);
841
+ }
842
+ if (!labelStart || !labelStart._balanced) {
843
+ return nok(code);
844
+ }
845
+ const id = normalizeIdentifier(
846
+ self.sliceSerialize({
847
+ start: labelStart.end,
848
+ end: self.now()
849
+ })
850
+ );
851
+ if (id.codePointAt(0) !== codes.caret) {
852
+ return nok(code);
853
+ }
854
+ effects.enter("gfmFootnoteCallLabelMarker");
855
+ effects.consume(code);
856
+ effects.exit("gfmFootnoteCallLabelMarker");
857
+ return ok(code);
858
+ }
859
+ }
860
+
7
861
  // src/detector/index.ts
8
862
  var RE_FENCE_START = /^(\s*)((`{3,})|(~{3,}))/;
9
863
  var RE_EMPTY_LINE = /^\s*$/;
@@ -16,6 +870,8 @@ var RE_HTML_BLOCK_1 = /^\s{0,3}<(script|pre|style|textarea|!--|!DOCTYPE|\?|!\[CD
16
870
  var RE_HTML_BLOCK_2 = /^\s{0,3}<\/?[a-zA-Z][a-zA-Z0-9-]*(\s|>|$)/;
17
871
  var RE_TABLE_DELIMITER = /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?$/;
18
872
  var RE_ESCAPE_SPECIAL = /[.*+?^${}()|[\]\\]/g;
873
+ var RE_FOOTNOTE_DEFINITION = /^\[\^[^\]]+\]:\s/;
874
+ var RE_FOOTNOTE_CONTINUATION = /^(?: |\t)/;
19
875
  var fenceEndPatternCache = /* @__PURE__ */ new Map();
20
876
  var containerPatternCache = /* @__PURE__ */ new Map();
21
877
  function detectFenceStart(line) {
@@ -68,6 +924,12 @@ function isHtmlBlock(line) {
68
924
  function isTableDelimiter(line) {
69
925
  return RE_TABLE_DELIMITER.test(line.trim());
70
926
  }
927
+ function isFootnoteDefinitionStart(line) {
928
+ return RE_FOOTNOTE_DEFINITION.test(line);
929
+ }
930
+ function isFootnoteContinuation(line) {
931
+ return RE_FOOTNOTE_CONTINUATION.test(line);
932
+ }
71
933
  function detectContainer(line, config) {
72
934
  const marker = config?.marker || ":";
73
935
  const minLength = config?.minMarkerLength || 3;
@@ -179,6 +1041,34 @@ function updateContext(line, context, containerConfig) {
179
1041
  return newContext;
180
1042
  }
181
1043
 
1044
+ // src/utils/index.ts
1045
+ var idCounter = 0;
1046
+ function generateId(prefix = "block") {
1047
+ return `${prefix}-${++idCounter}`;
1048
+ }
1049
+ function resetIdCounter() {
1050
+ idCounter = 0;
1051
+ }
1052
+ function calculateLineOffset(lines, lineIndex) {
1053
+ let offset = 0;
1054
+ for (let i = 0; i < lineIndex && i < lines.length; i++) {
1055
+ offset += lines[i].length + 1;
1056
+ }
1057
+ return offset;
1058
+ }
1059
+ function splitLines(text) {
1060
+ return text.split("\n");
1061
+ }
1062
+ function joinLines(lines, start, end) {
1063
+ return lines.slice(start, end + 1).join("\n");
1064
+ }
1065
+ function isDefinitionNode(node) {
1066
+ return node.type === "definition";
1067
+ }
1068
+ function isFootnoteDefinitionNode(node) {
1069
+ return node.type === "footnoteDefinition";
1070
+ }
1071
+
182
1072
  // src/parser/IncremarkParser.ts
183
1073
  var IncremarkParser = class {
184
1074
  buffer = "";
@@ -192,8 +1082,16 @@ var IncremarkParser = class {
192
1082
  options;
193
1083
  /** 缓存的容器配置,避免重复计算 */
194
1084
  containerConfig;
1085
+ /** 缓存的 HTML 树配置,避免重复计算 */
1086
+ htmlTreeConfig;
195
1087
  /** 上次 append 返回的 pending blocks,用于 getAst 复用 */
196
1088
  lastPendingBlocks = [];
1089
+ /** Definition 映射表(用于引用式图片和链接) */
1090
+ definitionMap = {};
1091
+ /** Footnote Definition 映射表 */
1092
+ footnoteDefinitionMap = {};
1093
+ /** Footnote Reference 出现顺序(按引用在文档中的顺序) */
1094
+ footnoteReferenceOrder = [];
197
1095
  constructor(options = {}) {
198
1096
  this.options = {
199
1097
  gfm: true,
@@ -201,6 +1099,7 @@ var IncremarkParser = class {
201
1099
  };
202
1100
  this.context = createInitialContext();
203
1101
  this.containerConfig = this.computeContainerConfig();
1102
+ this.htmlTreeConfig = this.computeHtmlTreeConfig();
204
1103
  }
205
1104
  generateBlockId() {
206
1105
  return `block-${++this.blockIdCounter}`;
@@ -210,12 +1109,82 @@ var IncremarkParser = class {
210
1109
  if (!containers) return void 0;
211
1110
  return containers === true ? {} : containers;
212
1111
  }
1112
+ computeHtmlTreeConfig() {
1113
+ const htmlTree = this.options.htmlTree;
1114
+ if (!htmlTree) return void 0;
1115
+ return htmlTree === true ? {} : htmlTree;
1116
+ }
1117
+ /**
1118
+ * 将 HTML 节点转换为纯文本
1119
+ * 递归处理 AST 中所有 html 类型的节点
1120
+ * - 块级 HTML 节点 → 转换为 paragraph 包含 text
1121
+ * - 内联 HTML 节点(在段落内部)→ 转换为 text 节点
1122
+ */
1123
+ convertHtmlToText(ast) {
1124
+ const processInlineChildren = (children) => {
1125
+ return children.map((node) => {
1126
+ const n = node;
1127
+ if (n.type === "html") {
1128
+ const htmlNode = n;
1129
+ const textNode = {
1130
+ type: "text",
1131
+ value: htmlNode.value,
1132
+ position: htmlNode.position
1133
+ };
1134
+ return textNode;
1135
+ }
1136
+ if ("children" in n && Array.isArray(n.children)) {
1137
+ const parent = n;
1138
+ return {
1139
+ ...parent,
1140
+ children: processInlineChildren(parent.children)
1141
+ };
1142
+ }
1143
+ return n;
1144
+ });
1145
+ };
1146
+ const processBlockChildren = (children) => {
1147
+ return children.map((node) => {
1148
+ if (node.type === "html") {
1149
+ const htmlNode = node;
1150
+ const textNode = {
1151
+ type: "text",
1152
+ value: htmlNode.value
1153
+ };
1154
+ const paragraphNode = {
1155
+ type: "paragraph",
1156
+ children: [textNode],
1157
+ position: htmlNode.position
1158
+ };
1159
+ return paragraphNode;
1160
+ }
1161
+ if ("children" in node && Array.isArray(node.children)) {
1162
+ const parent = node;
1163
+ if (node.type === "paragraph" || node.type === "heading" || node.type === "tableCell" || node.type === "delete" || node.type === "emphasis" || node.type === "strong" || node.type === "link" || node.type === "linkReference") {
1164
+ return {
1165
+ ...parent,
1166
+ children: processInlineChildren(parent.children)
1167
+ };
1168
+ }
1169
+ return {
1170
+ ...parent,
1171
+ children: processBlockChildren(parent.children)
1172
+ };
1173
+ }
1174
+ return node;
1175
+ });
1176
+ };
1177
+ return {
1178
+ ...ast,
1179
+ children: processBlockChildren(ast.children)
1180
+ };
1181
+ }
213
1182
  parse(text) {
214
1183
  const extensions = [];
215
1184
  const mdastExtensions = [];
216
1185
  if (this.options.gfm) {
217
1186
  extensions.push(gfm());
218
- mdastExtensions.push(...gfmFromMarkdown());
1187
+ mdastExtensions.push(...gfmFromMarkdown(), gfmFootnoteFromMarkdown());
219
1188
  }
220
1189
  if (this.options.extensions) {
221
1190
  extensions.push(...this.options.extensions);
@@ -223,7 +1192,79 @@ var IncremarkParser = class {
223
1192
  if (this.options.mdastExtensions) {
224
1193
  mdastExtensions.push(...this.options.mdastExtensions);
225
1194
  }
226
- return fromMarkdown(text, { extensions, mdastExtensions });
1195
+ if (this.options.gfm) {
1196
+ extensions.push(gfmFootnoteIncremental());
1197
+ }
1198
+ extensions.push(micromarkReferenceExtension());
1199
+ let ast = fromMarkdown(text, { extensions, mdastExtensions });
1200
+ if (this.htmlTreeConfig) {
1201
+ ast = transformHtmlNodes(ast, this.htmlTreeConfig);
1202
+ } else {
1203
+ ast = this.convertHtmlToText(ast);
1204
+ }
1205
+ return ast;
1206
+ }
1207
+ updateDefinationsFromComplatedBlocks(blocks) {
1208
+ for (const block of blocks) {
1209
+ this.definitionMap = {
1210
+ ...this.definitionMap,
1211
+ ...this.findDefinition(block)
1212
+ };
1213
+ this.footnoteDefinitionMap = {
1214
+ ...this.footnoteDefinitionMap,
1215
+ ...this.findFootnoteDefinition(block)
1216
+ };
1217
+ }
1218
+ }
1219
+ findDefinition(block) {
1220
+ const definitions = [];
1221
+ function findDefination(node) {
1222
+ if (isDefinitionNode(node)) {
1223
+ definitions.push(node);
1224
+ }
1225
+ if ("children" in node && Array.isArray(node.children)) {
1226
+ for (const child of node.children) {
1227
+ findDefination(child);
1228
+ }
1229
+ }
1230
+ }
1231
+ findDefination(block.node);
1232
+ return definitions.reduce((acc, node) => {
1233
+ acc[node.identifier] = node;
1234
+ return acc;
1235
+ }, {});
1236
+ }
1237
+ findFootnoteDefinition(block) {
1238
+ const footnoteDefinitions = [];
1239
+ function findFootnoteDefinition(node) {
1240
+ if (isFootnoteDefinitionNode(node)) {
1241
+ footnoteDefinitions.push(node);
1242
+ }
1243
+ }
1244
+ findFootnoteDefinition(block.node);
1245
+ return footnoteDefinitions.reduce((acc, node) => {
1246
+ acc[node.identifier] = node;
1247
+ return acc;
1248
+ }, {});
1249
+ }
1250
+ /**
1251
+ * 收集 AST 中的脚注引用(按出现顺序)
1252
+ * 用于确定脚注的显示顺序
1253
+ */
1254
+ collectFootnoteReferences(nodes) {
1255
+ const visitNode = (node) => {
1256
+ if (!node) return;
1257
+ if (node.type === "footnoteReference") {
1258
+ const identifier = node.identifier;
1259
+ if (!this.footnoteReferenceOrder.includes(identifier)) {
1260
+ this.footnoteReferenceOrder.push(identifier);
1261
+ }
1262
+ }
1263
+ if (node.children && Array.isArray(node.children)) {
1264
+ node.children.forEach(visitNode);
1265
+ }
1266
+ };
1267
+ nodes.forEach(visitNode);
227
1268
  }
228
1269
  /**
229
1270
  * 增量更新 lines 和 lineOffsets
@@ -310,7 +1351,30 @@ var IncremarkParser = class {
310
1351
  if (lineIndex >= this.lines.length - 1) {
311
1352
  return -1;
312
1353
  }
1354
+ if (isFootnoteDefinitionStart(prevLine)) {
1355
+ if (isEmptyLine(line) || isFootnoteContinuation(line)) {
1356
+ return -1;
1357
+ }
1358
+ if (isFootnoteDefinitionStart(line)) {
1359
+ return lineIndex - 1;
1360
+ }
1361
+ }
1362
+ if (!isEmptyLine(prevLine) && isFootnoteContinuation(prevLine)) {
1363
+ const footnoteStartLine = this.findFootnoteStart(lineIndex - 1);
1364
+ if (footnoteStartLine >= 0) {
1365
+ if (isEmptyLine(line) || isFootnoteContinuation(line)) {
1366
+ return -1;
1367
+ }
1368
+ if (isFootnoteDefinitionStart(line)) {
1369
+ return lineIndex - 1;
1370
+ }
1371
+ return lineIndex - 1;
1372
+ }
1373
+ }
313
1374
  if (!isEmptyLine(prevLine)) {
1375
+ if (isFootnoteDefinitionStart(line) && !isFootnoteDefinitionStart(prevLine)) {
1376
+ return lineIndex - 1;
1377
+ }
314
1378
  if (isHeading(line)) {
315
1379
  return lineIndex - 1;
316
1380
  }
@@ -338,6 +1402,37 @@ var IncremarkParser = class {
338
1402
  }
339
1403
  return -1;
340
1404
  }
1405
+ /**
1406
+ * 从指定行向上查找脚注定义的起始行
1407
+ *
1408
+ * @param fromLine 开始查找的行索引
1409
+ * @returns 脚注起始行索引,如果不属于脚注返回 -1
1410
+ *
1411
+ * @example
1412
+ * // 假设 lines 为:
1413
+ * // 0: "[^1]: 第一行"
1414
+ * // 1: " 第二行"
1415
+ * // 2: " 第三行"
1416
+ * findFootnoteStart(2) // 返回 0
1417
+ * findFootnoteStart(1) // 返回 0
1418
+ */
1419
+ findFootnoteStart(fromLine) {
1420
+ const maxLookback = 20;
1421
+ const startLine = Math.max(0, fromLine - maxLookback);
1422
+ for (let i = fromLine; i >= startLine; i--) {
1423
+ const line = this.lines[i];
1424
+ if (isFootnoteDefinitionStart(line)) {
1425
+ return i;
1426
+ }
1427
+ if (isEmptyLine(line)) {
1428
+ continue;
1429
+ }
1430
+ if (!isFootnoteContinuation(line)) {
1431
+ return -1;
1432
+ }
1433
+ }
1434
+ return -1;
1435
+ }
341
1436
  nodesToBlocks(nodes, startOffset, rawText, status) {
342
1437
  const blocks = [];
343
1438
  let currentOffset = startOffset;
@@ -368,7 +1463,10 @@ var IncremarkParser = class {
368
1463
  completed: [],
369
1464
  updated: [],
370
1465
  pending: [],
371
- ast: { type: "root", children: [] }
1466
+ ast: { type: "root", children: [] },
1467
+ definitions: {},
1468
+ footnoteDefinitions: {},
1469
+ footnoteReferenceOrder: []
372
1470
  };
373
1471
  if (stableBoundary >= this.pendingStartLine && stableBoundary >= 0) {
374
1472
  const stableText = this.lines.slice(this.pendingStartLine, stableBoundary + 1).join("\n");
@@ -377,6 +1475,7 @@ var IncremarkParser = class {
377
1475
  const newBlocks = this.nodesToBlocks(ast.children, stableOffset, stableText, "completed");
378
1476
  this.completedBlocks.push(...newBlocks);
379
1477
  update.completed = newBlocks;
1478
+ this.updateDefinationsFromComplatedBlocks(newBlocks);
380
1479
  this.context = contextAtLine;
381
1480
  this.pendingStartLine = stableBoundary + 1;
382
1481
  }
@@ -393,6 +1492,10 @@ var IncremarkParser = class {
393
1492
  type: "root",
394
1493
  children: [...this.completedBlocks.map((b) => b.node), ...update.pending.map((b) => b.node)]
395
1494
  };
1495
+ this.collectFootnoteReferences(update.ast.children);
1496
+ update.definitions = this.getDefinitionMap();
1497
+ update.footnoteDefinitions = this.getFootnoteDefinitionMap();
1498
+ update.footnoteReferenceOrder = this.getFootnoteReferenceOrder();
396
1499
  this.emitChange(update.pending);
397
1500
  return update;
398
1501
  }
@@ -401,7 +1504,7 @@ var IncremarkParser = class {
401
1504
  */
402
1505
  emitChange(pendingBlocks = []) {
403
1506
  if (this.options.onChange) {
404
- this.options.onChange({
1507
+ const state = {
405
1508
  completedBlocks: this.completedBlocks,
406
1509
  pendingBlocks,
407
1510
  markdown: this.buffer,
@@ -411,8 +1514,11 @@ var IncremarkParser = class {
411
1514
  ...this.completedBlocks.map((b) => b.node),
412
1515
  ...pendingBlocks.map((b) => b.node)
413
1516
  ]
414
- }
415
- });
1517
+ },
1518
+ definitions: { ...this.definitionMap },
1519
+ footnoteDefinitions: { ...this.footnoteDefinitionMap }
1520
+ };
1521
+ this.options.onChange(state);
416
1522
  }
417
1523
  }
418
1524
  /**
@@ -424,7 +1530,10 @@ var IncremarkParser = class {
424
1530
  completed: [],
425
1531
  updated: [],
426
1532
  pending: [],
427
- ast: { type: "root", children: [] }
1533
+ ast: { type: "root", children: [] },
1534
+ definitions: {},
1535
+ footnoteDefinitions: {},
1536
+ footnoteReferenceOrder: []
428
1537
  };
429
1538
  if (this.pendingStartLine < this.lines.length) {
430
1539
  const remainingText = this.lines.slice(this.pendingStartLine).join("\n");
@@ -439,6 +1548,7 @@ var IncremarkParser = class {
439
1548
  );
440
1549
  this.completedBlocks.push(...finalBlocks);
441
1550
  update.completed = finalBlocks;
1551
+ this.updateDefinationsFromComplatedBlocks(finalBlocks);
442
1552
  }
443
1553
  }
444
1554
  this.lastPendingBlocks = [];
@@ -447,6 +1557,10 @@ var IncremarkParser = class {
447
1557
  type: "root",
448
1558
  children: this.completedBlocks.map((b) => b.node)
449
1559
  };
1560
+ this.collectFootnoteReferences(update.ast.children);
1561
+ update.definitions = this.getDefinitionMap();
1562
+ update.footnoteDefinitions = this.getFootnoteDefinitionMap();
1563
+ update.footnoteReferenceOrder = this.getFootnoteReferenceOrder();
450
1564
  this.emitChange([]);
451
1565
  return update;
452
1566
  }
@@ -462,12 +1576,14 @@ var IncremarkParser = class {
462
1576
  * 复用上次 append 的 pending 结果,避免重复解析
463
1577
  */
464
1578
  getAst() {
1579
+ const children = [
1580
+ ...this.completedBlocks.map((b) => b.node),
1581
+ ...this.lastPendingBlocks.map((b) => b.node)
1582
+ ];
1583
+ this.collectFootnoteReferences(children);
465
1584
  return {
466
1585
  type: "root",
467
- children: [
468
- ...this.completedBlocks.map((b) => b.node),
469
- ...this.lastPendingBlocks.map((b) => b.node)
470
- ]
1586
+ children
471
1587
  };
472
1588
  }
473
1589
  /**
@@ -482,11 +1598,33 @@ var IncremarkParser = class {
482
1598
  getBuffer() {
483
1599
  return this.buffer;
484
1600
  }
1601
+ /**
1602
+ * 获取 Definition 映射表(用于引用式图片和链接)
1603
+ */
1604
+ getDefinitionMap() {
1605
+ return { ...this.definitionMap };
1606
+ }
1607
+ /**
1608
+ * 获取 Footnote Definition 映射表
1609
+ */
1610
+ getFootnoteDefinitionMap() {
1611
+ return { ...this.footnoteDefinitionMap };
1612
+ }
1613
+ /**
1614
+ * 获取脚注引用的出现顺序
1615
+ */
1616
+ getFootnoteReferenceOrder() {
1617
+ return [...this.footnoteReferenceOrder];
1618
+ }
485
1619
  /**
486
1620
  * 设置状态变化回调(用于 DevTools 等)
487
1621
  */
488
1622
  setOnChange(callback) {
489
- this.options.onChange = callback;
1623
+ const originalOnChange = this.options.onChange;
1624
+ this.options.onChange = (state) => {
1625
+ originalOnChange?.(state);
1626
+ callback?.(state);
1627
+ };
490
1628
  }
491
1629
  /**
492
1630
  * 重置解析器状态
@@ -500,6 +1638,9 @@ var IncremarkParser = class {
500
1638
  this.blockIdCounter = 0;
501
1639
  this.context = createInitialContext();
502
1640
  this.lastPendingBlocks = [];
1641
+ this.definitionMap = {};
1642
+ this.footnoteDefinitionMap = {};
1643
+ this.footnoteReferenceOrder = [];
503
1644
  this.emitChange([]);
504
1645
  }
505
1646
  /**
@@ -517,28 +1658,6 @@ function createIncremarkParser(options) {
517
1658
  return new IncremarkParser(options);
518
1659
  }
519
1660
 
520
- // src/utils/index.ts
521
- var idCounter = 0;
522
- function generateId(prefix = "block") {
523
- return `${prefix}-${++idCounter}`;
524
- }
525
- function resetIdCounter() {
526
- idCounter = 0;
527
- }
528
- function calculateLineOffset(lines, lineIndex) {
529
- let offset = 0;
530
- for (let i = 0; i < lineIndex && i < lines.length; i++) {
531
- offset += lines[i].length + 1;
532
- }
533
- return offset;
534
- }
535
- function splitLines(text) {
536
- return text.split("\n");
537
- }
538
- function joinLines(lines, start, end) {
539
- return lines.slice(start, end + 1).join("\n");
540
- }
541
-
542
1661
  // src/transformer/utils.ts
543
1662
  function countChars(node) {
544
1663
  return countCharsInNode(node);
@@ -1256,7 +2375,51 @@ function createPlugin(name, matcher, options = {}) {
1256
2375
  ...options
1257
2376
  };
1258
2377
  }
2378
+ /**
2379
+ * @file Micromark 扩展:支持增量解析的 Reference 语法
2380
+ *
2381
+ * @description
2382
+ * 在增量解析场景中,引用式图片/链接(如 `![Alt][id]`)可能在定义(`[id]: url`)之前出现。
2383
+ * 标准 micromark 会检查 parser.defined,如果 id 未定义就解析为文本。
2384
+ *
2385
+ * 本扩展通过覆盖 labelEnd 构造,移除 parser.defined 检查,
2386
+ * 使得 reference 语法总是被解析为 reference token,
2387
+ * 由渲染层根据实际的 definitionMap 决定如何渲染。
2388
+ *
2389
+ * @module micromark-reference-extension
2390
+ *
2391
+ * @features
2392
+ * - ✅ 支持所有 resource 语法(带 title 的图片/链接)
2393
+ * - ✅ 支持所有 reference 语法(full, collapsed, shortcut)
2394
+ * - ✅ 延迟验证:解析时不检查定义是否存在
2395
+ * - ✅ 使用官方 factory 函数,保证与 CommonMark 标准一致
2396
+ *
2397
+ * @dependencies
2398
+ * - micromark-factory-destination: 解析 URL(支持尖括号、括号平衡)
2399
+ * - micromark-factory-title: 解析 title(支持三种引号,支持多行)
2400
+ * - micromark-factory-label: 解析 label(支持转义、长度限制)
2401
+ * - micromark-factory-whitespace: 解析空白符(正确生成 lineEnding/linePrefix token)
2402
+ * - micromark-util-character: 字符判断工具
2403
+ * - micromark-util-symbol: 常量(codes, types, constants)
2404
+ * - micromark-util-types: TypeScript 类型定义
2405
+ *
2406
+ * @see {@link https://github.com/micromark/micromark} - micromark 官方文档
2407
+ * @see {@link https://spec.commonmark.org/0.30/#images} - CommonMark 图片规范
2408
+ * @see {@link https://spec.commonmark.org/0.30/#links} - CommonMark 链接规范
2409
+ *
2410
+ * @example
2411
+ * ```typescript
2412
+ * import { micromarkReferenceExtension } from './micromark-reference-extension'
2413
+ * import { fromMarkdown } from 'mdast-util-from-markdown'
2414
+ *
2415
+ * const extensions = [micromarkReferenceExtension()]
2416
+ * const ast = fromMarkdown(text, { extensions })
2417
+ * ```
2418
+ *
2419
+ * @author Incremark Team
2420
+ * @license MIT
2421
+ */
1259
2422
 
1260
- export { BlockTransformer, IncremarkParser, allPlugins, calculateLineOffset, cloneNode, codeBlockPlugin, countChars, createBlockTransformer, createIncremarkParser, createInitialContext, createPlugin, defaultPlugins, detectContainer, detectContainerEnd, detectFenceEnd, detectFenceStart, generateId, imagePlugin, isBlockBoundary, isBlockquoteStart, isEmptyLine, isHeading, isHtmlBlock, isListItemStart, isTableDelimiter, isThematicBreak, joinLines, mathPlugin, mermaidPlugin, resetIdCounter, sliceAst, splitLines, thematicBreakPlugin, updateContext };
2423
+ export { BlockTransformer, DEFAULT_ATTR_BLACKLIST as HTML_ATTR_BLACKLIST, DEFAULT_PROTOCOL_BLACKLIST as HTML_PROTOCOL_BLACKLIST, DEFAULT_TAG_BLACKLIST as HTML_TAG_BLACKLIST, IncremarkParser, allPlugins, calculateLineOffset, cloneNode, codeBlockPlugin, countChars, createBlockTransformer, createHtmlTreeTransformer, createIncremarkParser, createInitialContext, createPlugin, defaultPlugins, detectContainer, detectContainerEnd, detectFenceEnd, detectFenceStart, detectHtmlContentType, findHtmlElementsByTag, generateId, htmlElementToString, htmlTreeExtension, imagePlugin, isBlockBoundary, isBlockquoteStart, isEmptyLine, isHeading, isHtmlBlock, isHtmlElementNode, isListItemStart, isTableDelimiter, isThematicBreak, joinLines, mathPlugin, mermaidPlugin, parseHtmlFragment, parseHtmlTag, resetIdCounter, sliceAst, splitLines, thematicBreakPlugin, transformHtmlNodes, updateContext, walkHtmlElements };
1261
2424
  //# sourceMappingURL=index.js.map
1262
2425
  //# sourceMappingURL=index.js.map