safe-mdx 1.4.0 → 1.6.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.
- package/README.md +275 -13
- package/dist/dynamic-esm-component.d.ts +1 -1
- package/dist/dynamic-esm-component.d.ts.map +1 -1
- package/dist/dynamic-esm-component.js +9 -1
- package/dist/dynamic-esm-component.js.map +1 -1
- package/dist/esm-parser.d.ts +1 -1
- package/dist/esm-parser.d.ts.map +1 -1
- package/dist/esm-parser.js +5 -3
- package/dist/esm-parser.js.map +1 -1
- package/dist/esm-parser.test.js +5 -2
- package/dist/esm-parser.test.js.map +1 -1
- package/dist/html/html-and-md.test.js.map +1 -1
- package/dist/html/html-to-mdx-ast.d.ts +1 -1
- package/dist/html/html-to-mdx-ast.js +4 -4
- package/dist/html/html-to-mdx-ast.js.map +1 -1
- package/dist/html/html-to-mdx-ast.test.js +3 -3
- package/dist/html/html-to-mdx-ast.test.js.map +1 -1
- package/dist/parse.d.ts +1 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +5 -1
- package/dist/parse.js.map +1 -1
- package/dist/safe-mdx.bench.js +2 -2
- package/dist/safe-mdx.bench.js.map +1 -1
- package/dist/safe-mdx.d.ts +45 -3
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +62 -36
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +221 -5
- package/dist/safe-mdx.test.js.map +1 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +3 -1
- package/dist/streaming.js.map +1 -1
- package/package.json +30 -7
- package/src/esm-parser.test.ts +6 -3
- package/src/esm-parser.ts +6 -4
- package/src/html/html-and-md.test.ts +2 -2
- package/src/html/html-to-mdx-ast.test.ts +3 -3
- package/src/html/html-to-mdx-ast.ts +4 -4
- package/src/parse.ts +3 -1
- package/src/safe-mdx.bench.tsx +2 -2
- package/src/safe-mdx.test.tsx +251 -11
- package/src/safe-mdx.tsx +109 -36
- package/src/streaming.tsx +2 -1
package/src/safe-mdx.tsx
CHANGED
|
@@ -8,10 +8,10 @@ import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
|
|
|
8
8
|
|
|
9
9
|
import { Fragment, ReactNode } from 'react'
|
|
10
10
|
import { DynamicEsmComponent } from 'safe-mdx/client'
|
|
11
|
-
import { extractComponentInfo, parseEsmImports } from './esm-parser.
|
|
12
|
-
import { resolveModulePath, type EagerModules } from './parse.
|
|
13
|
-
import { htmlToMdxAst } from './html/html-to-mdx-ast.
|
|
14
|
-
import { validHtmlElements, nativeTags } from './html/valid-html-elements.
|
|
11
|
+
import { extractComponentInfo, parseEsmImports } from './esm-parser.ts'
|
|
12
|
+
import { resolveModulePath, type EagerModules } from './parse.ts'
|
|
13
|
+
import { htmlToMdxAst } from './html/html-to-mdx-ast.ts'
|
|
14
|
+
import { validHtmlElements, nativeTags } from './html/valid-html-elements.ts'
|
|
15
15
|
|
|
16
16
|
export type MyRootContent = RootContent | Root
|
|
17
17
|
|
|
@@ -29,7 +29,10 @@ export type RenderNode = (
|
|
|
29
29
|
transform: (node: MyRootContent) => ReactNode,
|
|
30
30
|
) => ReactNode | undefined
|
|
31
31
|
|
|
32
|
+
export type SafeMdxErrorType = 'validation' | 'missing-component' | 'expression' | 'esm-import'
|
|
33
|
+
|
|
32
34
|
export interface SafeMdxError {
|
|
35
|
+
type: SafeMdxErrorType
|
|
33
36
|
message: string
|
|
34
37
|
line?: number
|
|
35
38
|
schemaPath?: string
|
|
@@ -43,6 +46,18 @@ export type CreateElementFunction = (
|
|
|
43
46
|
...children: ReactNode[]
|
|
44
47
|
) => ReactNode
|
|
45
48
|
|
|
49
|
+
export interface EvaluateOptions {
|
|
50
|
+
/** Enable function calls in expressions. Automatically enabled when `scope` is provided. */
|
|
51
|
+
functions?: boolean
|
|
52
|
+
/** Pass `escodegen.generate` to support inline function expressions
|
|
53
|
+
* like arrow functions in `.map(x => x.name)`. Requires `functions: true`. */
|
|
54
|
+
generate?: (ast: any) => string
|
|
55
|
+
/** Force logical operators (`&&`, `||`) to return booleans. */
|
|
56
|
+
booleanLogicalOperators?: boolean
|
|
57
|
+
/** Throw when variables referenced in expressions are undefined. */
|
|
58
|
+
strict?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
47
62
|
components,
|
|
48
63
|
markdown = '',
|
|
@@ -54,6 +69,9 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
54
69
|
addMarkdownLineNumbers = false,
|
|
55
70
|
modules,
|
|
56
71
|
baseUrl,
|
|
72
|
+
onError,
|
|
73
|
+
scope,
|
|
74
|
+
evaluateOptions,
|
|
57
75
|
}: {
|
|
58
76
|
components?: ComponentsMap
|
|
59
77
|
markdown?: string
|
|
@@ -70,6 +88,18 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
70
88
|
/** Directory of the current MDX file, used to resolve relative import
|
|
71
89
|
* sources against `modules` keys. E.g. `'./pages/getting-started/'` */
|
|
72
90
|
baseUrl?: string
|
|
91
|
+
/** Called for each error during rendering (missing components, invalid props, failed expressions).
|
|
92
|
+
* Throw inside this callback to stop rendering on first error. */
|
|
93
|
+
onError?: (error: SafeMdxError) => void
|
|
94
|
+
/** Variables and functions available in MDX expressions.
|
|
95
|
+
* When scope contains functions, function calls in expressions are
|
|
96
|
+
* automatically enabled. */
|
|
97
|
+
scope?: Record<string, any>
|
|
98
|
+
/** Options passed to `eval-estree-expression` for expression evaluation.
|
|
99
|
+
* Pass `{ functions: true }` to enable function calls, or
|
|
100
|
+
* `{ functions: true, generate: escodegen.generate }` to also support
|
|
101
|
+
* inline arrow functions and callbacks like `.map(x => x.name)`. */
|
|
102
|
+
evaluateOptions?: EvaluateOptions
|
|
73
103
|
}) {
|
|
74
104
|
const visitor = new MdastToJsx({
|
|
75
105
|
markdown,
|
|
@@ -82,6 +112,9 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
82
112
|
addMarkdownLineNumbers,
|
|
83
113
|
modules,
|
|
84
114
|
baseUrl,
|
|
115
|
+
onError,
|
|
116
|
+
scope,
|
|
117
|
+
evaluateOptions,
|
|
85
118
|
})
|
|
86
119
|
const result = visitor.run()
|
|
87
120
|
return result
|
|
@@ -101,6 +134,9 @@ export class MdastToJsx {
|
|
|
101
134
|
addMarkdownLineNumbers: boolean
|
|
102
135
|
modules?: EagerModules
|
|
103
136
|
baseUrl?: string
|
|
137
|
+
onError?: (error: SafeMdxError) => void
|
|
138
|
+
scope?: Record<string, any>
|
|
139
|
+
evaluateOptions?: EvaluateOptions
|
|
104
140
|
|
|
105
141
|
constructor({
|
|
106
142
|
markdown: code = '',
|
|
@@ -113,6 +149,9 @@ export class MdastToJsx {
|
|
|
113
149
|
addMarkdownLineNumbers = false,
|
|
114
150
|
modules,
|
|
115
151
|
baseUrl,
|
|
152
|
+
onError,
|
|
153
|
+
scope,
|
|
154
|
+
evaluateOptions,
|
|
116
155
|
}: {
|
|
117
156
|
markdown?: string
|
|
118
157
|
mdast: MyRootContent
|
|
@@ -127,6 +166,18 @@ export class MdastToJsx {
|
|
|
127
166
|
addMarkdownLineNumbers?: boolean
|
|
128
167
|
modules?: EagerModules
|
|
129
168
|
baseUrl?: string
|
|
169
|
+
/** Called for each error during rendering (missing components, invalid props, failed expressions).
|
|
170
|
+
* Throw inside this callback to stop rendering on first error. */
|
|
171
|
+
onError?: (error: SafeMdxError) => void
|
|
172
|
+
/** Variables and functions available in MDX expressions.
|
|
173
|
+
* When scope contains functions, function calls in expressions are
|
|
174
|
+
* automatically enabled. */
|
|
175
|
+
scope?: Record<string, any>
|
|
176
|
+
/** Options passed to `eval-estree-expression` for expression evaluation.
|
|
177
|
+
* Pass `{ functions: true }` to enable function calls, or
|
|
178
|
+
* `{ functions: true, generate: escodegen.generate }` to also support
|
|
179
|
+
* inline arrow functions and callbacks like `.map(x => x.name)`. */
|
|
180
|
+
evaluateOptions?: EvaluateOptions
|
|
130
181
|
}) {
|
|
131
182
|
this.str = code
|
|
132
183
|
|
|
@@ -144,6 +195,9 @@ export class MdastToJsx {
|
|
|
144
195
|
|
|
145
196
|
this.modules = modules
|
|
146
197
|
this.baseUrl = baseUrl
|
|
198
|
+
this.onError = onError
|
|
199
|
+
this.scope = scope
|
|
200
|
+
this.evaluateOptions = evaluateOptions
|
|
147
201
|
|
|
148
202
|
this.c = {
|
|
149
203
|
...Object.fromEntries(
|
|
@@ -156,6 +210,11 @@ export class MdastToJsx {
|
|
|
156
210
|
|
|
157
211
|
}
|
|
158
212
|
|
|
213
|
+
pushError(error: SafeMdxError): void {
|
|
214
|
+
this.errors.push(error)
|
|
215
|
+
this.onError?.(error)
|
|
216
|
+
}
|
|
217
|
+
|
|
159
218
|
/**
|
|
160
219
|
* Resolve import declarations from an mdxjsEsm node against `this.modules`.
|
|
161
220
|
* Resolved components are added directly to `this.c` (the component map)
|
|
@@ -234,7 +293,8 @@ export class MdastToJsx {
|
|
|
234
293
|
if (result.issues) {
|
|
235
294
|
result.issues.forEach((issue) => {
|
|
236
295
|
const propPath = issue.path?.join('.') || 'unknown'
|
|
237
|
-
this.
|
|
296
|
+
this.pushError({
|
|
297
|
+
type: 'validation',
|
|
238
298
|
message: `Invalid props for component "${componentName}" at "${propPath}": ${issue.message}`,
|
|
239
299
|
line,
|
|
240
300
|
schemaPath: issue.path?.join('.'),
|
|
@@ -302,7 +362,7 @@ export class MdastToJsx {
|
|
|
302
362
|
extractComponentInfo(esmImportInfo)
|
|
303
363
|
Component = DynamicEsmComponent
|
|
304
364
|
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
305
|
-
this.
|
|
365
|
+
this.pushError(err)
|
|
306
366
|
})
|
|
307
367
|
let attrs = Object.fromEntries(attrsList)
|
|
308
368
|
|
|
@@ -318,7 +378,8 @@ export class MdastToJsx {
|
|
|
318
378
|
Component = accessWithDot(this.c, node.name)
|
|
319
379
|
|
|
320
380
|
if (!Component) {
|
|
321
|
-
this.
|
|
381
|
+
this.pushError({
|
|
382
|
+
type: 'missing-component',
|
|
322
383
|
message: `Unsupported jsx component ${node.name}`,
|
|
323
384
|
line: node.position?.start?.line,
|
|
324
385
|
})
|
|
@@ -327,7 +388,7 @@ export class MdastToJsx {
|
|
|
327
388
|
}
|
|
328
389
|
|
|
329
390
|
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
330
|
-
this.
|
|
391
|
+
this.pushError(err)
|
|
331
392
|
})
|
|
332
393
|
|
|
333
394
|
let attrs = Object.fromEntries(attrsList)
|
|
@@ -365,6 +426,7 @@ export class MdastToJsx {
|
|
|
365
426
|
: null
|
|
366
427
|
if (!tagName) {
|
|
367
428
|
onError?.({
|
|
429
|
+
type: 'expression',
|
|
368
430
|
message: 'JSX element missing component name',
|
|
369
431
|
line: line,
|
|
370
432
|
})
|
|
@@ -387,6 +449,7 @@ export class MdastToJsx {
|
|
|
387
449
|
Component = accessWithDot(this.c, tagName)
|
|
388
450
|
if (!Component) {
|
|
389
451
|
onError?.({
|
|
452
|
+
type: 'missing-component',
|
|
390
453
|
message: `Unsupported jsx component ${tagName} in attribute`,
|
|
391
454
|
line: line,
|
|
392
455
|
})
|
|
@@ -454,6 +517,7 @@ export class MdastToJsx {
|
|
|
454
517
|
} catch (error) {
|
|
455
518
|
// Return null if transformation fails
|
|
456
519
|
onError?.({
|
|
520
|
+
type: 'expression',
|
|
457
521
|
message: `Failed to transform JSX element: ${
|
|
458
522
|
error instanceof Error ? error.message : 'Unknown error'
|
|
459
523
|
}`,
|
|
@@ -464,6 +528,15 @@ export class MdastToJsx {
|
|
|
464
528
|
return null
|
|
465
529
|
}
|
|
466
530
|
|
|
531
|
+
evaluateExpression(expression: any) {
|
|
532
|
+
const hasScope = this.scope && Object.keys(this.scope).length > 0
|
|
533
|
+
const context = hasScope ? this.scope : undefined
|
|
534
|
+
const options = hasScope || this.evaluateOptions
|
|
535
|
+
? { ...(hasScope ? { functions: true } : {}), ...this.evaluateOptions }
|
|
536
|
+
: undefined
|
|
537
|
+
return Evaluate.evaluate.sync(expression, context, options)
|
|
538
|
+
}
|
|
539
|
+
|
|
467
540
|
getJsxAttrs(
|
|
468
541
|
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
469
542
|
onError: (err: SafeMdxError) => void = console.error,
|
|
@@ -476,14 +549,15 @@ export class MdastToJsx {
|
|
|
476
549
|
if (attr.data?.estree) {
|
|
477
550
|
try {
|
|
478
551
|
const program = attr.data.estree
|
|
552
|
+
const firstBody = program.body?.[0]
|
|
479
553
|
if (
|
|
480
|
-
|
|
481
|
-
|
|
554
|
+
firstBody &&
|
|
555
|
+
firstBody.type === 'ExpressionStatement'
|
|
482
556
|
) {
|
|
483
|
-
const expression =
|
|
557
|
+
const expression = firstBody.expression
|
|
484
558
|
try {
|
|
485
559
|
const result =
|
|
486
|
-
|
|
560
|
+
this.evaluateExpression(expression)
|
|
487
561
|
|
|
488
562
|
// Handle spread syntax - merge the evaluated object
|
|
489
563
|
if (
|
|
@@ -495,6 +569,7 @@ export class MdastToJsx {
|
|
|
495
569
|
}
|
|
496
570
|
} catch (error) {
|
|
497
571
|
onError({
|
|
572
|
+
type: 'expression',
|
|
498
573
|
message: `Failed to evaluate expression attribute: ${attr.value
|
|
499
574
|
.replace(/\n+/g, ' ')
|
|
500
575
|
.replace(/ +/g, ' ')}. ${
|
|
@@ -508,6 +583,7 @@ export class MdastToJsx {
|
|
|
508
583
|
}
|
|
509
584
|
} catch (error) {
|
|
510
585
|
onError({
|
|
586
|
+
type: 'expression',
|
|
511
587
|
message: `Failed to evaluate expression attribute: ${attr.value
|
|
512
588
|
.replace(/\n+/g, ' ')
|
|
513
589
|
.replace(/ +/g, ' ')}. ${
|
|
@@ -520,6 +596,7 @@ export class MdastToJsx {
|
|
|
520
596
|
}
|
|
521
597
|
} else {
|
|
522
598
|
onError({
|
|
599
|
+
type: 'expression',
|
|
523
600
|
message: `Expressions in jsx props are not supported (${attr.value
|
|
524
601
|
.replace(/\n+/g, ' ')
|
|
525
602
|
.replace(/ +/g, ' ')})`,
|
|
@@ -531,6 +608,7 @@ export class MdastToJsx {
|
|
|
531
608
|
|
|
532
609
|
if (attr.type !== 'mdxJsxAttribute') {
|
|
533
610
|
onError({
|
|
611
|
+
type: 'expression',
|
|
534
612
|
message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
|
|
535
613
|
line: node.position?.start?.line,
|
|
536
614
|
})
|
|
@@ -569,11 +647,12 @@ export class MdastToJsx {
|
|
|
569
647
|
try {
|
|
570
648
|
// Extract the expression from the Program body
|
|
571
649
|
const program = v.data.estree
|
|
650
|
+
const firstBody = program.body?.[0]
|
|
572
651
|
if (
|
|
573
|
-
|
|
574
|
-
|
|
652
|
+
firstBody &&
|
|
653
|
+
firstBody.type === 'ExpressionStatement'
|
|
575
654
|
) {
|
|
576
|
-
const expression =
|
|
655
|
+
const expression = firstBody.expression
|
|
577
656
|
|
|
578
657
|
// Check if this is a JSX element
|
|
579
658
|
if (expression.type === 'JSXElement') {
|
|
@@ -592,11 +671,12 @@ export class MdastToJsx {
|
|
|
592
671
|
try {
|
|
593
672
|
// Evaluate the expression synchronously
|
|
594
673
|
const result =
|
|
595
|
-
|
|
674
|
+
this.evaluateExpression(expression)
|
|
596
675
|
attrsList.push([attr.name, result])
|
|
597
676
|
continue
|
|
598
677
|
} catch (error) {
|
|
599
678
|
onError({
|
|
679
|
+
type: 'expression',
|
|
600
680
|
message: `Failed to evaluate expression attribute: ${
|
|
601
681
|
attr.name
|
|
602
682
|
}={${v.value}}. ${
|
|
@@ -614,6 +694,7 @@ export class MdastToJsx {
|
|
|
614
694
|
}
|
|
615
695
|
|
|
616
696
|
onError({
|
|
697
|
+
type: 'expression',
|
|
617
698
|
message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
|
|
618
699
|
line: attr.position?.start?.line,
|
|
619
700
|
})
|
|
@@ -623,7 +704,7 @@ export class MdastToJsx {
|
|
|
623
704
|
}
|
|
624
705
|
|
|
625
706
|
run() {
|
|
626
|
-
const res = this.mdastTransformer(this.mdast, 'root')
|
|
707
|
+
const res = this.mdastTransformer(this.mdast, 'root')
|
|
627
708
|
if (Array.isArray(res) && res.length === 1) {
|
|
628
709
|
return res[0]
|
|
629
710
|
}
|
|
@@ -639,7 +720,7 @@ export class MdastToJsx {
|
|
|
639
720
|
if (this.renderNode) {
|
|
640
721
|
const customResult = this.renderNode(
|
|
641
722
|
node,
|
|
642
|
-
(n
|
|
723
|
+
(n) => this.mdastTransformer(n, node.type),
|
|
643
724
|
)
|
|
644
725
|
if (customResult !== undefined) {
|
|
645
726
|
return customResult
|
|
@@ -655,7 +736,7 @@ export class MdastToJsx {
|
|
|
655
736
|
// Parse ESM imports for client-side dynamic loading (only if allowed)
|
|
656
737
|
if (this.allowClientEsmImports) {
|
|
657
738
|
const parsedImports = parseEsmImports(node, (err) =>
|
|
658
|
-
this.
|
|
739
|
+
this.pushError(err),
|
|
659
740
|
)
|
|
660
741
|
parsedImports.forEach((value, key) => {
|
|
661
742
|
this.esmImports.set(key, value)
|
|
@@ -693,18 +774,20 @@ export class MdastToJsx {
|
|
|
693
774
|
try {
|
|
694
775
|
// Extract the expression from the Program body
|
|
695
776
|
const program = node.data.estree
|
|
777
|
+
const firstBody = program.body?.[0]
|
|
696
778
|
if (
|
|
697
|
-
|
|
698
|
-
|
|
779
|
+
firstBody &&
|
|
780
|
+
firstBody.type === 'ExpressionStatement'
|
|
699
781
|
) {
|
|
700
|
-
const expression =
|
|
782
|
+
const expression = firstBody.expression
|
|
701
783
|
try {
|
|
702
784
|
// Evaluate the expression synchronously
|
|
703
785
|
const result =
|
|
704
|
-
|
|
786
|
+
this.evaluateExpression(expression)
|
|
705
787
|
return result
|
|
706
788
|
} catch (error) {
|
|
707
|
-
this.
|
|
789
|
+
this.pushError({
|
|
790
|
+
type: 'expression',
|
|
708
791
|
message: `Failed to evaluate expression: ${
|
|
709
792
|
node.value
|
|
710
793
|
}. ${
|
|
@@ -717,7 +800,8 @@ export class MdastToJsx {
|
|
|
717
800
|
}
|
|
718
801
|
}
|
|
719
802
|
} catch (error) {
|
|
720
|
-
this.
|
|
803
|
+
this.pushError({
|
|
804
|
+
type: 'expression',
|
|
721
805
|
message: `Failed to evaluate expression: ${
|
|
722
806
|
node.value
|
|
723
807
|
}. ${
|
|
@@ -1028,9 +1112,6 @@ export class MdastToJsx {
|
|
|
1028
1112
|
}
|
|
1029
1113
|
}
|
|
1030
1114
|
|
|
1031
|
-
function isTruthy<T>(val: T | undefined | null | false): val is T {
|
|
1032
|
-
return Boolean(val)
|
|
1033
|
-
}
|
|
1034
1115
|
|
|
1035
1116
|
function accessWithDot(obj, path: string) {
|
|
1036
1117
|
return path
|
|
@@ -1061,14 +1142,6 @@ export function mdastBfs(
|
|
|
1061
1142
|
return result
|
|
1062
1143
|
}
|
|
1063
1144
|
|
|
1064
|
-
function safeJsonParse(str: string) {
|
|
1065
|
-
try {
|
|
1066
|
-
return JSON.parse(str)
|
|
1067
|
-
} catch (err) {
|
|
1068
|
-
return null
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
1145
|
type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any } & {
|
|
1073
1146
|
[key: string]: any
|
|
1074
1147
|
}
|
package/src/streaming.tsx
CHANGED
|
@@ -13,6 +13,7 @@ function matchJsxTag(code: string) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const [fullMatch, tagName, attributes, selfClosing] = match
|
|
16
|
+
if (!tagName) return null
|
|
16
17
|
|
|
17
18
|
const type = selfClosing
|
|
18
19
|
? 'self-closing'
|
|
@@ -24,7 +25,7 @@ function matchJsxTag(code: string) {
|
|
|
24
25
|
tag: fullMatch,
|
|
25
26
|
tagName,
|
|
26
27
|
type,
|
|
27
|
-
attributes: attributes.trim(),
|
|
28
|
+
attributes: (attributes ?? '').trim(),
|
|
28
29
|
startIndex: match.index,
|
|
29
30
|
endIndex: match.index + fullMatch.length,
|
|
30
31
|
}
|