@incremark/core 0.1.1 → 0.2.0

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.
@@ -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
+