@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/detector/index.d.ts +1 -1
- package/dist/detector/index.js +9 -1
- package/dist/detector/index.js.map +1 -1
- package/dist/index-3rgnFbip.d.ts +396 -0
- package/dist/index.d.ts +53 -2
- package/dist/index.js +1198 -35
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +6 -1
- package/dist/utils/index.js +7 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +10 -1
- package/src/__tests__/footnote.test.ts +214 -0
- package/src/detector/index.ts +30 -0
- package/src/extensions/html-extension/index.test.ts +409 -0
- package/src/extensions/html-extension/index.ts +792 -0
- package/src/extensions/micromark-gfm-footnote-incremental.ts +275 -0
- package/src/extensions/micromark-reference-extension.ts +724 -0
- package/src/index.ts +33 -0
- package/src/parser/IncremarkParser.footnote.test.ts +334 -0
- package/src/parser/IncremarkParser.ts +374 -14
- package/src/types/index.ts +29 -1
- package/src/utils/index.ts +9 -0
- package/dist/index-ChNeZ1wr.d.ts +0 -217
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Micromark 扩展:支持增量解析的 Reference 语法
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 在增量解析场景中,引用式图片/链接(如 `![Alt][id]`)可能在定义(`[id]: url`)之前出现。
|
|
6
|
+
* 标准 micromark 会检查 parser.defined,如果 id 未定义就解析为文本。
|
|
7
|
+
*
|
|
8
|
+
* 本扩展通过覆盖 labelEnd 构造,移除 parser.defined 检查,
|
|
9
|
+
* 使得 reference 语法总是被解析为 reference token,
|
|
10
|
+
* 由渲染层根据实际的 definitionMap 决定如何渲染。
|
|
11
|
+
*
|
|
12
|
+
* @module micromark-reference-extension
|
|
13
|
+
*
|
|
14
|
+
* @features
|
|
15
|
+
* - ✅ 支持所有 resource 语法(带 title 的图片/链接)
|
|
16
|
+
* - ✅ 支持所有 reference 语法(full, collapsed, shortcut)
|
|
17
|
+
* - ✅ 延迟验证:解析时不检查定义是否存在
|
|
18
|
+
* - ✅ 使用官方 factory 函数,保证与 CommonMark 标准一致
|
|
19
|
+
*
|
|
20
|
+
* @dependencies
|
|
21
|
+
* - micromark-factory-destination: 解析 URL(支持尖括号、括号平衡)
|
|
22
|
+
* - micromark-factory-title: 解析 title(支持三种引号,支持多行)
|
|
23
|
+
* - micromark-factory-label: 解析 label(支持转义、长度限制)
|
|
24
|
+
* - micromark-factory-whitespace: 解析空白符(正确生成 lineEnding/linePrefix token)
|
|
25
|
+
* - micromark-util-character: 字符判断工具
|
|
26
|
+
* - micromark-util-symbol: 常量(codes, types, constants)
|
|
27
|
+
* - micromark-util-types: TypeScript 类型定义
|
|
28
|
+
*
|
|
29
|
+
* @see {@link https://github.com/micromark/micromark} - micromark 官方文档
|
|
30
|
+
* @see {@link https://spec.commonmark.org/0.30/#images} - CommonMark 图片规范
|
|
31
|
+
* @see {@link https://spec.commonmark.org/0.30/#links} - CommonMark 链接规范
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { micromarkReferenceExtension } from './micromark-reference-extension'
|
|
36
|
+
* import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
37
|
+
*
|
|
38
|
+
* const extensions = [micromarkReferenceExtension()]
|
|
39
|
+
* const ast = fromMarkdown(text, { extensions })
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @author Incremark Team
|
|
43
|
+
* @license MIT
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import type {
|
|
47
|
+
Code,
|
|
48
|
+
Construct,
|
|
49
|
+
Extension,
|
|
50
|
+
Event,
|
|
51
|
+
Resolver,
|
|
52
|
+
State,
|
|
53
|
+
TokenizeContext,
|
|
54
|
+
Tokenizer,
|
|
55
|
+
Token
|
|
56
|
+
} from 'micromark-util-types'
|
|
57
|
+
import { codes, types, constants } from 'micromark-util-symbol'
|
|
58
|
+
import {
|
|
59
|
+
markdownLineEnding,
|
|
60
|
+
markdownSpace,
|
|
61
|
+
markdownLineEndingOrSpace
|
|
62
|
+
} from 'micromark-util-character'
|
|
63
|
+
import { factoryDestination } from 'micromark-factory-destination'
|
|
64
|
+
import { factoryTitle } from 'micromark-factory-title'
|
|
65
|
+
import { factoryLabel } from 'micromark-factory-label'
|
|
66
|
+
import { factoryWhitespace } from 'micromark-factory-whitespace'
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 创建支持增量解析的 reference 扩展
|
|
70
|
+
*
|
|
71
|
+
* 这个扩展覆盖了 micromark-core-commonmark 中的 labelEnd 构造,
|
|
72
|
+
* 移除了对 parser.defined 的检查,使得 reference 语法总是被解析为 reference token,
|
|
73
|
+
* 即使对应的 definition 尚未出现。
|
|
74
|
+
*
|
|
75
|
+
* @returns Micromark 扩展对象
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* - labelEnd 在 text 中注册,键是 `codes.rightSquareBracket`(']')
|
|
79
|
+
* - 我们使用相同的键来覆盖它
|
|
80
|
+
* - 根据 combineExtensions 的逻辑,后添加的扩展会先被尝试
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // 在 IncremarkParser 中使用
|
|
85
|
+
* const extensions = [
|
|
86
|
+
* gfm(),
|
|
87
|
+
* micromarkReferenceExtension() // 最后添加,确保覆盖
|
|
88
|
+
* ]
|
|
89
|
+
* const ast = fromMarkdown(text, { extensions })
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function micromarkReferenceExtension(): Extension {
|
|
93
|
+
// 关键:不使用 disable,直接覆盖
|
|
94
|
+
// 根据 combineExtensions 的逻辑,后添加的扩展会先被尝试(before 数组会被插入到 existing 的开头)
|
|
95
|
+
return {
|
|
96
|
+
// 在 text 中使用 codes.rightSquareBracket 键覆盖 labelEnd
|
|
97
|
+
text: {
|
|
98
|
+
[codes.rightSquareBracket]: {
|
|
99
|
+
name: 'labelEnd',
|
|
100
|
+
resolveAll: resolveAllLabelEnd,
|
|
101
|
+
resolveTo: resolveToLabelEnd,
|
|
102
|
+
tokenize: tokenizeLabelEnd,
|
|
103
|
+
// 添加 add: 'before' 确保先被尝试
|
|
104
|
+
add: 'before'
|
|
105
|
+
} as Construct
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve all label end events.
|
|
112
|
+
* 从原始代码复制,保持不变。
|
|
113
|
+
*/
|
|
114
|
+
function resolveAllLabelEnd(events: Event[]): Event[] {
|
|
115
|
+
let index = -1
|
|
116
|
+
const newEvents: Event[] = []
|
|
117
|
+
while (++index < events.length) {
|
|
118
|
+
const token = events[index][1]
|
|
119
|
+
newEvents.push(events[index])
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
token.type === types.labelImage ||
|
|
123
|
+
token.type === types.labelLink ||
|
|
124
|
+
token.type === types.labelEnd
|
|
125
|
+
) {
|
|
126
|
+
// Remove the marker.
|
|
127
|
+
const offset = token.type === types.labelImage ? 4 : 2
|
|
128
|
+
token.type = types.data
|
|
129
|
+
index += offset
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If the events are equal, we don't have to copy newEvents to events
|
|
134
|
+
if (events.length !== newEvents.length) {
|
|
135
|
+
// 简化:直接替换
|
|
136
|
+
events.length = 0
|
|
137
|
+
events.push(...newEvents)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return events
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve to label end.
|
|
145
|
+
* 这是关键函数,负责将 labelEnd 和 reference 关联到 image/link。
|
|
146
|
+
* 需要完整实现,否则 mdast 无法找到 image/link token。
|
|
147
|
+
*/
|
|
148
|
+
function resolveToLabelEnd(events: Event[], context: any): Event[] {
|
|
149
|
+
let index = events.length
|
|
150
|
+
let offset = 0
|
|
151
|
+
/** @type {any} */
|
|
152
|
+
let token: any
|
|
153
|
+
/** @type {number | undefined} */
|
|
154
|
+
let open: number | undefined
|
|
155
|
+
/** @type {number | undefined} */
|
|
156
|
+
let close: number | undefined
|
|
157
|
+
/** @type {Array<Event>} */
|
|
158
|
+
let media: Event[]
|
|
159
|
+
|
|
160
|
+
// Find an opening.
|
|
161
|
+
while (index--) {
|
|
162
|
+
token = events[index][1]
|
|
163
|
+
|
|
164
|
+
if (open !== undefined) {
|
|
165
|
+
// If we see another link, or inactive link label, we've been here before.
|
|
166
|
+
if (
|
|
167
|
+
token.type === types.link ||
|
|
168
|
+
(token.type === types.labelLink && token._inactive)
|
|
169
|
+
) {
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Mark other link openings as inactive, as we can't have links in links.
|
|
174
|
+
if (events[index][0] === 'enter' && token.type === types.labelLink) {
|
|
175
|
+
token._inactive = true
|
|
176
|
+
}
|
|
177
|
+
} else if (close !== undefined) {
|
|
178
|
+
if (
|
|
179
|
+
events[index][0] === 'enter' &&
|
|
180
|
+
(token.type === types.labelImage || token.type === types.labelLink) &&
|
|
181
|
+
!token._balanced
|
|
182
|
+
) {
|
|
183
|
+
open = index
|
|
184
|
+
|
|
185
|
+
if (token.type !== types.labelLink) {
|
|
186
|
+
offset = 2
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if (token.type === types.labelEnd) {
|
|
191
|
+
close = index
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (open === undefined || close === undefined) {
|
|
196
|
+
// 如果没有找到匹配的 open 和 close,直接返回
|
|
197
|
+
return events
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const group = {
|
|
201
|
+
type: events[open][1].type === types.labelLink ? types.link : types.image,
|
|
202
|
+
start: {...events[open][1].start},
|
|
203
|
+
end: {...events[events.length - 1][1].end}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const label = {
|
|
207
|
+
type: types.label,
|
|
208
|
+
start: {...events[open][1].start},
|
|
209
|
+
end: {...events[close][1].end}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const text = {
|
|
213
|
+
type: types.labelText,
|
|
214
|
+
start: {...events[open + offset + 2][1].end},
|
|
215
|
+
end: {...events[close - 2][1].start}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
media = [
|
|
219
|
+
['enter', group, context],
|
|
220
|
+
['enter', label, context]
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
// Opening marker.
|
|
224
|
+
media.push(...events.slice(open + 1, open + offset + 3))
|
|
225
|
+
|
|
226
|
+
// Text open.
|
|
227
|
+
media.push(['enter', text, context])
|
|
228
|
+
|
|
229
|
+
// Between (label text content)
|
|
230
|
+
// 简化:直接使用 events,不调用 resolveAll
|
|
231
|
+
media.push(...events.slice(open + offset + 4, close - 3))
|
|
232
|
+
|
|
233
|
+
// Text close, marker close, label close.
|
|
234
|
+
media.push(
|
|
235
|
+
['exit', text, context],
|
|
236
|
+
events[close - 2],
|
|
237
|
+
events[close - 1],
|
|
238
|
+
['exit', label, context]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
// Reference, resource, or so.
|
|
242
|
+
media.push(...events.slice(close + 1))
|
|
243
|
+
|
|
244
|
+
// Media close.
|
|
245
|
+
media.push(['exit', group, context])
|
|
246
|
+
|
|
247
|
+
// 替换 events
|
|
248
|
+
events.splice(open, events.length - open, ...media)
|
|
249
|
+
|
|
250
|
+
return events
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Tokenize label end,支持增量解析
|
|
255
|
+
*
|
|
256
|
+
* 关键修改:
|
|
257
|
+
* 1. 移除了对 parser.defined 的检查
|
|
258
|
+
* 2. 在 after 函数中,总是尝试解析为 reference
|
|
259
|
+
* 3. 在 referenceFullAfter 中,总是返回 ok
|
|
260
|
+
*
|
|
261
|
+
* 注意:这是一个简化实现,主要目的是让 reference 语法总是被解析为 reference token。
|
|
262
|
+
* 完整的实现需要 factoryLabel、factoryDestination 等工具函数,但这些不在公共 npm 包中。
|
|
263
|
+
* 这个简化版本应该能够处理基本的 reference 语法。
|
|
264
|
+
*/
|
|
265
|
+
function tokenizeLabelEnd(
|
|
266
|
+
this: TokenizeContext,
|
|
267
|
+
effects: Parameters<Tokenizer>[0],
|
|
268
|
+
ok: State,
|
|
269
|
+
nok: State
|
|
270
|
+
): State {
|
|
271
|
+
const self = this
|
|
272
|
+
let index = self.events.length
|
|
273
|
+
/** @type {any} */
|
|
274
|
+
let labelStart: any
|
|
275
|
+
|
|
276
|
+
// Find an opening.
|
|
277
|
+
while (index--) {
|
|
278
|
+
if (
|
|
279
|
+
(self.events[index][1].type === types.labelImage ||
|
|
280
|
+
self.events[index][1].type === types.labelLink) &&
|
|
281
|
+
!self.events[index][1]._balanced
|
|
282
|
+
) {
|
|
283
|
+
labelStart = self.events[index][1]
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return start as State
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Start of label end.
|
|
292
|
+
*/
|
|
293
|
+
function start(code: Code): State | void {
|
|
294
|
+
// If there is not an okay opening.
|
|
295
|
+
if (!labelStart) {
|
|
296
|
+
return nok(code)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If the corresponding label (link) start is marked as inactive,
|
|
300
|
+
// it means we'd be wrapping a link, like this:
|
|
301
|
+
//
|
|
302
|
+
// ```markdown
|
|
303
|
+
// > | a [b [c](d) e](f) g.
|
|
304
|
+
// ^
|
|
305
|
+
// ```
|
|
306
|
+
//
|
|
307
|
+
// We can't have that, so it's just balanced brackets.
|
|
308
|
+
if (labelStart._inactive) {
|
|
309
|
+
return labelEndNok(code)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 检测脚注引用:如果标签以 ^ 开头,交给 GFM 脚注扩展处理
|
|
313
|
+
// 注意:这里只检查 labelLink,不检查 labelImage
|
|
314
|
+
// 因为脚注引用是 [^1],不是 ![^1]
|
|
315
|
+
if (labelStart.type === types.labelLink) {
|
|
316
|
+
const labelText = self.sliceSerialize({start: labelStart.end, end: self.now()})
|
|
317
|
+
if (labelText.startsWith('^')) {
|
|
318
|
+
// 这是脚注引用,交给 GFM 脚注扩展处理
|
|
319
|
+
return nok(code)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 关键修改:移除了对 parser.defined 的检查
|
|
324
|
+
// 原始代码会检查:
|
|
325
|
+
// defined = self.parser.defined.includes(
|
|
326
|
+
// normalizeIdentifier(
|
|
327
|
+
// self.sliceSerialize({start: labelStart.end, end: self.now()})
|
|
328
|
+
// )
|
|
329
|
+
// )
|
|
330
|
+
|
|
331
|
+
effects.enter(types.labelEnd)
|
|
332
|
+
effects.enter(types.labelMarker)
|
|
333
|
+
effects.consume(code)
|
|
334
|
+
effects.exit(types.labelMarker)
|
|
335
|
+
effects.exit(types.labelEnd)
|
|
336
|
+
return after as State
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* After `]`.
|
|
341
|
+
*/
|
|
342
|
+
function after(code: Code): State | void {
|
|
343
|
+
// Resource (`[asd](fgh)`)?
|
|
344
|
+
if (code === codes.leftParenthesis) {
|
|
345
|
+
// 对于 resource,保持原始逻辑(总是尝试解析)
|
|
346
|
+
// 注意:resource 不依赖于 definition,所以应该总是能正确解析
|
|
347
|
+
// 如果解析失败,返回 labelEndNok,避免被错误解析为 shortcut reference
|
|
348
|
+
return effects.attempt(
|
|
349
|
+
{
|
|
350
|
+
tokenize: tokenizeResource,
|
|
351
|
+
partial: false
|
|
352
|
+
},
|
|
353
|
+
labelEndOk as State,
|
|
354
|
+
labelEndNok as State // 修复:resource 解析失败时返回 nok
|
|
355
|
+
)(code)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Full (`[asd][fgh]`) or collapsed (`[asd][]`) reference?
|
|
359
|
+
if (code === codes.leftSquareBracket) {
|
|
360
|
+
// 关键修改:总是尝试解析为 reference,不检查 defined
|
|
361
|
+
return effects.attempt(
|
|
362
|
+
{
|
|
363
|
+
tokenize: tokenizeReferenceFull,
|
|
364
|
+
partial: false
|
|
365
|
+
},
|
|
366
|
+
labelEndOk as State,
|
|
367
|
+
referenceNotFull as State // 修改:即使不是 full reference,也尝试 collapsed
|
|
368
|
+
)(code)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Shortcut (`[asd]`) reference?
|
|
372
|
+
// 关键修改:总是返回 ok,让后续处理
|
|
373
|
+
return labelEndOk(code) as State
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* After `]`, at `[`, but not at a full reference.
|
|
378
|
+
*/
|
|
379
|
+
function referenceNotFull(code: Code): State | void {
|
|
380
|
+
return effects.attempt(
|
|
381
|
+
{
|
|
382
|
+
tokenize: tokenizeReferenceCollapsed,
|
|
383
|
+
partial: false
|
|
384
|
+
},
|
|
385
|
+
labelEndOk as State,
|
|
386
|
+
labelEndOk as State // 修改:即使失败也返回 ok
|
|
387
|
+
)(code)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Done, we found something.
|
|
392
|
+
*/
|
|
393
|
+
function labelEndOk(code: Code): State | void {
|
|
394
|
+
return ok(code) as State
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Done, it's nothing.
|
|
399
|
+
*/
|
|
400
|
+
function labelEndNok(code: Code): State | void {
|
|
401
|
+
labelStart._balanced = true
|
|
402
|
+
return nok(code)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 解析 resource 语法:[text](url) 或 [text](url "title")
|
|
408
|
+
*
|
|
409
|
+
* 支持的语法:
|
|
410
|
+
* - [text](url)
|
|
411
|
+
* - [text](url "title")
|
|
412
|
+
* - [text](url 'title')
|
|
413
|
+
* - [text](url (title))
|
|
414
|
+
* - [text](<url with spaces>)
|
|
415
|
+
* - [text](url "title with \"escaped\"")
|
|
416
|
+
*
|
|
417
|
+
* 完整实现:使用官方 factory 函数保证与 CommonMark 标准一致
|
|
418
|
+
*
|
|
419
|
+
* @param effects - Token 生成器
|
|
420
|
+
* @param ok - 成功时的状态函数
|
|
421
|
+
* @param nok - 失败时的状态函数
|
|
422
|
+
* @returns 起始状态函数
|
|
423
|
+
*/
|
|
424
|
+
function tokenizeResource(
|
|
425
|
+
this: TokenizeContext,
|
|
426
|
+
effects: Parameters<Tokenizer>[0],
|
|
427
|
+
ok: State,
|
|
428
|
+
nok: State
|
|
429
|
+
): State {
|
|
430
|
+
return resourceStart
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 在 resource 起始位置,期望 '('
|
|
434
|
+
*
|
|
435
|
+
* ```markdown
|
|
436
|
+
* > | [a](b) c
|
|
437
|
+
* ^
|
|
438
|
+
* ```
|
|
439
|
+
*
|
|
440
|
+
* @param code - 当前字符编码
|
|
441
|
+
*/
|
|
442
|
+
function resourceStart(code: Code): State | undefined {
|
|
443
|
+
if (code !== codes.leftParenthesis) {
|
|
444
|
+
return nok(code)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
effects.enter(types.resource)
|
|
448
|
+
effects.enter(types.resourceMarker)
|
|
449
|
+
effects.consume(code)
|
|
450
|
+
effects.exit(types.resourceMarker)
|
|
451
|
+
return resourceBefore
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 在 '(' 之后,可能有空白符
|
|
456
|
+
*
|
|
457
|
+
* ```markdown
|
|
458
|
+
* > | [a]( b) c
|
|
459
|
+
* ^
|
|
460
|
+
* ```
|
|
461
|
+
*
|
|
462
|
+
* @param code - 当前字符编码
|
|
463
|
+
*/
|
|
464
|
+
function resourceBefore(code: Code): State | undefined {
|
|
465
|
+
return markdownLineEndingOrSpace(code)
|
|
466
|
+
? factoryWhitespace(effects, resourceOpen)(code)
|
|
467
|
+
: resourceOpen(code)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 在空白符之后,期望 destination 或 ')'
|
|
472
|
+
*
|
|
473
|
+
* ```markdown
|
|
474
|
+
* > | [a](b) c
|
|
475
|
+
* ^
|
|
476
|
+
* ```
|
|
477
|
+
*
|
|
478
|
+
* @param code - 当前字符编码
|
|
479
|
+
*/
|
|
480
|
+
function resourceOpen(code: Code): State | undefined {
|
|
481
|
+
// 空 resource: [text]()
|
|
482
|
+
if (code === codes.rightParenthesis) {
|
|
483
|
+
return resourceEnd(code)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 使用官方 factoryDestination 解析 URL
|
|
487
|
+
return factoryDestination(
|
|
488
|
+
effects,
|
|
489
|
+
resourceDestinationAfter,
|
|
490
|
+
resourceDestinationMissing,
|
|
491
|
+
types.resourceDestination,
|
|
492
|
+
types.resourceDestinationLiteral,
|
|
493
|
+
types.resourceDestinationLiteralMarker,
|
|
494
|
+
types.resourceDestinationRaw,
|
|
495
|
+
types.resourceDestinationString,
|
|
496
|
+
constants.linkResourceDestinationBalanceMax
|
|
497
|
+
)(code)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* 在 destination 之后,可能有空白符或 title
|
|
502
|
+
*
|
|
503
|
+
* ```markdown
|
|
504
|
+
* > | [a](b ) c
|
|
505
|
+
* ^
|
|
506
|
+
* ```
|
|
507
|
+
*
|
|
508
|
+
* @param code - 当前字符编码
|
|
509
|
+
*/
|
|
510
|
+
function resourceDestinationAfter(code: Code): State | undefined {
|
|
511
|
+
return markdownLineEndingOrSpace(code)
|
|
512
|
+
? factoryWhitespace(effects, resourceBetween)(code)
|
|
513
|
+
: resourceEnd(code)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Destination 解析失败(格式错误)
|
|
518
|
+
*
|
|
519
|
+
* ```markdown
|
|
520
|
+
* > | [a](<<) b
|
|
521
|
+
* ^
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* @param code - 当前字符编码
|
|
525
|
+
*/
|
|
526
|
+
function resourceDestinationMissing(code: Code): State | undefined {
|
|
527
|
+
return nok(code)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* 在 destination 和空白符之后,可能有 title
|
|
532
|
+
*
|
|
533
|
+
* ```markdown
|
|
534
|
+
* > | [a](b "c") d
|
|
535
|
+
* ^
|
|
536
|
+
* ```
|
|
537
|
+
*
|
|
538
|
+
* @param code - 当前字符编码
|
|
539
|
+
*/
|
|
540
|
+
function resourceBetween(code: Code): State | undefined {
|
|
541
|
+
// 检测 title 起始标记:双引号、单引号或左括号
|
|
542
|
+
if (
|
|
543
|
+
code === codes.quotationMark ||
|
|
544
|
+
code === codes.apostrophe ||
|
|
545
|
+
code === codes.leftParenthesis
|
|
546
|
+
) {
|
|
547
|
+
// 使用官方 factoryTitle 解析 title
|
|
548
|
+
return factoryTitle(
|
|
549
|
+
effects,
|
|
550
|
+
resourceTitleAfter,
|
|
551
|
+
nok,
|
|
552
|
+
types.resourceTitle,
|
|
553
|
+
types.resourceTitleMarker,
|
|
554
|
+
types.resourceTitleString
|
|
555
|
+
)(code)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 没有 title,直接结束
|
|
559
|
+
return resourceEnd(code)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 在 title 之后,可能有空白符
|
|
564
|
+
*
|
|
565
|
+
* ```markdown
|
|
566
|
+
* > | [a](b "c" ) d
|
|
567
|
+
* ^
|
|
568
|
+
* ```
|
|
569
|
+
*
|
|
570
|
+
* @param code - 当前字符编码
|
|
571
|
+
*/
|
|
572
|
+
function resourceTitleAfter(code: Code): State | undefined {
|
|
573
|
+
return markdownLineEndingOrSpace(code)
|
|
574
|
+
? factoryWhitespace(effects, resourceEnd)(code)
|
|
575
|
+
: resourceEnd(code)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 在 resource 结束位置,期望 ')'
|
|
580
|
+
*
|
|
581
|
+
* ```markdown
|
|
582
|
+
* > | [a](b) c
|
|
583
|
+
* ^
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
586
|
+
* @param code - 当前字符编码
|
|
587
|
+
*/
|
|
588
|
+
function resourceEnd(code: Code): State | undefined {
|
|
589
|
+
if (code === codes.rightParenthesis) {
|
|
590
|
+
effects.enter(types.resourceMarker)
|
|
591
|
+
effects.consume(code)
|
|
592
|
+
effects.exit(types.resourceMarker)
|
|
593
|
+
effects.exit(types.resource)
|
|
594
|
+
return ok
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return nok(code)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 解析 full reference:[text][id]
|
|
603
|
+
*
|
|
604
|
+
* 注意:不检查 id 是否已定义(支持增量解析的核心特性)
|
|
605
|
+
*
|
|
606
|
+
* @param effects - Token 生成器
|
|
607
|
+
* @param ok - 成功时的状态函数
|
|
608
|
+
* @param nok - 失败时的状态函数
|
|
609
|
+
* @returns 起始状态函数
|
|
610
|
+
*/
|
|
611
|
+
function tokenizeReferenceFull(
|
|
612
|
+
this: TokenizeContext,
|
|
613
|
+
effects: Parameters<Tokenizer>[0],
|
|
614
|
+
ok: State,
|
|
615
|
+
nok: State
|
|
616
|
+
): State {
|
|
617
|
+
const self = this
|
|
618
|
+
|
|
619
|
+
return referenceFull
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 在 reference 起始位置,期望 '['
|
|
623
|
+
*
|
|
624
|
+
* ```markdown
|
|
625
|
+
* > | [a][b] d
|
|
626
|
+
* ^
|
|
627
|
+
* ```
|
|
628
|
+
*
|
|
629
|
+
* @param code - 当前字符编码
|
|
630
|
+
*/
|
|
631
|
+
function referenceFull(code: Code): State | undefined {
|
|
632
|
+
if (code !== codes.leftSquareBracket) {
|
|
633
|
+
return nok(code)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 使用官方 factoryLabel 解析 [id]
|
|
637
|
+
// 使用 .call() 确保正确的 this 上下文
|
|
638
|
+
return factoryLabel.call(
|
|
639
|
+
self,
|
|
640
|
+
effects,
|
|
641
|
+
referenceFullAfter,
|
|
642
|
+
referenceFullMissing,
|
|
643
|
+
types.reference,
|
|
644
|
+
types.referenceMarker,
|
|
645
|
+
types.referenceString
|
|
646
|
+
)(code)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 在 reference 结束后
|
|
651
|
+
*
|
|
652
|
+
* 🔑 核心特性:总是返回 ok,不检查 parser.defined
|
|
653
|
+
* 这使得增量解析场景下,前向引用能够正常工作
|
|
654
|
+
*
|
|
655
|
+
* ```markdown
|
|
656
|
+
* > | [a][b] d
|
|
657
|
+
* ^
|
|
658
|
+
* ```
|
|
659
|
+
*
|
|
660
|
+
* @param code - 当前字符编码
|
|
661
|
+
*/
|
|
662
|
+
function referenceFullAfter(code: Code): State | undefined {
|
|
663
|
+
// 关键修改:不检查 parser.defined
|
|
664
|
+
//
|
|
665
|
+
// 原始 micromark-core-commonmark 的代码:
|
|
666
|
+
// return self.parser.defined.includes(
|
|
667
|
+
// normalizeIdentifier(
|
|
668
|
+
// self.sliceSerialize(self.events[self.events.length - 1][1]).slice(1, -1)
|
|
669
|
+
// )
|
|
670
|
+
// ) ? ok(code) : nok(code)
|
|
671
|
+
//
|
|
672
|
+
// 修改后:总是返回 ok,延迟验证到渲染层
|
|
673
|
+
return ok(code)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Reference label 格式错误
|
|
678
|
+
*
|
|
679
|
+
* ```markdown
|
|
680
|
+
* > | [a][b d
|
|
681
|
+
* ^
|
|
682
|
+
* ```
|
|
683
|
+
*
|
|
684
|
+
* @param code - 当前字符编码
|
|
685
|
+
*/
|
|
686
|
+
function referenceFullMissing(code: Code): State | undefined {
|
|
687
|
+
return nok(code)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Tokenize collapsed reference (e.g., `[text][]`).
|
|
693
|
+
*/
|
|
694
|
+
function tokenizeReferenceCollapsed(
|
|
695
|
+
this: TokenizeContext,
|
|
696
|
+
effects: Parameters<Tokenizer>[0],
|
|
697
|
+
ok: State,
|
|
698
|
+
nok: State
|
|
699
|
+
): State {
|
|
700
|
+
return referenceCollapsedStart as State
|
|
701
|
+
|
|
702
|
+
function referenceCollapsedStart(code: Code): State | void {
|
|
703
|
+
if (code !== codes.leftSquareBracket) {
|
|
704
|
+
return nok(code)
|
|
705
|
+
}
|
|
706
|
+
effects.enter(types.reference)
|
|
707
|
+
effects.enter(types.referenceMarker)
|
|
708
|
+
effects.consume(code)
|
|
709
|
+
effects.exit(types.referenceMarker)
|
|
710
|
+
return referenceCollapsedOpen as State
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function referenceCollapsedOpen(code: Code): State | void {
|
|
714
|
+
if (code === codes.rightSquareBracket) {
|
|
715
|
+
effects.enter(types.referenceMarker)
|
|
716
|
+
effects.consume(code)
|
|
717
|
+
effects.exit(types.referenceMarker)
|
|
718
|
+
effects.exit(types.reference)
|
|
719
|
+
return ok as State
|
|
720
|
+
}
|
|
721
|
+
return nok(code)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|