schema-dsl 2.0.0 → 2.0.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/CHANGELOG.md +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StringExtensions — opt-in String.prototype chainable DSL extensions.
|
|
3
|
-
*
|
|
4
|
-
* v2 fixes:
|
|
5
|
-
* S-01/S-02: array-driven symmetric install/uninstall (v1 uninstall was missing `format` and
|
|
6
|
-
* `phoneNumber`). All method names are now maintained in the EXTENSION_METHODS
|
|
7
|
-
* array so both operations are guaranteed to be in sync.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* import { installStringExtensions } from 'schema-dsl'
|
|
11
|
-
* installStringExtensions(dsl)
|
|
12
|
-
* // Then you can use:
|
|
13
|
-
* 'email!'.label('Email address').messages({ format: 'Invalid format' })
|
|
14
|
-
* 'string:3-32!'.username('medium')
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { DslBuilder } from './DslBuilder.js'
|
|
18
|
-
|
|
19
|
-
// S-01/S-02 fix: all extension method names are managed here so install/uninstall stay symmetric
|
|
20
|
-
const EXTENSION_METHODS = [
|
|
21
|
-
'pattern',
|
|
22
|
-
'label',
|
|
23
|
-
'messages',
|
|
24
|
-
'error',
|
|
25
|
-
'description',
|
|
26
|
-
'format',
|
|
27
|
-
'custom',
|
|
28
|
-
'default',
|
|
29
|
-
'toSchema',
|
|
30
|
-
'toJsonSchema',
|
|
31
|
-
'username',
|
|
32
|
-
'password',
|
|
33
|
-
'phone',
|
|
34
|
-
'phoneNumber',
|
|
35
|
-
'idCard',
|
|
36
|
-
'creditCard',
|
|
37
|
-
'licensePlate',
|
|
38
|
-
'postalCode',
|
|
39
|
-
'passport',
|
|
40
|
-
'slug',
|
|
41
|
-
'domain',
|
|
42
|
-
'ip',
|
|
43
|
-
'base64',
|
|
44
|
-
'jwt',
|
|
45
|
-
'dateGreater',
|
|
46
|
-
'dateLess',
|
|
47
|
-
'after',
|
|
48
|
-
'before',
|
|
49
|
-
'dateFormat',
|
|
50
|
-
'min',
|
|
51
|
-
'max',
|
|
52
|
-
'alphanum',
|
|
53
|
-
'lowercase',
|
|
54
|
-
'uppercase',
|
|
55
|
-
'json',
|
|
56
|
-
'precision',
|
|
57
|
-
'multiple',
|
|
58
|
-
'port',
|
|
59
|
-
'requireAll',
|
|
60
|
-
'strict',
|
|
61
|
-
'noSparse',
|
|
62
|
-
'includesRequired',
|
|
63
|
-
'required',
|
|
64
|
-
'optional',
|
|
65
|
-
'enum',
|
|
66
|
-
'_dslExtensionsInstalled',
|
|
67
|
-
] as const
|
|
68
|
-
|
|
69
|
-
type DslFn = (dslStr: string) => DslBuilder
|
|
70
|
-
|
|
71
|
-
// Track which dslFunction the extensions were installed with
|
|
72
|
-
let _installedDslFn: DslFn | null = null
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Install String.prototype extensions.
|
|
76
|
-
* @param dslFunction - dsl() function (converts a string to a DslBuilder instance)
|
|
77
|
-
*/
|
|
78
|
-
export function installStringExtensions(dslFunction: DslFn): void {
|
|
79
|
-
// Idempotent: skip only if installed with the same dslFunction reference
|
|
80
|
-
if (_installedDslFn === dslFunction) return
|
|
81
|
-
// If installed with a different function, uninstall first then reinstall
|
|
82
|
-
if (_installedDslFn !== null) {
|
|
83
|
-
uninstallStringExtensions()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const proto = String.prototype as unknown as Record<string, unknown>
|
|
87
|
-
|
|
88
|
-
function extend(name: string, fn: (...args: unknown[]) => unknown): void {
|
|
89
|
-
proto[name] = fn
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Proxy all listed methods transparently to the DslBuilder instance
|
|
93
|
-
const delegatedMethods: string[] = [
|
|
94
|
-
'pattern', 'label', 'messages', 'error', 'description', 'format', 'custom',
|
|
95
|
-
'default', 'username', 'password', 'phone', 'phoneNumber', 'idCard', 'creditCard',
|
|
96
|
-
'licensePlate', 'postalCode', 'passport', 'slug', 'domain', 'ip', 'base64', 'jwt',
|
|
97
|
-
'dateGreater', 'dateLess', 'after', 'before', 'dateFormat',
|
|
98
|
-
'min', 'max', 'alphanum', 'lowercase', 'uppercase', 'json',
|
|
99
|
-
'precision', 'multiple', 'port', 'requireAll', 'strict',
|
|
100
|
-
'noSparse', 'includesRequired', 'required', 'optional',
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
for (const method of delegatedMethods) {
|
|
104
|
-
extend(method, function (this: string, ...args: unknown[]): DslBuilder {
|
|
105
|
-
const builder = dslFunction(String(this))
|
|
106
|
-
return (builder as unknown as Record<string, (...args: unknown[]) => DslBuilder>)[method](...args)
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// enum method accepts rest parameters
|
|
111
|
-
extend('enum', function (this: string, ...values: unknown[]): DslBuilder {
|
|
112
|
-
return dslFunction(String(this)).enum(...values)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
// toSchema / toJsonSchema return JSONSchema (not DslBuilder)
|
|
116
|
-
extend('toSchema', function (this: string) {
|
|
117
|
-
return dslFunction(String(this)).toSchema()
|
|
118
|
-
})
|
|
119
|
-
extend('toJsonSchema', function (this: string) {
|
|
120
|
-
return dslFunction(String(this)).toJsonSchema()
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
// Installation marker (v1 BC)
|
|
124
|
-
proto['_dslExtensionsInstalled'] = true
|
|
125
|
-
_installedDslFn = dslFunction
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Uninstall String.prototype extensions (useful for tests or clean-up).
|
|
130
|
-
* S-01/S-02 fix: uses EXTENSION_METHODS array to guarantee perfect symmetry with install.
|
|
131
|
-
*/
|
|
132
|
-
export function uninstallStringExtensions(): void {
|
|
133
|
-
if (!(String.prototype as unknown as Record<string, unknown>)['_dslExtensionsInstalled']) return
|
|
134
|
-
|
|
135
|
-
const proto = String.prototype as unknown as Record<string, unknown>
|
|
136
|
-
for (const method of EXTENSION_METHODS) {
|
|
137
|
-
delete proto[method]
|
|
138
|
-
}
|
|
139
|
-
_installedDslFn = null
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* StringExtensions — opt-in String.prototype chainable DSL extensions.
|
|
3
|
+
*
|
|
4
|
+
* v2 fixes:
|
|
5
|
+
* S-01/S-02: array-driven symmetric install/uninstall (v1 uninstall was missing `format` and
|
|
6
|
+
* `phoneNumber`). All method names are now maintained in the EXTENSION_METHODS
|
|
7
|
+
* array so both operations are guaranteed to be in sync.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { installStringExtensions } from 'schema-dsl'
|
|
11
|
+
* installStringExtensions(dsl)
|
|
12
|
+
* // Then you can use:
|
|
13
|
+
* 'email!'.label('Email address').messages({ format: 'Invalid format' })
|
|
14
|
+
* 'string:3-32!'.username('medium')
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { DslBuilder } from './DslBuilder.js'
|
|
18
|
+
|
|
19
|
+
// S-01/S-02 fix: all extension method names are managed here so install/uninstall stay symmetric
|
|
20
|
+
const EXTENSION_METHODS = [
|
|
21
|
+
'pattern',
|
|
22
|
+
'label',
|
|
23
|
+
'messages',
|
|
24
|
+
'error',
|
|
25
|
+
'description',
|
|
26
|
+
'format',
|
|
27
|
+
'custom',
|
|
28
|
+
'default',
|
|
29
|
+
'toSchema',
|
|
30
|
+
'toJsonSchema',
|
|
31
|
+
'username',
|
|
32
|
+
'password',
|
|
33
|
+
'phone',
|
|
34
|
+
'phoneNumber',
|
|
35
|
+
'idCard',
|
|
36
|
+
'creditCard',
|
|
37
|
+
'licensePlate',
|
|
38
|
+
'postalCode',
|
|
39
|
+
'passport',
|
|
40
|
+
'slug',
|
|
41
|
+
'domain',
|
|
42
|
+
'ip',
|
|
43
|
+
'base64',
|
|
44
|
+
'jwt',
|
|
45
|
+
'dateGreater',
|
|
46
|
+
'dateLess',
|
|
47
|
+
'after',
|
|
48
|
+
'before',
|
|
49
|
+
'dateFormat',
|
|
50
|
+
'min',
|
|
51
|
+
'max',
|
|
52
|
+
'alphanum',
|
|
53
|
+
'lowercase',
|
|
54
|
+
'uppercase',
|
|
55
|
+
'json',
|
|
56
|
+
'precision',
|
|
57
|
+
'multiple',
|
|
58
|
+
'port',
|
|
59
|
+
'requireAll',
|
|
60
|
+
'strict',
|
|
61
|
+
'noSparse',
|
|
62
|
+
'includesRequired',
|
|
63
|
+
'required',
|
|
64
|
+
'optional',
|
|
65
|
+
'enum',
|
|
66
|
+
'_dslExtensionsInstalled',
|
|
67
|
+
] as const
|
|
68
|
+
|
|
69
|
+
type DslFn = (dslStr: string) => DslBuilder
|
|
70
|
+
|
|
71
|
+
// Track which dslFunction the extensions were installed with
|
|
72
|
+
let _installedDslFn: DslFn | null = null
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Install String.prototype extensions.
|
|
76
|
+
* @param dslFunction - dsl() function (converts a string to a DslBuilder instance)
|
|
77
|
+
*/
|
|
78
|
+
export function installStringExtensions(dslFunction: DslFn): void {
|
|
79
|
+
// Idempotent: skip only if installed with the same dslFunction reference
|
|
80
|
+
if (_installedDslFn === dslFunction) return
|
|
81
|
+
// If installed with a different function, uninstall first then reinstall
|
|
82
|
+
if (_installedDslFn !== null) {
|
|
83
|
+
uninstallStringExtensions()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const proto = String.prototype as unknown as Record<string, unknown>
|
|
87
|
+
|
|
88
|
+
function extend(name: string, fn: (...args: unknown[]) => unknown): void {
|
|
89
|
+
proto[name] = fn
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Proxy all listed methods transparently to the DslBuilder instance
|
|
93
|
+
const delegatedMethods: string[] = [
|
|
94
|
+
'pattern', 'label', 'messages', 'error', 'description', 'format', 'custom',
|
|
95
|
+
'default', 'username', 'password', 'phone', 'phoneNumber', 'idCard', 'creditCard',
|
|
96
|
+
'licensePlate', 'postalCode', 'passport', 'slug', 'domain', 'ip', 'base64', 'jwt',
|
|
97
|
+
'dateGreater', 'dateLess', 'after', 'before', 'dateFormat',
|
|
98
|
+
'min', 'max', 'alphanum', 'lowercase', 'uppercase', 'json',
|
|
99
|
+
'precision', 'multiple', 'port', 'requireAll', 'strict',
|
|
100
|
+
'noSparse', 'includesRequired', 'required', 'optional',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for (const method of delegatedMethods) {
|
|
104
|
+
extend(method, function (this: string, ...args: unknown[]): DslBuilder {
|
|
105
|
+
const builder = dslFunction(String(this))
|
|
106
|
+
return (builder as unknown as Record<string, (...args: unknown[]) => DslBuilder>)[method](...args)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// enum method accepts rest parameters
|
|
111
|
+
extend('enum', function (this: string, ...values: unknown[]): DslBuilder {
|
|
112
|
+
return dslFunction(String(this)).enum(...values)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// toSchema / toJsonSchema return JSONSchema (not DslBuilder)
|
|
116
|
+
extend('toSchema', function (this: string) {
|
|
117
|
+
return dslFunction(String(this)).toSchema()
|
|
118
|
+
})
|
|
119
|
+
extend('toJsonSchema', function (this: string) {
|
|
120
|
+
return dslFunction(String(this)).toJsonSchema()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Installation marker (v1 BC)
|
|
124
|
+
proto['_dslExtensionsInstalled'] = true
|
|
125
|
+
_installedDslFn = dslFunction
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Uninstall String.prototype extensions (useful for tests or clean-up).
|
|
130
|
+
* S-01/S-02 fix: uses EXTENSION_METHODS array to guarantee perfect symmetry with install.
|
|
131
|
+
*/
|
|
132
|
+
export function uninstallStringExtensions(): void {
|
|
133
|
+
if (!(String.prototype as unknown as Record<string, unknown>)['_dslExtensionsInstalled']) return
|
|
134
|
+
|
|
135
|
+
const proto = String.prototype as unknown as Record<string, unknown>
|
|
136
|
+
for (const method of EXTENSION_METHODS) {
|
|
137
|
+
delete proto[method]
|
|
138
|
+
}
|
|
139
|
+
_installedDslFn = null
|
|
140
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified template engine.
|
|
3
|
-
*
|
|
4
|
-
* Merges the two rendering implementations from v1 — MessageTemplate.render() and
|
|
5
|
-
* ErrorFormatter._interpolate() — into a single pipeline.
|
|
6
|
-
* Fixes the CORE-03 template injection vulnerability (single-pass replacement so that
|
|
7
|
-
* substituted values are never expanded a second time).
|
|
8
|
-
*
|
|
9
|
-
* Supports two placeholder formats:
|
|
10
|
-
* {{#key}} ← all v1 locale files use this format (must remain compatible)
|
|
11
|
-
* {key} ← recommended format for v2 message templates
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Render a template string by replacing placeholders with corresponding values from params.
|
|
16
|
-
*
|
|
17
|
-
* @param template - Template string, e.g. "{{#label}} must be at least {{#min}} characters"
|
|
18
|
-
* @param params - Substitution parameter object
|
|
19
|
-
* @returns Rendered string; placeholders with no matching key are kept as-is (aids debugging)
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* renderTemplate('{{#label}} is required', { label: 'Email' })
|
|
23
|
-
* // → 'Email is required'
|
|
24
|
-
*
|
|
25
|
-
* renderTemplate('{field} must be {min}~{max}', { field: 'age', min: 18, max: 65 })
|
|
26
|
-
* // → 'age must be 18~65'
|
|
27
|
-
*/
|
|
28
|
-
export function renderTemplate(template: string, params: Record<string, unknown>): string {
|
|
29
|
-
// Single-pass replace — prevents substituted values from being expanded again (CORE-03 fix)
|
|
30
|
-
// Regex matches both formats: {{#key}} and {key}
|
|
31
|
-
return template.replace(/\{\{#([^}]+)\}\}|\{([^}]+)\}/g, (match, k1: string | undefined, k2: string | undefined) => {
|
|
32
|
-
const key = k1 ?? k2
|
|
33
|
-
if (key !== undefined && key in params) {
|
|
34
|
-
const val = params[key]
|
|
35
|
-
if (val === null) return 'null'
|
|
36
|
-
if (val === undefined) return match
|
|
37
|
-
if (Array.isArray(val)) return val.join(', ')
|
|
38
|
-
if (val instanceof RegExp) return val.toString()
|
|
39
|
-
if (val instanceof Date) return val.toISOString()
|
|
40
|
-
return String(val)
|
|
41
|
-
}
|
|
42
|
-
return match // No matching key — keep placeholder as-is
|
|
43
|
-
})
|
|
44
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Unified template engine.
|
|
3
|
+
*
|
|
4
|
+
* Merges the two rendering implementations from v1 — MessageTemplate.render() and
|
|
5
|
+
* ErrorFormatter._interpolate() — into a single pipeline.
|
|
6
|
+
* Fixes the CORE-03 template injection vulnerability (single-pass replacement so that
|
|
7
|
+
* substituted values are never expanded a second time).
|
|
8
|
+
*
|
|
9
|
+
* Supports two placeholder formats:
|
|
10
|
+
* {{#key}} ← all v1 locale files use this format (must remain compatible)
|
|
11
|
+
* {key} ← recommended format for v2 message templates
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render a template string by replacing placeholders with corresponding values from params.
|
|
16
|
+
*
|
|
17
|
+
* @param template - Template string, e.g. "{{#label}} must be at least {{#min}} characters"
|
|
18
|
+
* @param params - Substitution parameter object
|
|
19
|
+
* @returns Rendered string; placeholders with no matching key are kept as-is (aids debugging)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* renderTemplate('{{#label}} is required', { label: 'Email' })
|
|
23
|
+
* // → 'Email is required'
|
|
24
|
+
*
|
|
25
|
+
* renderTemplate('{field} must be {min}~{max}', { field: 'age', min: 18, max: 65 })
|
|
26
|
+
* // → 'age must be 18~65'
|
|
27
|
+
*/
|
|
28
|
+
export function renderTemplate(template: string, params: Record<string, unknown>): string {
|
|
29
|
+
// Single-pass replace — prevents substituted values from being expanded again (CORE-03 fix)
|
|
30
|
+
// Regex matches both formats: {{#key}} and {key}
|
|
31
|
+
return template.replace(/\{\{#([^}]+)\}\}|\{([^}]+)\}/g, (match, k1: string | undefined, k2: string | undefined) => {
|
|
32
|
+
const key = k1 ?? k2
|
|
33
|
+
if (key !== undefined && key in params) {
|
|
34
|
+
const val = params[key]
|
|
35
|
+
if (val === null) return 'null'
|
|
36
|
+
if (val === undefined) return match
|
|
37
|
+
if (Array.isArray(val)) return val.join(', ')
|
|
38
|
+
if (val instanceof RegExp) return val.toString()
|
|
39
|
+
if (val instanceof Date) return val.toISOString()
|
|
40
|
+
return String(val)
|
|
41
|
+
}
|
|
42
|
+
return match // No matching key — keep placeholder as-is
|
|
43
|
+
})
|
|
44
|
+
}
|