@zipbul/baker 2.2.0 → 3.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +132 -69
  3. package/dist/index.d.ts +7 -6
  4. package/dist/index.js +10 -321
  5. package/dist/src/collect.d.ts +13 -10
  6. package/dist/src/collect.js +26 -0
  7. package/dist/src/configure.d.ts +8 -6
  8. package/dist/src/configure.js +43 -0
  9. package/dist/src/create-rule.js +41 -0
  10. package/dist/src/decorators/field.d.ts +22 -18
  11. package/dist/src/decorators/field.js +268 -0
  12. package/dist/src/decorators/index.d.ts +1 -0
  13. package/dist/src/decorators/index.js +2 -2
  14. package/dist/src/decorators/recipe.d.ts +17 -0
  15. package/dist/src/decorators/recipe.js +23 -0
  16. package/dist/src/errors.d.ts +27 -17
  17. package/dist/src/errors.js +52 -0
  18. package/dist/src/functions/check-call-options.d.ts +8 -0
  19. package/dist/src/functions/check-call-options.js +51 -0
  20. package/dist/src/functions/deserialize.d.ts +13 -6
  21. package/dist/src/functions/deserialize.js +57 -0
  22. package/dist/src/functions/serialize.d.ts +10 -4
  23. package/dist/src/functions/serialize.js +52 -0
  24. package/dist/src/functions/validate.d.ts +13 -10
  25. package/dist/src/functions/validate.js +49 -0
  26. package/dist/src/interfaces.d.ts +1 -1
  27. package/dist/src/interfaces.js +4 -0
  28. package/dist/src/meta-access.d.ts +19 -0
  29. package/dist/src/meta-access.js +75 -0
  30. package/dist/src/registry.js +8 -0
  31. package/dist/src/rule-metadata.d.ts +11 -0
  32. package/dist/src/rule-metadata.js +17 -0
  33. package/dist/src/rule-plan.d.ts +10 -11
  34. package/dist/src/rule-plan.js +117 -0
  35. package/dist/src/rules/array.d.ts +7 -6
  36. package/dist/src/rules/array.js +96 -0
  37. package/dist/src/rules/common.js +77 -0
  38. package/dist/src/rules/date.js +35 -0
  39. package/dist/src/rules/index.d.ts +2 -4
  40. package/dist/src/rules/index.js +8 -21
  41. package/dist/src/rules/locales.d.ts +5 -4
  42. package/dist/src/rules/locales.js +249 -0
  43. package/dist/src/rules/number.js +79 -0
  44. package/dist/src/rules/object.d.ts +1 -1
  45. package/dist/src/rules/object.js +49 -0
  46. package/dist/src/rules/string.d.ts +83 -80
  47. package/dist/src/rules/string.js +1998 -0
  48. package/dist/src/rules/typechecker.js +143 -0
  49. package/dist/src/seal/circular-analyzer.js +63 -0
  50. package/dist/src/seal/codegen-utils.js +18 -0
  51. package/dist/src/seal/deserialize-builder.d.ts +8 -4
  52. package/dist/src/seal/deserialize-builder.js +1546 -0
  53. package/dist/src/seal/expose-validator.d.ts +3 -2
  54. package/dist/src/seal/expose-validator.js +65 -0
  55. package/dist/src/seal/seal-state.d.ts +10 -0
  56. package/dist/src/seal/seal-state.js +18 -0
  57. package/dist/src/seal/seal.d.ts +22 -21
  58. package/dist/src/seal/seal.js +431 -0
  59. package/dist/src/seal/serialize-builder.d.ts +3 -2
  60. package/dist/src/seal/serialize-builder.js +374 -0
  61. package/dist/src/seal/validate-meta.d.ts +13 -0
  62. package/dist/src/seal/validate-meta.js +61 -0
  63. package/dist/src/symbols.d.ts +1 -1
  64. package/dist/src/symbols.js +13 -2
  65. package/dist/src/transformers/collection.transformer.js +25 -0
  66. package/dist/src/transformers/date.transformer.js +18 -0
  67. package/dist/src/transformers/index.js +6 -2
  68. package/dist/src/transformers/luxon.transformer.d.ts +4 -2
  69. package/dist/src/transformers/luxon.transformer.js +34 -0
  70. package/dist/src/transformers/moment.transformer.d.ts +4 -2
  71. package/dist/src/transformers/moment.transformer.js +32 -0
  72. package/dist/src/transformers/number.transformer.js +8 -0
  73. package/dist/src/transformers/string.transformer.js +12 -0
  74. package/dist/src/types.d.ts +27 -25
  75. package/dist/src/types.js +1 -0
  76. package/dist/src/utils.d.ts +2 -2
  77. package/dist/src/utils.js +10 -0
  78. package/package.json +80 -68
  79. package/dist/index-03cysbck.js +0 -3
  80. package/dist/index-dcbd798a.js +0 -3
  81. package/dist/index-jp2yjd6g.js +0 -3
  82. package/dist/index-mw7met6r.js +0 -3
  83. package/dist/index-xdn55cz3.js +0 -1
  84. package/dist/src/functions/_run-sealed.d.ts +0 -7
  85. package/dist/src/functions/index.d.ts +0 -3
  86. package/dist/src/seal/index.d.ts +0 -5
@@ -0,0 +1,1998 @@
1
+ import { makePlannedRule, makeRule, planCompare, planLength, planOr } from '../rule-plan.js';
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // Helpers
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ function makeStringRule(name, validate, buildEmit, requiresType = 'string', constraints = {}) {
6
+ return makeRule({
7
+ name,
8
+ requiresType,
9
+ constraints,
10
+ validate: value => typeof value === 'string' && validate(value),
11
+ emit: buildEmit,
12
+ });
13
+ }
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // Group A: Length / Range
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ function minLength(min) {
18
+ const plan = { cacheKey: 'length', failure: planCompare(planLength(), '<', min) };
19
+ return makePlannedRule({
20
+ name: 'minLength',
21
+ requiresType: 'string',
22
+ constraints: { min },
23
+ plan,
24
+ validate: value => typeof value === 'string' && value.length >= min,
25
+ });
26
+ }
27
+ function maxLength(max) {
28
+ const plan = { cacheKey: 'length', failure: planCompare(planLength(), '>', max) };
29
+ return makePlannedRule({
30
+ name: 'maxLength',
31
+ requiresType: 'string',
32
+ constraints: { max },
33
+ plan,
34
+ validate: value => typeof value === 'string' && value.length <= max,
35
+ });
36
+ }
37
+ function length(minLen, maxLen) {
38
+ const plan = {
39
+ cacheKey: 'length',
40
+ failure: planOr(planCompare(planLength(), '<', minLen), planCompare(planLength(), '>', maxLen)),
41
+ };
42
+ return makePlannedRule({
43
+ name: 'length',
44
+ requiresType: 'string',
45
+ constraints: { min: minLen, max: maxLen },
46
+ plan,
47
+ validate: value => typeof value === 'string' && value.length >= minLen && value.length <= maxLen,
48
+ });
49
+ }
50
+ function contains(seed) {
51
+ return makeRule({
52
+ name: 'contains',
53
+ requiresType: 'string',
54
+ constraints: { seed },
55
+ validate: value => typeof value === 'string' && value.includes(seed),
56
+ emit: (varName, ctx) => {
57
+ const i = ctx.addRef(seed);
58
+ return `if (!${varName}.includes(refs[${i}])) ${ctx.fail('contains')};`;
59
+ },
60
+ });
61
+ }
62
+ function notContains(seed) {
63
+ return makeRule({
64
+ name: 'notContains',
65
+ requiresType: 'string',
66
+ constraints: { seed },
67
+ validate: value => typeof value === 'string' && !value.includes(seed),
68
+ emit: (varName, ctx) => {
69
+ const i = ctx.addRef(seed);
70
+ return `if (${varName}.includes(refs[${i}])) ${ctx.fail('notContains')};`;
71
+ },
72
+ });
73
+ }
74
+ function matches(pattern, modifiers) {
75
+ const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, modifiers);
76
+ return makeRule({
77
+ name: 'matches',
78
+ requiresType: 'string',
79
+ constraints: { pattern: re.source },
80
+ validate: value => typeof value === 'string' && re.test(value),
81
+ emit: (varName, ctx) => {
82
+ const i = ctx.addRegex(re);
83
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('matches')};`;
84
+ },
85
+ });
86
+ }
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // Group B: Simple Boolean Checks
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ const isLowercase = makeRule({
91
+ name: 'isLowercase',
92
+ requiresType: 'string',
93
+ constraints: {},
94
+ validate: value => typeof value === 'string' && value === value.toLowerCase(),
95
+ emit: (varName, ctx) => `if (${varName} !== ${varName}.toLowerCase()) ${ctx.fail('isLowercase')};`,
96
+ });
97
+ const isUppercase = makeRule({
98
+ name: 'isUppercase',
99
+ requiresType: 'string',
100
+ constraints: {},
101
+ validate: value => typeof value === 'string' && value === value.toUpperCase(),
102
+ emit: (varName, ctx) => `if (${varName} !== ${varName}.toUpperCase()) ${ctx.fail('isUppercase')};`,
103
+ });
104
+ // ASCII: all code points in [0x00, 0x7F]
105
+ const ASCII_RE = new RegExp(`^[${String.fromCharCode(0)}-${String.fromCharCode(0x7f)}]*$`);
106
+ const isAscii = makeStringRule('isAscii', v => ASCII_RE.test(v), (varName, ctx) => {
107
+ const i = ctx.addRegex(ASCII_RE);
108
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isAscii')};`;
109
+ });
110
+ // Alpha — [a-zA-Z]+ singleton
111
+ const ALPHA_DEFAULT_RE = /^[a-zA-Z]+$/;
112
+ // length > 0 guard is dead — `+` quantifier requires ≥1 char so the regex returns false on empty.
113
+ const isAlpha = makeStringRule('isAlpha', v => ALPHA_DEFAULT_RE.test(v), (varName, ctx) => {
114
+ const i = ctx.addRegex(ALPHA_DEFAULT_RE);
115
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isAlpha')};`;
116
+ });
117
+ // Alphanumeric — [a-zA-Z0-9]+ singleton (same empty-input rationale as isAlpha)
118
+ const ALNUM_DEFAULT_RE = /^[a-zA-Z0-9]+$/;
119
+ const isAlphanumeric = makeStringRule('isAlphanumeric', v => ALNUM_DEFAULT_RE.test(v), (varName, ctx) => {
120
+ const i = ctx.addRegex(ALNUM_DEFAULT_RE);
121
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isAlphanumeric')};`;
122
+ });
123
+ // HTTP token — RFC 9110 §5.6.2: token = 1*tchar.
124
+ // tchar = "!"/"#"/"$"/"%"/"&"/"'"/"*"/"+"/"-"/"."/"^"/"_"/"`"/"|"/"~" / DIGIT / ALPHA.
125
+ // Used for HTTP method names and header field-names (not field-values). The hyphen is
126
+ // escaped so it stays literal — an unescaped `+-.` would form a range that admits ",".
127
+ const HTTP_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
128
+ const isHttpToken = makeStringRule('isHttpToken', v => HTTP_TOKEN_RE.test(v), (varName, ctx) => {
129
+ const i = ctx.addRegex(HTTP_TOKEN_RE);
130
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isHttpToken')};`;
131
+ });
132
+ // BooleanString: 'true' | 'false' | '1' | '0'
133
+ const isBooleanString = makeRule({
134
+ name: 'isBooleanString',
135
+ requiresType: 'string',
136
+ constraints: {},
137
+ validate: value => value === 'true' || value === 'false' || value === '1' || value === '0',
138
+ emit: (varName, ctx) => `if (${varName} !== 'true' && ${varName} !== 'false' && ${varName} !== '1' && ${varName} !== '0') ${ctx.fail('isBooleanString')};`,
139
+ });
140
+ const NO_SYMBOLS_RE = /^[0-9]+$/;
141
+ // A numeric string: optional sign, integer/decimal/leading-dot form. No whitespace, hex, or
142
+ // exponent — `Number()` coercion accepted all of those (e.g. " ", "0x1A", "1e5"), which is far
143
+ // looser than "is this string a number". Matches validator.js's default isNumeric behavior.
144
+ const NUMERIC_STRING_RE = /^[+-]?(?:[0-9]*\.)?[0-9]+$/;
145
+ function isNumberString(options) {
146
+ const noSymbols = options?.no_symbols ?? false;
147
+ const re = noSymbols ? NO_SYMBOLS_RE : NUMERIC_STRING_RE;
148
+ return makeStringRule('isNumberString', (s) => re.test(s), (varName, ctx) => {
149
+ const i = ctx.addRegex(re);
150
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isNumberString')};`;
151
+ }, 'string', { no_symbols: noSymbols });
152
+ }
153
+ function isDecimal() {
154
+ // Require a digit after the dot — `\d+(?:\.\d*)?` accepted a dangling "5.".
155
+ const decimalRe = /^[-+]?(?:\d+(?:\.\d+)?|\.\d+)$/;
156
+ return makeStringRule('isDecimal', v => decimalRe.test(v), (varName, ctx) => {
157
+ const i = ctx.addRegex(decimalRe);
158
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isDecimal')};`;
159
+ });
160
+ }
161
+ // Full-width characters (Unicode fullwidth forms)
162
+ const FULLWIDTH_RE = /[^\u0020-\u007E\uFF61-\uFF9F]/;
163
+ // Empty-string guard is redundant — non-anchored char-class regex returns false on empty input.
164
+ const isFullWidth = makeStringRule('isFullWidth', v => FULLWIDTH_RE.test(v), (varName, ctx) => {
165
+ const i = ctx.addRegex(FULLWIDTH_RE);
166
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isFullWidth')};`;
167
+ });
168
+ // Half-width characters
169
+ const HALFWIDTH_RE = /[\u0020-\u007E\uFF61-\uFF9F]/;
170
+ const isHalfWidth = makeStringRule('isHalfWidth', v => HALFWIDTH_RE.test(v), (varName, ctx) => {
171
+ const i = ctx.addRegex(HALFWIDTH_RE);
172
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isHalfWidth')};`;
173
+ });
174
+ // Variable-width: must contain both full-width AND half-width
175
+ const isVariableWidth = makeStringRule('isVariableWidth', v => FULLWIDTH_RE.test(v) && HALFWIDTH_RE.test(v), (varName, ctx) => {
176
+ const i1 = ctx.addRegex(FULLWIDTH_RE);
177
+ const i2 = ctx.addRegex(HALFWIDTH_RE);
178
+ return `if (!re[${i1}].test(${varName}) || !re[${i2}].test(${varName})) ${ctx.fail('isVariableWidth')};`;
179
+ });
180
+ // Multibyte: any character outside Latin-1 / half-width range
181
+ const MULTIBYTE_RE = new RegExp(`[^${String.fromCharCode(0)}-${String.fromCharCode(0xff)}]`);
182
+ const isMultibyte = makeStringRule('isMultibyte', v => MULTIBYTE_RE.test(v), (varName, ctx) => {
183
+ const i = ctx.addRegex(MULTIBYTE_RE);
184
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isMultibyte')};`;
185
+ });
186
+ // Surrogate pairs
187
+ const SURROGATE_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
188
+ const isSurrogatePair = makeStringRule('isSurrogatePair', v => SURROGATE_RE.test(v), (varName, ctx) => {
189
+ const i = ctx.addRegex(SURROGATE_RE);
190
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isSurrogatePair')};`;
191
+ });
192
+ // Hexadecimal
193
+ const HEX_RE = /^[0-9a-fA-F]+$/;
194
+ const isHexadecimal = makeStringRule('isHexadecimal', v => HEX_RE.test(v), (varName, ctx) => {
195
+ const i = ctx.addRegex(HEX_RE);
196
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isHexadecimal')};`;
197
+ });
198
+ // Octal
199
+ const OCTAL_RE = /^(0[oO])?[0-7]+$/;
200
+ const isOctal = makeStringRule('isOctal', v => OCTAL_RE.test(v), (varName, ctx) => {
201
+ const i = ctx.addRegex(OCTAL_RE);
202
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isOctal')};`;
203
+ });
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+ // Group C: Regex-based
206
+ // ─────────────────────────────────────────────────────────────────────────────
207
+ // Email — RFC 5322 simplified
208
+ const EMAIL_RE = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
209
+ function isEmail() {
210
+ return makeStringRule('isEmail', v => EMAIL_RE.test(v), (varName, ctx) => {
211
+ const i = ctx.addRegex(EMAIL_RE);
212
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isEmail')};`;
213
+ }, 'string', { format: 'email' });
214
+ }
215
+ const URL_PROTOCOLS_DEFAULT = ['http', 'https', 'ftp'];
216
+ function isURL(options) {
217
+ const protocols = options?.protocols ?? URL_PROTOCOLS_DEFAULT;
218
+ const protocolPattern = protocols.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
219
+ const re = new RegExp(`^(?:${protocolPattern}):\\/\\/(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)(?::(6553[0-5]|655[0-2]\\d|65[0-4]\\d{2}|6[0-4]\\d{3}|[1-5]\\d{4}|[1-9]\\d{0,3}|0))?(?:\\/[^\\s]*)?$`);
220
+ return makeRule({
221
+ name: 'isURL',
222
+ requiresType: 'string',
223
+ constraints: { format: 'uri', protocols },
224
+ validate: value => typeof value === 'string' && re.test(value),
225
+ emit: (varName, ctx) => {
226
+ const i = ctx.addRegex(re);
227
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isURL')};`;
228
+ },
229
+ });
230
+ }
231
+ // UUID
232
+ const UUID_RE = {
233
+ all: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,
234
+ 1: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-1[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
235
+ 2: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-2[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
236
+ 3: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
237
+ 4: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
238
+ 5: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
239
+ };
240
+ function isUUID(version) {
241
+ const re = version != null ? UUID_RE[version] : UUID_RE.all;
242
+ return makeStringRule('isUUID', v => re.test(v), (varName, ctx) => {
243
+ const i = ctx.addRegex(re);
244
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isUUID')};`;
245
+ }, 'string', { format: 'uuid', version });
246
+ }
247
+ // IP
248
+ const IPV4_RE = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/;
249
+ const IPV6_RE = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}$|^(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}$|^(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}$|^::$|^::1$|^::(?:ffff(?::0{1,4})?:)?(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$|^(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
250
+ function isIP(version) {
251
+ return makeRule({
252
+ name: 'isIP',
253
+ requiresType: 'string',
254
+ constraints: { version },
255
+ validate: value => {
256
+ if (typeof value !== 'string') {
257
+ return false;
258
+ }
259
+ if (version === 4) {
260
+ return IPV4_RE.test(value);
261
+ }
262
+ if (version === 6) {
263
+ return IPV6_RE.test(value);
264
+ }
265
+ return IPV4_RE.test(value) || IPV6_RE.test(value);
266
+ },
267
+ emit: (varName, ctx) => {
268
+ if (version === 4) {
269
+ const i = ctx.addRegex(IPV4_RE);
270
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isIP')};`;
271
+ }
272
+ if (version === 6) {
273
+ const i = ctx.addRegex(IPV6_RE);
274
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isIP')};`;
275
+ }
276
+ const i4 = ctx.addRegex(IPV4_RE);
277
+ const i6 = ctx.addRegex(IPV6_RE);
278
+ return `if (!re[${i4}].test(${varName}) && !re[${i6}].test(${varName})) ${ctx.fail('isIP')};`;
279
+ },
280
+ });
281
+ }
282
+ // HexColor: #RGB or #RRGGBB
283
+ const HEX_COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
284
+ const isHexColor = makeStringRule('isHexColor', v => HEX_COLOR_RE.test(v), (varName, ctx) => {
285
+ const i = ctx.addRegex(HEX_COLOR_RE);
286
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isHexColor')};`;
287
+ });
288
+ // RgbColor
289
+ const RGB_RE = /^rgb\(\s*(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\s*\)$/;
290
+ const RGBA_RE = /^rgba\(\s*(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\s*,\s*(0|0?\.\d+|1(\.0+)?)\s*\)$/;
291
+ // Percent forms: rgb(...) must NOT have alpha; rgba(...) MUST have alpha.
292
+ const RGB_PERCENT_NOALPHA_RE = /^rgb\(\s*(\d{1,2}|100)%\s*,\s*(\d{1,2}|100)%\s*,\s*(\d{1,2}|100)%\s*\)$/;
293
+ const RGBA_PERCENT_RE = /^rgba\(\s*(\d{1,2}|100)%\s*,\s*(\d{1,2}|100)%\s*,\s*(\d{1,2}|100)%\s*,\s*(0|0?\.\d+|1(?:\.0+)?)\s*\)$/;
294
+ function isRgbColor(includePercentValues = false) {
295
+ return makeRule({
296
+ name: 'isRgbColor',
297
+ requiresType: 'string',
298
+ constraints: { includePercentValues },
299
+ validate: value => {
300
+ if (typeof value !== 'string') {
301
+ return false;
302
+ }
303
+ if (includePercentValues) {
304
+ return RGB_PERCENT_NOALPHA_RE.test(value) || RGBA_PERCENT_RE.test(value) || RGB_RE.test(value) || RGBA_RE.test(value);
305
+ }
306
+ return RGB_RE.test(value) || RGBA_RE.test(value);
307
+ },
308
+ emit: (varName, ctx) => {
309
+ if (includePercentValues) {
310
+ const ip1 = ctx.addRegex(RGB_PERCENT_NOALPHA_RE);
311
+ const ip2 = ctx.addRegex(RGBA_PERCENT_RE);
312
+ const ip3 = ctx.addRegex(RGB_RE);
313
+ const ip4 = ctx.addRegex(RGBA_RE);
314
+ return `if (!re[${ip1}].test(${varName}) && !re[${ip2}].test(${varName}) && !re[${ip3}].test(${varName}) && !re[${ip4}].test(${varName})) ${ctx.fail('isRgbColor')};`;
315
+ }
316
+ const i1 = ctx.addRegex(RGB_RE);
317
+ const i2 = ctx.addRegex(RGBA_RE);
318
+ return `if (!re[${i1}].test(${varName}) && !re[${i2}].test(${varName})) ${ctx.fail('isRgbColor')};`;
319
+ },
320
+ });
321
+ }
322
+ // HSL: hsl(H, S%, L%) or hsla(H, S%, L%, A)
323
+ // Alpha belongs to hsla() only — `hsla?(...)?` previously let hsl() carry alpha and hsla() omit it.
324
+ const HSL_RE = /^(?:hsl\(\s*(?:360|3[0-5]\d|[12]\d{2}|[1-9]\d|\d)\s*,\s*(?:100|[1-9]\d|\d)%\s*,\s*(?:100|[1-9]\d|\d)%\s*\)|hsla\(\s*(?:360|3[0-5]\d|[12]\d{2}|[1-9]\d|\d)\s*,\s*(?:100|[1-9]\d|\d)%\s*,\s*(?:100|[1-9]\d|\d)%\s*,\s*(?:0|0?\.\d+|1(?:\.0+)?)\s*\))$/;
325
+ const isHSL = makeStringRule('isHSL', v => HSL_RE.test(v), (varName, ctx) => {
326
+ const i = ctx.addRegex(HSL_RE);
327
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isHSL')};`;
328
+ });
329
+ const MAC_COLON_RE = /^[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}$/;
330
+ const MAC_HYPHEN_RE = /^[0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5}$/;
331
+ const MAC_NO_SEP_RE = /^[0-9a-fA-F]{12}$/;
332
+ function isMACAddress(options) {
333
+ return makeRule({
334
+ name: 'isMACAddress',
335
+ requiresType: 'string',
336
+ constraints: { no_separators: options?.no_separators },
337
+ validate: value => {
338
+ if (typeof value !== 'string') {
339
+ return false;
340
+ }
341
+ if (options?.no_separators) {
342
+ return MAC_NO_SEP_RE.test(value);
343
+ }
344
+ return MAC_COLON_RE.test(value) || MAC_HYPHEN_RE.test(value);
345
+ },
346
+ emit: (varName, ctx) => {
347
+ if (options?.no_separators) {
348
+ const i = ctx.addRegex(MAC_NO_SEP_RE);
349
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isMACAddress')};`;
350
+ }
351
+ const i1 = ctx.addRegex(MAC_COLON_RE);
352
+ const i2 = ctx.addRegex(MAC_HYPHEN_RE);
353
+ return `if (!re[${i1}].test(${varName}) && !re[${i2}].test(${varName})) ${ctx.fail('isMACAddress')};`;
354
+ },
355
+ });
356
+ }
357
+ // ISBN
358
+ function validateISBN10(str) {
359
+ const s = str.replace(/[-\s]/g, '');
360
+ if (!/^\d{9}[\dX]$/.test(s)) {
361
+ return false;
362
+ }
363
+ let sum = 0;
364
+ for (let i = 0; i < 9; i++) {
365
+ sum += (10 - i) * (s.charCodeAt(i) - 48);
366
+ }
367
+ const last = s[9] === 'X' ? 10 : s.charCodeAt(9) - 48;
368
+ sum += last;
369
+ return sum % 11 === 0;
370
+ }
371
+ function validateISBN13(str) {
372
+ const s = str.replace(/[-\s]/g, '');
373
+ if (!/^\d{13}$/.test(s)) {
374
+ return false;
375
+ }
376
+ let sum = 0;
377
+ for (let i = 0; i < 12; i++) {
378
+ sum += (s.charCodeAt(i) - 48) * (i % 2 === 0 ? 1 : 3);
379
+ }
380
+ const check = (10 - (sum % 10)) % 10;
381
+ return check === s.charCodeAt(12) - 48;
382
+ }
383
+ function isISBN(version) {
384
+ const validateFn = (value) => {
385
+ if (typeof value !== 'string') {
386
+ return false;
387
+ }
388
+ if (version === 10) {
389
+ return validateISBN10(value);
390
+ }
391
+ if (version === 13) {
392
+ return validateISBN13(value);
393
+ }
394
+ return validateISBN10(value) || validateISBN13(value);
395
+ };
396
+ const emitISBN10 = (v) => `{var s=${v}.replace(/[-\\s]/g,'');` +
397
+ `if(!/^\\d{9}[\\dX]$/.test(s)){%%FAIL%%}` +
398
+ `else{var sm=0;for(var i=0;i<9;i++)sm+=(10-i)*(s.charCodeAt(i)-48);` +
399
+ `var l=s[9]==='X'?10:(s.charCodeAt(9)-48);sm+=l;` +
400
+ `if(sm%11!==0){%%FAIL%%}}}`;
401
+ const emitISBN13 = (v) => `{var s=${v}.replace(/[-\\s]/g,'');` +
402
+ `if(!/^\\d{13}$/.test(s)){%%FAIL%%}` +
403
+ `else{var sm=0;for(var i=0;i<12;i++)sm+=(s.charCodeAt(i)-48)*(i%2===0?1:3);` +
404
+ `var ck=(10-(sm%10))%10;` +
405
+ `if(ck!==(s.charCodeAt(12)-48)){%%FAIL%%}}}`;
406
+ return makeRule({
407
+ name: 'isISBN',
408
+ requiresType: 'string',
409
+ constraints: { version },
410
+ validate: validateFn,
411
+ emit: (varName, ctx) => {
412
+ const fail = ctx.fail('isISBN');
413
+ if (version === 10) {
414
+ return emitISBN10(varName).replace(/%%FAIL%%/g, fail);
415
+ }
416
+ if (version === 13) {
417
+ return emitISBN13(varName).replace(/%%FAIL%%/g, fail);
418
+ }
419
+ const emit10 = emitISBN10(varName).replace(/%%FAIL%%/g, '__isbn_ok=false');
420
+ const emit13 = emitISBN13(varName).replace(/%%FAIL%%/g, '__isbn_ok=false');
421
+ return `{var __isbn_ok=true;${emit10} if(!__isbn_ok){__isbn_ok=true;${emit13}} if(!__isbn_ok)${fail};}`;
422
+ },
423
+ });
424
+ }
425
+ // ISIN — ISO 6166
426
+ const ISIN_RE = /^[A-Z]{2}[A-Z0-9]{9}[0-9]$/;
427
+ function validateISINStr(v) {
428
+ if (!ISIN_RE.test(v)) {
429
+ return false;
430
+ }
431
+ // Luhn mod10 on expanded digits — walk right-to-left, expanding letters as A=10..Z=35 on the fly.
432
+ // No intermediate string/array allocations.
433
+ let sum = 0;
434
+ let alternate = false;
435
+ for (let i = v.length - 1; i >= 0; i--) {
436
+ const code = v.charCodeAt(i);
437
+ if (code <= 57) {
438
+ // ASCII digit '0'..'9'
439
+ let n = code - 48;
440
+ if (alternate) {
441
+ n *= 2;
442
+ if (n > 9) {
443
+ n -= 9;
444
+ }
445
+ }
446
+ sum += n;
447
+ alternate = !alternate;
448
+ }
449
+ else {
450
+ // ASCII letter 'A'..'Z' → two-digit value, ones first when walking right-to-left
451
+ const value = code - 55;
452
+ const ones = value % 10;
453
+ let n = ones;
454
+ if (alternate) {
455
+ n *= 2;
456
+ if (n > 9) {
457
+ n -= 9;
458
+ }
459
+ }
460
+ sum += n;
461
+ alternate = !alternate;
462
+ n = (value - ones) / 10;
463
+ if (alternate) {
464
+ n *= 2;
465
+ if (n > 9) {
466
+ n -= 9;
467
+ }
468
+ }
469
+ sum += n;
470
+ alternate = !alternate;
471
+ }
472
+ }
473
+ return sum % 10 === 0;
474
+ }
475
+ const isISIN = makeStringRule('isISIN', validateISINStr, (varName, ctx) => {
476
+ const i = ctx.addRegex(ISIN_RE);
477
+ return (`if (!re[${i}].test(${varName})) ${ctx.fail('isISIN')};\n` +
478
+ `else { var isSum=0,isAlt=false;\n` +
479
+ `for(var isI=${varName}.length-1;isI>=0;isI--){var isCd=${varName}.charCodeAt(isI);` +
480
+ `if(isCd<=57){var isN=isCd-48;if(isAlt){isN*=2;if(isN>9)isN-=9;}isSum+=isN;isAlt=!isAlt;}` +
481
+ `else{var isVal=isCd-55;var isO=isVal%10;var isN=isO;if(isAlt){isN*=2;if(isN>9)isN-=9;}isSum+=isN;isAlt=!isAlt;` +
482
+ `isN=(isVal-isO)/10;if(isAlt){isN*=2;if(isN>9)isN-=9;}isSum+=isN;isAlt=!isAlt;}}\n` +
483
+ `if(isSum%10!==0)${ctx.fail('isISIN')}; }`);
484
+ });
485
+ // ISO 8601
486
+ const ISO8601_RE = /^\d{4}(?:-\d{2}(?:-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?)?)?$/;
487
+ // Strict ISO8601: requires month/day AND hour/minute/second to be valid values
488
+ function validateISO8601Strict(v) {
489
+ if (!ISO8601_RE.test(v)) {
490
+ return false;
491
+ }
492
+ const m = v.match(/^(\d{4})-(\d{2})(?:-(\d{2}))?/);
493
+ if (!m) {
494
+ return true;
495
+ } // year-only — no month/day to range-check
496
+ const month = Number(m[2]);
497
+ if (month < 1 || month > 12) {
498
+ return false;
499
+ }
500
+ if (m[3] !== undefined) {
501
+ const day = Number(m[3]);
502
+ const maxDay = new Date(Number(m[1]), month, 0).getDate();
503
+ if (day < 1 || day > maxDay) {
504
+ return false;
505
+ }
506
+ }
507
+ // Time component check: hour 0-23, minute 0-59, second 0-60 (leap second).
508
+ const tm = v.match(/T(\d{2}):(\d{2}):(\d{2})/);
509
+ if (!tm) {
510
+ return true;
511
+ }
512
+ const hh = Number(tm[1]);
513
+ const mm = Number(tm[2]);
514
+ const ss = Number(tm[3]);
515
+ return hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59 && ss >= 0 && ss <= 60;
516
+ }
517
+ function isISO8601(options) {
518
+ if (options?.strict) {
519
+ const validateStrict = (v) => typeof v === 'string' && validateISO8601Strict(v);
520
+ return makeRule({
521
+ name: 'isISO8601',
522
+ requiresType: 'string',
523
+ constraints: { format: 'date-time', strict: true },
524
+ validate: validateStrict,
525
+ emit: (varName, ctx) => {
526
+ const i = ctx.addRegex(ISO8601_RE);
527
+ return (`if (!re[${i}].test(${varName})) ${ctx.fail('isISO8601')};\n` +
528
+ `else { var dm=${varName}.match(/^(\\d{4})-(\\d{2})(?:-(\\d{2}))?/);` +
529
+ `if(dm){var mo=Number(dm[2]);` +
530
+ `if(mo<1||mo>12){${ctx.fail('isISO8601')}}` +
531
+ `else if(dm[3]!==undefined){var da=Number(dm[3]),md=new Date(Number(dm[1]),mo,0).getDate();` +
532
+ `if(da<1||da>md){${ctx.fail('isISO8601')}}}}` +
533
+ `var tm=${varName}.match(/T(\\d{2}):(\\d{2}):(\\d{2})/);` +
534
+ `if(tm){var hh=Number(tm[1]),mm=Number(tm[2]),ss=Number(tm[3]);` +
535
+ `if(hh<0||hh>23||mm<0||mm>59||ss<0||ss>60)${ctx.fail('isISO8601')};} }`);
536
+ },
537
+ });
538
+ }
539
+ // non-strict: both validate and emit use same ISO8601_RE
540
+ return makeStringRule('isISO8601', v => ISO8601_RE.test(v), (varName, ctx) => {
541
+ const i = ctx.addRegex(ISO8601_RE);
542
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isISO8601')};`;
543
+ }, 'string', { format: 'date-time', strict: false });
544
+ }
545
+ // ISRC — ISO 3901
546
+ const ISRC_RE = /^[A-Z]{2}-[A-Z0-9]{3}-\d{2}-\d{5}$|^[A-Z]{2}[A-Z0-9]{3}\d{7}$/;
547
+ const isISRC = makeStringRule('isISRC', v => ISRC_RE.test(v), (varName, ctx) => {
548
+ const i = ctx.addRegex(ISRC_RE);
549
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isISRC')};`;
550
+ });
551
+ function validateISSN(value, options) {
552
+ const requireHyphen = options?.requireHyphen !== false;
553
+ const s = requireHyphen ? value : value.replace(/-/g, '');
554
+ // Format with hyphen: NNNN-NNNX, without: NNNNNNXX
555
+ const re = requireHyphen ? /^\d{4}-\d{3}[\dX]$/ : /^\d{7}[\dX]$/;
556
+ if (!re.test(s)) {
557
+ return false;
558
+ }
559
+ const digits = s.replace(/-/g, '');
560
+ let sum = 0;
561
+ for (let i = 0; i < 7; i++) {
562
+ sum += (8 - i) * (digits.charCodeAt(i) - 48);
563
+ }
564
+ const last = digits[7] === 'X' ? 10 : digits.charCodeAt(7) - 48;
565
+ sum += last;
566
+ return sum % 11 === 0;
567
+ }
568
+ function isISSN(options) {
569
+ const requireHyphen = options?.requireHyphen !== false;
570
+ const validateIssn = (value) => typeof value === 'string' && validateISSN(value, options);
571
+ const formatRe = requireHyphen ? /^\d{4}-\d{3}[\dX]$/ : /^\d{7}[\dX]$/;
572
+ return makeRule({
573
+ name: 'isISSN',
574
+ requiresType: 'string',
575
+ constraints: { requireHyphen: options?.requireHyphen },
576
+ validate: validateIssn,
577
+ emit: (varName, ctx) => {
578
+ const ri = ctx.addRegex(formatRe);
579
+ const strip = requireHyphen ? varName : `${varName}.replace(/-/g,'')`;
580
+ return (`{var issn=${strip};` +
581
+ `if(!re[${ri}].test(issn)){${ctx.fail('isISSN')}}` +
582
+ `else{var id=issn.replace(/-/g,''),iss=0;` +
583
+ `for(var ii=0;ii<7;ii++)iss+=(8-ii)*(id.charCodeAt(ii)-48);` +
584
+ `var il=id[7]==='X'?10:(id.charCodeAt(7)-48);iss+=il;` +
585
+ `if(iss%11!==0)${ctx.fail('isISSN')};}}`);
586
+ },
587
+ });
588
+ }
589
+ // JWT — 3-part dot-separated base64url
590
+ const JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
591
+ const isJWT = makeStringRule('isJWT', v => JWT_RE.test(v), (varName, ctx) => {
592
+ const i = ctx.addRegex(JWT_RE);
593
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isJWT')};`;
594
+ });
595
+ // LatLong
596
+ const LAT_LONG_RE = /^[-+]?([1-8]?\d(?:\.\d+)?|90(?:\.0+)?),\s*[-+]?(180(?:\.0+)?|1[0-7]\d(?:\.\d+)?|\d{1,2}(?:\.\d+)?)$/;
597
+ function isLatLong() {
598
+ return makeStringRule('isLatLong', v => LAT_LONG_RE.test(v), (varName, ctx) => {
599
+ const i = ctx.addRegex(LAT_LONG_RE);
600
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isLatLong')};`;
601
+ });
602
+ }
603
+ // Locale — BCP 47 simplified
604
+ const LOCALE_RE = /^[a-zA-Z]{2,3}(?:-[a-zA-Z]{4})?(?:-(?:[a-zA-Z]{2}|\d{3}))?(?:-[a-zA-Z\d]{5,8})*$/;
605
+ const isLocale = makeStringRule('isLocale', v => LOCALE_RE.test(v), (varName, ctx) => {
606
+ const i = ctx.addRegex(LOCALE_RE);
607
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isLocale')};`;
608
+ });
609
+ // DataURI
610
+ const DATA_URI_RE = /^data:([a-zA-Z0-9!#$&\-^_]+\/[a-zA-Z0-9!#$&\-^_]+)(?:;[a-zA-Z0-9-]+=[a-zA-Z0-9-]+)*(?:;base64)?,[\s\S]*$/;
611
+ const isDataURI = makeStringRule('isDataURI', v => DATA_URI_RE.test(v), (varName, ctx) => {
612
+ const i = ctx.addRegex(DATA_URI_RE);
613
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isDataURI')};`;
614
+ });
615
+ function isFQDN(options) {
616
+ const requireTld = options?.require_tld !== false;
617
+ const allowUnderscores = options?.allow_underscores ?? false;
618
+ const allowTrailingDot = options?.allow_trailing_dot ?? false;
619
+ const partRe = allowUnderscores ? /^[a-zA-Z0-9_-]+$/ : /^[a-zA-Z0-9-]+$/;
620
+ const validateFqdn = (value) => {
621
+ if (typeof value !== 'string') {
622
+ return false;
623
+ }
624
+ let str = value;
625
+ if (allowTrailingDot && str.endsWith('.')) {
626
+ str = str.slice(0, -1);
627
+ }
628
+ if (str.length === 0) {
629
+ return false;
630
+ }
631
+ const parts = str.split('.');
632
+ if (requireTld && parts.length < 2) {
633
+ return false;
634
+ }
635
+ if (requireTld) {
636
+ const tld = parts[parts.length - 1];
637
+ if (!tld || tld.length < 2 || !/^[a-zA-Z]{2,}$/.test(tld)) {
638
+ return false;
639
+ }
640
+ }
641
+ return parts.every(part => {
642
+ if (part.length === 0 || part.length > 63) {
643
+ return false;
644
+ }
645
+ if (!partRe.test(part)) {
646
+ return false;
647
+ }
648
+ if (!allowUnderscores && (part.startsWith('-') || part.endsWith('-'))) {
649
+ return false;
650
+ }
651
+ return true;
652
+ });
653
+ };
654
+ return makeRule({
655
+ name: 'isFQDN',
656
+ requiresType: 'string',
657
+ constraints: {
658
+ require_tld: options?.require_tld,
659
+ allow_underscores: options?.allow_underscores,
660
+ allow_trailing_dot: options?.allow_trailing_dot,
661
+ },
662
+ validate: validateFqdn,
663
+ emit: (varName, ctx) => {
664
+ const ri = ctx.addRegex(partRe);
665
+ const tldRi = requireTld ? ctx.addRegex(/^[a-zA-Z]{2,}$/) : -1;
666
+ // Inline for-loop instead of fp.every(function(p){...}) — avoids per-call closure
667
+ // allocation inside the JIT executor.
668
+ const partCheck = `if(p.length===0||p.length>63){fqOk=false;break;}` +
669
+ `if(!re[${ri}].test(p)){fqOk=false;break;}` +
670
+ (allowUnderscores ? '' : `if(p[0]==='-'||p[p.length-1]==='-'){fqOk=false;break;}`);
671
+ const loopBlock = `var fqOk=true;for(var fi=0;fi<fp.length;fi++){var p=fp[fi];${partCheck}}if(!fqOk)${ctx.fail('isFQDN')};`;
672
+ let code = `{var fq=${varName};`;
673
+ if (allowTrailingDot) {
674
+ code += `if(fq.endsWith('.'))fq=fq.slice(0,-1);`;
675
+ }
676
+ code += `if(fq.length===0)${ctx.fail('isFQDN')};`;
677
+ code += `else{var fp=fq.split('.');`;
678
+ if (requireTld) {
679
+ code += `if(fp.length<2)${ctx.fail('isFQDN')};`;
680
+ code += `else{var tld=fp[fp.length-1];`;
681
+ code += `if(!tld||tld.length<2||!re[${tldRi}].test(tld))${ctx.fail('isFQDN')};`;
682
+ code += `else{${loopBlock}}`; // close tld inner else block
683
+ code += '}'; // close tld outer else block
684
+ }
685
+ else {
686
+ code += loopBlock;
687
+ }
688
+ code += '}'; // close split else{
689
+ code += '}'; // close outer {
690
+ return code;
691
+ },
692
+ });
693
+ }
694
+ // Port — 0 to 65535
695
+ const PORT_RE = /^(?:6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{1,3}|\d)$/;
696
+ const isPort = makeStringRule('isPort', v => PORT_RE.test(v), (varName, ctx) => {
697
+ const i = ctx.addRegex(PORT_RE);
698
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isPort')};`;
699
+ });
700
+ // EAN (EAN-8 and EAN-13 with checksum)
701
+ function validateEAN(value) {
702
+ if (!/^\d{8}$/.test(value) && !/^\d{13}$/.test(value)) {
703
+ return false;
704
+ }
705
+ // Walk via charCodeAt — no split/map array allocations
706
+ const len = value.length;
707
+ let sum = 0;
708
+ for (let i = 0; i < len - 1; i++) {
709
+ const d = value.charCodeAt(i) - 48;
710
+ sum += d * (len === 8 ? (i % 2 === 0 ? 3 : 1) : i % 2 === 0 ? 1 : 3);
711
+ }
712
+ const check = (10 - (sum % 10)) % 10;
713
+ return check === value.charCodeAt(len - 1) - 48;
714
+ }
715
+ const isEAN = makeStringRule('isEAN', validateEAN, (varName, ctx) => {
716
+ const re8 = ctx.addRegex(/^\d{8}$/);
717
+ const re13 = ctx.addRegex(/^\d{13}$/);
718
+ return (`{var ev=${varName};` +
719
+ `if(!re[${re8}].test(ev)&&!re[${re13}].test(ev)){${ctx.fail('isEAN')}}` +
720
+ `else{var el=ev.length,es=0;` +
721
+ `for(var ei=0;ei<el-1;ei++){var ed=ev.charCodeAt(ei)-48;es+=ed*(el===8?(ei%2===0?3:1):(ei%2===0?1:3));}` +
722
+ `var ec=(10-(es%10))%10;` +
723
+ `if(ec!==(ev.charCodeAt(el-1)-48))${ctx.fail('isEAN')};}}`);
724
+ });
725
+ // ISO 3166-1 Alpha-2
726
+ const ISO31661A2_CODES = new Set([
727
+ 'AD',
728
+ 'AE',
729
+ 'AF',
730
+ 'AG',
731
+ 'AI',
732
+ 'AL',
733
+ 'AM',
734
+ 'AO',
735
+ 'AQ',
736
+ 'AR',
737
+ 'AS',
738
+ 'AT',
739
+ 'AU',
740
+ 'AW',
741
+ 'AX',
742
+ 'AZ',
743
+ 'BA',
744
+ 'BB',
745
+ 'BD',
746
+ 'BE',
747
+ 'BF',
748
+ 'BG',
749
+ 'BH',
750
+ 'BI',
751
+ 'BJ',
752
+ 'BL',
753
+ 'BM',
754
+ 'BN',
755
+ 'BO',
756
+ 'BQ',
757
+ 'BR',
758
+ 'BS',
759
+ 'BT',
760
+ 'BV',
761
+ 'BW',
762
+ 'BY',
763
+ 'BZ',
764
+ 'CA',
765
+ 'CC',
766
+ 'CD',
767
+ 'CF',
768
+ 'CG',
769
+ 'CH',
770
+ 'CI',
771
+ 'CK',
772
+ 'CL',
773
+ 'CM',
774
+ 'CN',
775
+ 'CO',
776
+ 'CR',
777
+ 'CU',
778
+ 'CV',
779
+ 'CW',
780
+ 'CX',
781
+ 'CY',
782
+ 'CZ',
783
+ 'DE',
784
+ 'DJ',
785
+ 'DK',
786
+ 'DM',
787
+ 'DO',
788
+ 'DZ',
789
+ 'EC',
790
+ 'EE',
791
+ 'EG',
792
+ 'EH',
793
+ 'ER',
794
+ 'ES',
795
+ 'ET',
796
+ 'FI',
797
+ 'FJ',
798
+ 'FK',
799
+ 'FM',
800
+ 'FO',
801
+ 'FR',
802
+ 'GA',
803
+ 'GB',
804
+ 'GD',
805
+ 'GE',
806
+ 'GF',
807
+ 'GG',
808
+ 'GH',
809
+ 'GI',
810
+ 'GL',
811
+ 'GM',
812
+ 'GN',
813
+ 'GP',
814
+ 'GQ',
815
+ 'GR',
816
+ 'GS',
817
+ 'GT',
818
+ 'GU',
819
+ 'GW',
820
+ 'GY',
821
+ 'HK',
822
+ 'HM',
823
+ 'HN',
824
+ 'HR',
825
+ 'HT',
826
+ 'HU',
827
+ 'ID',
828
+ 'IE',
829
+ 'IL',
830
+ 'IM',
831
+ 'IN',
832
+ 'IO',
833
+ 'IQ',
834
+ 'IR',
835
+ 'IS',
836
+ 'IT',
837
+ 'JE',
838
+ 'JM',
839
+ 'JO',
840
+ 'JP',
841
+ 'KE',
842
+ 'KG',
843
+ 'KH',
844
+ 'KI',
845
+ 'KM',
846
+ 'KN',
847
+ 'KP',
848
+ 'KR',
849
+ 'KW',
850
+ 'KY',
851
+ 'KZ',
852
+ 'LA',
853
+ 'LB',
854
+ 'LC',
855
+ 'LI',
856
+ 'LK',
857
+ 'LR',
858
+ 'LS',
859
+ 'LT',
860
+ 'LU',
861
+ 'LV',
862
+ 'LY',
863
+ 'MA',
864
+ 'MC',
865
+ 'MD',
866
+ 'ME',
867
+ 'MF',
868
+ 'MG',
869
+ 'MH',
870
+ 'MK',
871
+ 'ML',
872
+ 'MM',
873
+ 'MN',
874
+ 'MO',
875
+ 'MP',
876
+ 'MQ',
877
+ 'MR',
878
+ 'MS',
879
+ 'MT',
880
+ 'MU',
881
+ 'MV',
882
+ 'MW',
883
+ 'MX',
884
+ 'MY',
885
+ 'MZ',
886
+ 'NA',
887
+ 'NC',
888
+ 'NE',
889
+ 'NF',
890
+ 'NG',
891
+ 'NI',
892
+ 'NL',
893
+ 'NO',
894
+ 'NP',
895
+ 'NR',
896
+ 'NU',
897
+ 'NZ',
898
+ 'OM',
899
+ 'PA',
900
+ 'PE',
901
+ 'PF',
902
+ 'PG',
903
+ 'PH',
904
+ 'PK',
905
+ 'PL',
906
+ 'PM',
907
+ 'PN',
908
+ 'PR',
909
+ 'PS',
910
+ 'PT',
911
+ 'PW',
912
+ 'PY',
913
+ 'QA',
914
+ 'RE',
915
+ 'RO',
916
+ 'RS',
917
+ 'RU',
918
+ 'RW',
919
+ 'SA',
920
+ 'SB',
921
+ 'SC',
922
+ 'SD',
923
+ 'SE',
924
+ 'SG',
925
+ 'SH',
926
+ 'SI',
927
+ 'SJ',
928
+ 'SK',
929
+ 'SL',
930
+ 'SM',
931
+ 'SN',
932
+ 'SO',
933
+ 'SR',
934
+ 'SS',
935
+ 'ST',
936
+ 'SV',
937
+ 'SX',
938
+ 'SY',
939
+ 'SZ',
940
+ 'TC',
941
+ 'TD',
942
+ 'TF',
943
+ 'TG',
944
+ 'TH',
945
+ 'TJ',
946
+ 'TK',
947
+ 'TL',
948
+ 'TM',
949
+ 'TN',
950
+ 'TO',
951
+ 'TR',
952
+ 'TT',
953
+ 'TV',
954
+ 'TW',
955
+ 'TZ',
956
+ 'UA',
957
+ 'UG',
958
+ 'UM',
959
+ 'US',
960
+ 'UY',
961
+ 'UZ',
962
+ 'VA',
963
+ 'VC',
964
+ 'VE',
965
+ 'VG',
966
+ 'VI',
967
+ 'VN',
968
+ 'VU',
969
+ 'WF',
970
+ 'WS',
971
+ 'YE',
972
+ 'YT',
973
+ 'ZA',
974
+ 'ZM',
975
+ 'ZW',
976
+ ]);
977
+ const isISO31661Alpha2 = makeRule({
978
+ name: 'isISO31661Alpha2',
979
+ requiresType: 'string',
980
+ constraints: {},
981
+ validate: value => typeof value === 'string' && ISO31661A2_CODES.has(value.toUpperCase()),
982
+ emit: (varName, ctx) => {
983
+ const i = ctx.addRef(ISO31661A2_CODES);
984
+ return `if (!refs[${i}].has(${varName}.toUpperCase())) ${ctx.fail('isISO31661Alpha2')};`;
985
+ },
986
+ });
987
+ // ISO 3166-1 Alpha-3
988
+ const ISO31661A3_CODES = new Set([
989
+ 'ABW',
990
+ 'AFG',
991
+ 'AGO',
992
+ 'AIA',
993
+ 'ALA',
994
+ 'ALB',
995
+ 'AND',
996
+ 'ANT',
997
+ 'ARE',
998
+ 'ARG',
999
+ 'ARM',
1000
+ 'ASM',
1001
+ 'ATA',
1002
+ 'ATF',
1003
+ 'ATG',
1004
+ 'AUS',
1005
+ 'AUT',
1006
+ 'AZE',
1007
+ 'BDI',
1008
+ 'BEL',
1009
+ 'BEN',
1010
+ 'BES',
1011
+ 'BFA',
1012
+ 'BGD',
1013
+ 'BGR',
1014
+ 'BHR',
1015
+ 'BHS',
1016
+ 'BIH',
1017
+ 'BLM',
1018
+ 'BLR',
1019
+ 'BLZ',
1020
+ 'BMU',
1021
+ 'BOL',
1022
+ 'BRA',
1023
+ 'BRB',
1024
+ 'BRN',
1025
+ 'BTN',
1026
+ 'BVT',
1027
+ 'BWA',
1028
+ 'CAF',
1029
+ 'CAN',
1030
+ 'CCK',
1031
+ 'CHE',
1032
+ 'CHL',
1033
+ 'CHN',
1034
+ 'CIV',
1035
+ 'CMR',
1036
+ 'COD',
1037
+ 'COG',
1038
+ 'COK',
1039
+ 'COL',
1040
+ 'COM',
1041
+ 'CPV',
1042
+ 'CRI',
1043
+ 'CUB',
1044
+ 'CUW',
1045
+ 'CXR',
1046
+ 'CYM',
1047
+ 'CYP',
1048
+ 'CZE',
1049
+ 'DEU',
1050
+ 'DJI',
1051
+ 'DMA',
1052
+ 'DNK',
1053
+ 'DOM',
1054
+ 'DZA',
1055
+ 'ECU',
1056
+ 'EGY',
1057
+ 'ERI',
1058
+ 'ESH',
1059
+ 'ESP',
1060
+ 'EST',
1061
+ 'ETH',
1062
+ 'FIN',
1063
+ 'FJI',
1064
+ 'FLK',
1065
+ 'FRA',
1066
+ 'FRO',
1067
+ 'FSM',
1068
+ 'GAB',
1069
+ 'GBR',
1070
+ 'GEO',
1071
+ 'GGY',
1072
+ 'GHA',
1073
+ 'GIB',
1074
+ 'GIN',
1075
+ 'GLP',
1076
+ 'GMB',
1077
+ 'GNB',
1078
+ 'GNQ',
1079
+ 'GRC',
1080
+ 'GRD',
1081
+ 'GRL',
1082
+ 'GTM',
1083
+ 'GUF',
1084
+ 'GUM',
1085
+ 'GUY',
1086
+ 'HKG',
1087
+ 'HMD',
1088
+ 'HND',
1089
+ 'HRV',
1090
+ 'HTI',
1091
+ 'HUN',
1092
+ 'IDN',
1093
+ 'IMN',
1094
+ 'IND',
1095
+ 'IOT',
1096
+ 'IRL',
1097
+ 'IRN',
1098
+ 'IRQ',
1099
+ 'ISL',
1100
+ 'ISR',
1101
+ 'ITA',
1102
+ 'JAM',
1103
+ 'JEY',
1104
+ 'JOR',
1105
+ 'JPN',
1106
+ 'KAZ',
1107
+ 'KEN',
1108
+ 'KGZ',
1109
+ 'KHM',
1110
+ 'KIR',
1111
+ 'KNA',
1112
+ 'KOR',
1113
+ 'KWT',
1114
+ 'LAO',
1115
+ 'LBN',
1116
+ 'LBR',
1117
+ 'LBY',
1118
+ 'LCA',
1119
+ 'LIE',
1120
+ 'LKA',
1121
+ 'LSO',
1122
+ 'LTU',
1123
+ 'LUX',
1124
+ 'LVA',
1125
+ 'MAC',
1126
+ 'MAF',
1127
+ 'MAR',
1128
+ 'MCO',
1129
+ 'MDA',
1130
+ 'MDG',
1131
+ 'MDV',
1132
+ 'MEX',
1133
+ 'MHL',
1134
+ 'MKD',
1135
+ 'MLI',
1136
+ 'MLT',
1137
+ 'MMR',
1138
+ 'MNE',
1139
+ 'MNG',
1140
+ 'MNP',
1141
+ 'MOZ',
1142
+ 'MRT',
1143
+ 'MSR',
1144
+ 'MTQ',
1145
+ 'MUS',
1146
+ 'MWI',
1147
+ 'MYS',
1148
+ 'MYT',
1149
+ 'NAM',
1150
+ 'NCL',
1151
+ 'NER',
1152
+ 'NFK',
1153
+ 'NGA',
1154
+ 'NIC',
1155
+ 'NIU',
1156
+ 'NLD',
1157
+ 'NOR',
1158
+ 'NPL',
1159
+ 'NRU',
1160
+ 'NZL',
1161
+ 'OMN',
1162
+ 'PAK',
1163
+ 'PAN',
1164
+ 'PCN',
1165
+ 'PER',
1166
+ 'PHL',
1167
+ 'PLW',
1168
+ 'PNG',
1169
+ 'POL',
1170
+ 'PRI',
1171
+ 'PRK',
1172
+ 'PRT',
1173
+ 'PRY',
1174
+ 'PSE',
1175
+ 'PYF',
1176
+ 'QAT',
1177
+ 'REU',
1178
+ 'ROU',
1179
+ 'RUS',
1180
+ 'RWA',
1181
+ 'SAU',
1182
+ 'SDN',
1183
+ 'SEN',
1184
+ 'SGP',
1185
+ 'SGS',
1186
+ 'SHN',
1187
+ 'SJM',
1188
+ 'SLB',
1189
+ 'SLE',
1190
+ 'SLV',
1191
+ 'SMR',
1192
+ 'SOM',
1193
+ 'SPM',
1194
+ 'SRB',
1195
+ 'SSD',
1196
+ 'STP',
1197
+ 'SUR',
1198
+ 'SVK',
1199
+ 'SVN',
1200
+ 'SWE',
1201
+ 'SWZ',
1202
+ 'SXM',
1203
+ 'SYC',
1204
+ 'SYR',
1205
+ 'TCA',
1206
+ 'TCD',
1207
+ 'TGO',
1208
+ 'THA',
1209
+ 'TJK',
1210
+ 'TKL',
1211
+ 'TKM',
1212
+ 'TLS',
1213
+ 'TON',
1214
+ 'TTO',
1215
+ 'TUN',
1216
+ 'TUR',
1217
+ 'TUV',
1218
+ 'TWN',
1219
+ 'TZA',
1220
+ 'UGA',
1221
+ 'UKR',
1222
+ 'UMI',
1223
+ 'URY',
1224
+ 'USA',
1225
+ 'UZB',
1226
+ 'VAT',
1227
+ 'VCT',
1228
+ 'VEN',
1229
+ 'VGB',
1230
+ 'VIR',
1231
+ 'VNM',
1232
+ 'VUT',
1233
+ 'WLF',
1234
+ 'WSM',
1235
+ 'YEM',
1236
+ 'ZAF',
1237
+ 'ZMB',
1238
+ 'ZWE',
1239
+ ]);
1240
+ const isISO31661Alpha3 = makeRule({
1241
+ name: 'isISO31661Alpha3',
1242
+ requiresType: 'string',
1243
+ constraints: {},
1244
+ validate: value => typeof value === 'string' && ISO31661A3_CODES.has(value.toUpperCase()),
1245
+ emit: (varName, ctx) => {
1246
+ const i = ctx.addRef(ISO31661A3_CODES);
1247
+ return `if (!refs[${i}].has(${varName}.toUpperCase())) ${ctx.fail('isISO31661Alpha3')};`;
1248
+ },
1249
+ });
1250
+ // BIC / SWIFT code — case-insensitive via /i flag avoids per-call .toUpperCase() string allocation
1251
+ const BIC_RE = /^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$/i;
1252
+ const isBIC = makeStringRule('isBIC', v => BIC_RE.test(v), (varName, ctx) => {
1253
+ const i = ctx.addRegex(BIC_RE);
1254
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isBIC')};`;
1255
+ });
1256
+ // Firebase Push ID — 20 chars, base64url charset (-0-9A-Za-z_)
1257
+ const FIREBASE_RE = /^[a-zA-Z0-9_-]{20}$/;
1258
+ const isFirebasePushId = makeStringRule('isFirebasePushId', v => FIREBASE_RE.test(v), (varName, ctx) => {
1259
+ const i = ctx.addRegex(FIREBASE_RE);
1260
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isFirebasePushId')};`;
1261
+ });
1262
+ // SemVer — Semantic Versioning 2.0
1263
+ const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
1264
+ const isSemVer = makeStringRule('isSemVer', v => SEMVER_RE.test(v), (varName, ctx) => {
1265
+ const i = ctx.addRegex(SEMVER_RE);
1266
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isSemVer')};`;
1267
+ });
1268
+ // MongoDB ObjectId — 24-char hex
1269
+ const MONGO_ID_RE = /^[0-9a-fA-F]{24}$/;
1270
+ const isMongoId = makeStringRule('isMongoId', v => MONGO_ID_RE.test(v), (varName, ctx) => {
1271
+ const i = ctx.addRegex(MONGO_ID_RE);
1272
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isMongoId')};`;
1273
+ });
1274
+ // JSON
1275
+ const validateJsonString = (value) => {
1276
+ if (typeof value !== 'string') {
1277
+ return false;
1278
+ }
1279
+ try {
1280
+ JSON.parse(value);
1281
+ return true;
1282
+ }
1283
+ catch {
1284
+ return false;
1285
+ }
1286
+ };
1287
+ const isJSON = makeRule({
1288
+ name: 'isJSON',
1289
+ requiresType: 'string',
1290
+ constraints: {},
1291
+ validate: validateJsonString,
1292
+ emit: (varName, ctx) => `try { JSON.parse(${varName}); } catch { ${ctx.fail('isJSON')}; }`,
1293
+ });
1294
+ // Base32
1295
+ const BASE32_RE = /^[A-Z2-7]+=*$/i;
1296
+ // Empty-string fails the `+`-quantified regex anyway, so the explicit length===0 check is dead.
1297
+ function isBase32() {
1298
+ return makeStringRule('isBase32', v => v.length % 8 === 0 && BASE32_RE.test(v), (varName, ctx) => {
1299
+ const i = ctx.addRegex(BASE32_RE);
1300
+ return `if (${varName}.length % 8 !== 0 || !re[${i}].test(${varName})) ${ctx.fail('isBase32')};`;
1301
+ });
1302
+ }
1303
+ // Base58
1304
+ const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]+$/;
1305
+ const isBase58 = makeStringRule('isBase58', v => BASE58_RE.test(v), (varName, ctx) => {
1306
+ const i = ctx.addRegex(BASE58_RE);
1307
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isBase58')};`;
1308
+ });
1309
+ // Base64
1310
+ const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
1311
+ const BASE64_URL_RE = /^[A-Za-z0-9_-]+={0,2}$/;
1312
+ function isBase64(options) {
1313
+ const re = options?.urlSafe ? BASE64_URL_RE : BASE64_RE;
1314
+ // Empty-string check is redundant — both base64 regexes require ≥1 char and fail on empty input.
1315
+ return makeStringRule('isBase64', v => re.test(v), (varName, ctx) => {
1316
+ const i = ctx.addRegex(re);
1317
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isBase64')};`;
1318
+ }, 'string', { urlSafe: options?.urlSafe });
1319
+ }
1320
+ // DateString — ISO 8601 date only (YYYY-MM-DD) with calendar validity (day must exist in month/year).
1321
+ const DATE_STRING_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
1322
+ function isCalendarValidDate(v) {
1323
+ if (!DATE_STRING_RE.test(v)) {
1324
+ return false;
1325
+ }
1326
+ const y = Number(v.slice(0, 4));
1327
+ const m = Number(v.slice(5, 7));
1328
+ const d = Number(v.slice(8, 10));
1329
+ const maxDay = new Date(y, m, 0).getDate();
1330
+ return d >= 1 && d <= maxDay;
1331
+ }
1332
+ function isDateString() {
1333
+ return makeStringRule('isDateString', isCalendarValidDate, (varName, ctx) => {
1334
+ const i = ctx.addRegex(DATE_STRING_RE);
1335
+ return (`if (!re[${i}].test(${varName})) ${ctx.fail('isDateString')};\n` +
1336
+ `else { var y=Number(${varName}.slice(0,4)),m=Number(${varName}.slice(5,7)),d=Number(${varName}.slice(8,10));` +
1337
+ `var md=new Date(y,m,0).getDate(); if(d<1||d>md)${ctx.fail('isDateString')}; }`);
1338
+ });
1339
+ }
1340
+ // MimeType
1341
+ const MIME_TYPE_RE = /^(application|audio|font|image|message|model|multipart|text|video)\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*(?:;.+)?$/;
1342
+ const isMimeType = makeStringRule('isMimeType', v => MIME_TYPE_RE.test(v), (varName, ctx) => {
1343
+ const i = ctx.addRegex(MIME_TYPE_RE);
1344
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isMimeType')};`;
1345
+ });
1346
+ // Currency
1347
+ // A single optional sign, either before the `$` (`-$5`, `+5`) or after it (`$-5`, `$+5`) — never
1348
+ // both. The previous `[-+]?\$?-?` allowed two signs (e.g. `+-5`, `-$-5`).
1349
+ const CURRENCY_RE = /^(?:[-+]?\$?|\$[-+]?)(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d{1,2})?$/;
1350
+ function isCurrency() {
1351
+ // Currency regex requires at least one digit; empty input fails the regex by itself.
1352
+ return makeStringRule('isCurrency', v => CURRENCY_RE.test(v), (varName, ctx) => {
1353
+ const i = ctx.addRegex(CURRENCY_RE);
1354
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isCurrency')};`;
1355
+ });
1356
+ }
1357
+ // Magnet URI
1358
+ const MAGNET_URI_RE = /^magnet:\?xt=urn:[a-z0-9]+:[a-z0-9]{32,40}(?:&[a-z][a-z0-9.]*=[^&\s]*)*$/i;
1359
+ const isMagnetURI = makeStringRule('isMagnetURI', v => MAGNET_URI_RE.test(v), (varName, ctx) => {
1360
+ const i = ctx.addRegex(MAGNET_URI_RE);
1361
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isMagnetURI')};`;
1362
+ });
1363
+ // ─────────────────────────────────────────────────────────────────────────────
1364
+ // Group D: Algorithm-based
1365
+ // ─────────────────────────────────────────────────────────────────────────────
1366
+ // Credit Card — Luhn algorithm (§4.8 C)
1367
+ function luhn(str) {
1368
+ const s = str.replace(/[\s-]/g, '');
1369
+ if (s.length === 0 || !/^\d+$/.test(s)) {
1370
+ return false;
1371
+ }
1372
+ let sum = 0;
1373
+ let alternate = false;
1374
+ for (let i = s.length - 1; i >= 0; i--) {
1375
+ let n = s.charCodeAt(i) - 48;
1376
+ if (alternate) {
1377
+ n *= 2;
1378
+ if (n > 9) {
1379
+ n -= 9;
1380
+ }
1381
+ }
1382
+ sum += n;
1383
+ alternate = !alternate;
1384
+ }
1385
+ return sum % 10 === 0;
1386
+ }
1387
+ const isCreditCard = makeRule({
1388
+ name: 'isCreditCard',
1389
+ requiresType: 'string',
1390
+ constraints: {},
1391
+ validate: value => typeof value === 'string' && luhn(value),
1392
+ emit: (varName, ctx) => `{
1393
+ var cs=${varName}.replace(/[\\s-]/g,'');
1394
+ if(cs.length===0||!/^\\d+$/.test(cs)){${ctx.fail('isCreditCard')}}
1395
+ else{var sum=0,alt=false;
1396
+ for(var ci=cs.length-1;ci>=0;ci--){var cn=cs.charCodeAt(ci)-48;if(alt){cn*=2;if(cn>9)cn-=9;}sum+=cn;alt=!alt;}
1397
+ if(sum%10!==0)${ctx.fail('isCreditCard')};}
1398
+ }`,
1399
+ });
1400
+ const IBAN_COUNTRY_LENGTH = {
1401
+ AD: 24,
1402
+ AE: 23,
1403
+ AL: 28,
1404
+ AT: 20,
1405
+ AZ: 28,
1406
+ BA: 20,
1407
+ BE: 16,
1408
+ BG: 22,
1409
+ BH: 22,
1410
+ BR: 29,
1411
+ CH: 21,
1412
+ CR: 22,
1413
+ CY: 28,
1414
+ CZ: 24,
1415
+ DE: 22,
1416
+ DK: 18,
1417
+ DO: 28,
1418
+ EE: 20,
1419
+ ES: 24,
1420
+ FI: 18,
1421
+ FO: 18,
1422
+ FR: 27,
1423
+ GB: 22,
1424
+ GE: 22,
1425
+ GI: 23,
1426
+ GL: 18,
1427
+ GR: 27,
1428
+ GT: 28,
1429
+ HR: 21,
1430
+ HU: 28,
1431
+ IE: 22,
1432
+ IL: 23,
1433
+ IS: 26,
1434
+ IT: 27,
1435
+ JO: 30,
1436
+ KW: 30,
1437
+ KZ: 20,
1438
+ LB: 28,
1439
+ LC: 32,
1440
+ LI: 21,
1441
+ LT: 20,
1442
+ LU: 20,
1443
+ LV: 21,
1444
+ MC: 27,
1445
+ MD: 24,
1446
+ ME: 22,
1447
+ MK: 19,
1448
+ MR: 27,
1449
+ MT: 31,
1450
+ MU: 30,
1451
+ NL: 18,
1452
+ NO: 15,
1453
+ PK: 24,
1454
+ PL: 28,
1455
+ PS: 29,
1456
+ PT: 25,
1457
+ QA: 29,
1458
+ RO: 24,
1459
+ RS: 22,
1460
+ SA: 24,
1461
+ SC: 31,
1462
+ SE: 24,
1463
+ SI: 19,
1464
+ SK: 24,
1465
+ SM: 27,
1466
+ ST: 25,
1467
+ SV: 28,
1468
+ TL: 23,
1469
+ TN: 24,
1470
+ TR: 26,
1471
+ UA: 29,
1472
+ VA: 22,
1473
+ VG: 24,
1474
+ XK: 20,
1475
+ };
1476
+ function validateIBAN(value, options) {
1477
+ let s = options?.allowSpaces ? value.replace(/\s/g, '') : value;
1478
+ s = s.toUpperCase();
1479
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(s)) {
1480
+ return false;
1481
+ }
1482
+ const country = s.slice(0, 2);
1483
+ const expectedLength = IBAN_COUNTRY_LENGTH[country];
1484
+ if (expectedLength !== undefined && s.length !== expectedLength) {
1485
+ return false;
1486
+ }
1487
+ // Rearrange: move first 4 chars to end
1488
+ const rearranged = s.slice(4) + s.slice(0, 4);
1489
+ // Walk char-by-char accumulating mod 97 — no .replace/closure, no String() coercion,
1490
+ // no parseInt() allocations.
1491
+ let remainder = 0;
1492
+ for (let i = 0; i < rearranged.length; i++) {
1493
+ const code = rearranged.charCodeAt(i);
1494
+ if (code <= 57) {
1495
+ // digit
1496
+ remainder = (remainder * 10 + (code - 48)) % 97;
1497
+ }
1498
+ else {
1499
+ // letter A-Z → two digits (value = code - 55)
1500
+ const value = code - 55;
1501
+ remainder = (remainder * 100 + value) % 97;
1502
+ }
1503
+ }
1504
+ return remainder === 1;
1505
+ }
1506
+ function isIBAN(options) {
1507
+ const allowSpaces = options?.allowSpaces ?? false;
1508
+ const validateIban = (value) => typeof value === 'string' && validateIBAN(value, options);
1509
+ return makeRule({
1510
+ name: 'isIBAN',
1511
+ requiresType: 'string',
1512
+ constraints: { allowSpaces: options?.allowSpaces },
1513
+ validate: validateIban,
1514
+ emit: (varName, ctx) => {
1515
+ const baseRi = ctx.addRegex(/^[A-Z]{2}\d{2}[A-Z0-9]+$/);
1516
+ const tableIdx = ctx.addRef(IBAN_COUNTRY_LENGTH);
1517
+ let code = '{';
1518
+ code += `var ib=${allowSpaces ? `${varName}.replace(/\\s/g,'')` : varName}.toUpperCase();`;
1519
+ code += `if(!re[${baseRi}].test(ib)){${ctx.fail('isIBAN')}}`;
1520
+ code += `else{var ic=ib.slice(0,2),il=refs[${tableIdx}][ic];`;
1521
+ code += `if(il!==undefined&&ib.length!==il){${ctx.fail('isIBAN')}}`;
1522
+ code += `else{var ir=ib.slice(4)+ib.slice(0,4);`;
1523
+ // Walk char-by-char for mod 97 — no .replace closure, no parseInt allocation
1524
+ code += `var im=0;for(var ii=0;ii<ir.length;ii++){var cc=ir.charCodeAt(ii);`;
1525
+ code += `if(cc<=57){im=(im*10+(cc-48))%97;}else{im=(im*100+(cc-55))%97;}}`;
1526
+ code += `if(im!==1)${ctx.fail('isIBAN')};}}}`;
1527
+ return code;
1528
+ },
1529
+ });
1530
+ }
1531
+ // ByteLength — counts UTF-8 bytes via Buffer.byteLength
1532
+ function isByteLength(min, max) {
1533
+ const validateByteLength = (value) => {
1534
+ if (typeof value !== 'string') {
1535
+ return false;
1536
+ }
1537
+ const byteLen = Buffer.byteLength(value, 'utf8');
1538
+ if (byteLen < min) {
1539
+ return false;
1540
+ }
1541
+ if (max !== undefined && byteLen > max) {
1542
+ return false;
1543
+ }
1544
+ return true;
1545
+ };
1546
+ return makeRule({
1547
+ name: 'isByteLength',
1548
+ requiresType: 'string',
1549
+ constraints: { min, max },
1550
+ validate: validateByteLength,
1551
+ emit: (varName, ctx) => {
1552
+ let code = `{var bl=Buffer.byteLength(${varName},'utf8');`;
1553
+ code += `if(bl<${min})${ctx.fail('isByteLength')};`;
1554
+ if (max !== undefined) {
1555
+ code += `else if(bl>${max})${ctx.fail('isByteLength')};`;
1556
+ }
1557
+ code += '}';
1558
+ return code;
1559
+ },
1560
+ });
1561
+ }
1562
+ // ─────────────────────────────────────────────────────────────────────────────
1563
+ // Group E: New Validators
1564
+ // ─────────────────────────────────────────────────────────────────────────────
1565
+ // isHash — per-algorithm hex regex (§4.8 B: regex inline)
1566
+ const HASH_REGEXES = {
1567
+ md5: /^[a-f0-9]{32}$/i,
1568
+ md4: /^[a-f0-9]{32}$/i,
1569
+ md2: /^[a-f0-9]{32}$/i,
1570
+ sha1: /^[a-f0-9]{40}$/i,
1571
+ sha256: /^[a-f0-9]{64}$/i,
1572
+ sha384: /^[a-f0-9]{96}$/i,
1573
+ sha512: /^[a-f0-9]{128}$/i,
1574
+ ripemd128: /^[a-f0-9]{32}$/i,
1575
+ ripemd160: /^[a-f0-9]{40}$/i,
1576
+ 'tiger128,3': /^[a-f0-9]{32}$/i,
1577
+ 'tiger128,4': /^[a-f0-9]{32}$/i,
1578
+ 'tiger160,3': /^[a-f0-9]{40}$/i,
1579
+ 'tiger160,4': /^[a-f0-9]{40}$/i,
1580
+ 'tiger192,3': /^[a-f0-9]{48}$/i,
1581
+ 'tiger192,4': /^[a-f0-9]{48}$/i,
1582
+ crc32: /^[a-f0-9]{8}$/i,
1583
+ crc32b: /^[a-f0-9]{8}$/i,
1584
+ };
1585
+ function isHash(algorithm) {
1586
+ const re = HASH_REGEXES[algorithm];
1587
+ return makeRule({
1588
+ name: 'isHash',
1589
+ requiresType: 'string',
1590
+ constraints: { algorithm },
1591
+ validate: value => typeof value === 'string' && !!re && re.test(value),
1592
+ emit: (varName, ctx) => {
1593
+ if (!re) {
1594
+ return ctx.fail('isHash') + ';';
1595
+ }
1596
+ const i = ctx.addRegex(re);
1597
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isHash')};`;
1598
+ },
1599
+ });
1600
+ }
1601
+ // isRFC3339 — RFC 3339 datetime (§4.8 B)
1602
+ const RFC3339_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/i;
1603
+ const isRFC3339 = makeStringRule('isRFC3339', v => RFC3339_RE.test(v), (varName, ctx) => {
1604
+ const i = ctx.addRegex(RFC3339_RE);
1605
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isRFC3339')};`;
1606
+ });
1607
+ // isMilitaryTime — HH:MM 24-hour format (§4.8 B)
1608
+ const MILITARY_TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
1609
+ const isMilitaryTime = makeStringRule('isMilitaryTime', v => MILITARY_TIME_RE.test(v), (varName, ctx) => {
1610
+ const i = ctx.addRegex(MILITARY_TIME_RE);
1611
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isMilitaryTime')};`;
1612
+ });
1613
+ // isLatitude — string or number, -90 to 90 (requiresType none)
1614
+ function checkLatitude(value) {
1615
+ if (typeof value === 'number') {
1616
+ return value >= -90 && value <= 90;
1617
+ }
1618
+ if (typeof value === 'string') {
1619
+ const n = parseFloat(value);
1620
+ if (isNaN(n)) {
1621
+ return false;
1622
+ }
1623
+ // parseFloat('90abc') = 90 — strict regex check rejects trailing garbage
1624
+ if (!/^-?\d+(\.\d+)?$/.test(value)) {
1625
+ return false;
1626
+ }
1627
+ return n >= -90 && n <= 90;
1628
+ }
1629
+ return false;
1630
+ }
1631
+ const isLatitude = makeRule({
1632
+ name: 'isLatitude',
1633
+ constraints: {},
1634
+ validate: checkLatitude,
1635
+ emit: (varName, ctx) => {
1636
+ const ri = ctx.addRegex(/^-?\d+(\.\d+)?$/);
1637
+ return (`if(typeof ${varName}==='number'){if(${varName}<-90||${varName}>90)${ctx.fail('isLatitude')};}` +
1638
+ `else if(typeof ${varName}==='string'){` +
1639
+ // Regex catches non-numeric strings; if it matches, parseFloat is guaranteed valid (no isNaN check needed)
1640
+ `if(!re[${ri}].test(${varName})){${ctx.fail('isLatitude')}}` +
1641
+ `else{var lt=parseFloat(${varName});if(lt<-90||lt>90)${ctx.fail('isLatitude')};}}` +
1642
+ `else{${ctx.fail('isLatitude')};}`);
1643
+ },
1644
+ });
1645
+ // isLongitude — string or number, -180 to 180 (requiresType none)
1646
+ function checkLongitude(value) {
1647
+ if (typeof value === 'number') {
1648
+ return value >= -180 && value <= 180;
1649
+ }
1650
+ if (typeof value === 'string') {
1651
+ const n = parseFloat(value);
1652
+ if (isNaN(n)) {
1653
+ return false;
1654
+ }
1655
+ if (!/^-?\d+(\.\d+)?$/.test(value)) {
1656
+ return false;
1657
+ }
1658
+ return n >= -180 && n <= 180;
1659
+ }
1660
+ return false;
1661
+ }
1662
+ const isLongitude = makeRule({
1663
+ name: 'isLongitude',
1664
+ constraints: {},
1665
+ validate: checkLongitude,
1666
+ emit: (varName, ctx) => {
1667
+ const ri = ctx.addRegex(/^-?\d+(\.\d+)?$/);
1668
+ return (`if(typeof ${varName}==='number'){if(${varName}<-180||${varName}>180)${ctx.fail('isLongitude')};}` +
1669
+ `else if(typeof ${varName}==='string'){` +
1670
+ `if(!re[${ri}].test(${varName})){${ctx.fail('isLongitude')}}` +
1671
+ `else{var ln=parseFloat(${varName});if(ln<-180||ln>180)${ctx.fail('isLongitude')};}}` +
1672
+ `else{${ctx.fail('isLongitude')};}`);
1673
+ },
1674
+ });
1675
+ // isEthereumAddress — 0x + 40 hex chars (§4.8 B)
1676
+ const ETH_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
1677
+ const isEthereumAddress = makeStringRule('isEthereumAddress', v => ETH_ADDRESS_RE.test(v), (varName, ctx) => {
1678
+ const i = ctx.addRegex(ETH_ADDRESS_RE);
1679
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isEthereumAddress')};`;
1680
+ });
1681
+ // isBtcAddress — P2PKH (1...), P2SH (3...), bech32 (bc1...) (§4.8 B)
1682
+ const BTC_P2PKH_RE = /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
1683
+ const BTC_P2SH_RE = /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/;
1684
+ const BTC_BECH32_RE = /^(bc1)[a-z0-9]{6,87}$/;
1685
+ const isBtcAddress = makeStringRule('isBtcAddress', v => BTC_P2PKH_RE.test(v) || BTC_P2SH_RE.test(v) || BTC_BECH32_RE.test(v), (varName, ctx) => {
1686
+ const i1 = ctx.addRegex(BTC_P2PKH_RE);
1687
+ const i2 = ctx.addRegex(BTC_P2SH_RE);
1688
+ const i3 = ctx.addRegex(BTC_BECH32_RE);
1689
+ return `if (!re[${i1}].test(${varName}) && !re[${i2}].test(${varName}) && !re[${i3}].test(${varName})) ${ctx.fail('isBtcAddress')};`;
1690
+ });
1691
+ // isISO4217CurrencyCode — ISO 4217 currency code set (§4.8 C: ref-based)
1692
+ const ISO4217_CODES = new Set([
1693
+ 'AED',
1694
+ 'AFN',
1695
+ 'ALL',
1696
+ 'AMD',
1697
+ 'ANG',
1698
+ 'AOA',
1699
+ 'ARS',
1700
+ 'AUD',
1701
+ 'AWG',
1702
+ 'AZN',
1703
+ 'BAM',
1704
+ 'BBD',
1705
+ 'BDT',
1706
+ 'BGN',
1707
+ 'BHD',
1708
+ 'BIF',
1709
+ 'BMD',
1710
+ 'BND',
1711
+ 'BOB',
1712
+ 'BOV',
1713
+ 'BRL',
1714
+ 'BSD',
1715
+ 'BTN',
1716
+ 'BWP',
1717
+ 'BYN',
1718
+ 'BZD',
1719
+ 'CAD',
1720
+ 'CDF',
1721
+ 'CHE',
1722
+ 'CHF',
1723
+ 'CHW',
1724
+ 'CLF',
1725
+ 'CLP',
1726
+ 'CNY',
1727
+ 'COP',
1728
+ 'COU',
1729
+ 'CRC',
1730
+ 'CUC',
1731
+ 'CUP',
1732
+ 'CVE',
1733
+ 'CZK',
1734
+ 'DJF',
1735
+ 'DKK',
1736
+ 'DOP',
1737
+ 'DZD',
1738
+ 'EGP',
1739
+ 'ERN',
1740
+ 'ETB',
1741
+ 'EUR',
1742
+ 'FJD',
1743
+ 'FKP',
1744
+ 'GBP',
1745
+ 'GEL',
1746
+ 'GHS',
1747
+ 'GIP',
1748
+ 'GMD',
1749
+ 'GNF',
1750
+ 'GTQ',
1751
+ 'GYD',
1752
+ 'HKD',
1753
+ 'HNL',
1754
+ 'HRK',
1755
+ 'HTG',
1756
+ 'HUF',
1757
+ 'IDR',
1758
+ 'ILS',
1759
+ 'INR',
1760
+ 'IQD',
1761
+ 'IRR',
1762
+ 'ISK',
1763
+ 'JMD',
1764
+ 'JOD',
1765
+ 'JPY',
1766
+ 'KES',
1767
+ 'KGS',
1768
+ 'KHR',
1769
+ 'KMF',
1770
+ 'KPW',
1771
+ 'KRW',
1772
+ 'KWD',
1773
+ 'KYD',
1774
+ 'KZT',
1775
+ 'LAK',
1776
+ 'LBP',
1777
+ 'LKR',
1778
+ 'LRD',
1779
+ 'LSL',
1780
+ 'LYD',
1781
+ 'MAD',
1782
+ 'MDL',
1783
+ 'MGA',
1784
+ 'MKD',
1785
+ 'MMK',
1786
+ 'MNT',
1787
+ 'MOP',
1788
+ 'MRU',
1789
+ 'MUR',
1790
+ 'MVR',
1791
+ 'MWK',
1792
+ 'MXN',
1793
+ 'MXV',
1794
+ 'MYR',
1795
+ 'MZN',
1796
+ 'NAD',
1797
+ 'NGN',
1798
+ 'NIO',
1799
+ 'NOK',
1800
+ 'NPR',
1801
+ 'NZD',
1802
+ 'OMR',
1803
+ 'PAB',
1804
+ 'PEN',
1805
+ 'PGK',
1806
+ 'PHP',
1807
+ 'PKR',
1808
+ 'PLN',
1809
+ 'PYG',
1810
+ 'QAR',
1811
+ 'RON',
1812
+ 'RSD',
1813
+ 'RUB',
1814
+ 'RWF',
1815
+ 'SAR',
1816
+ 'SBD',
1817
+ 'SCR',
1818
+ 'SDG',
1819
+ 'SEK',
1820
+ 'SGD',
1821
+ 'SHP',
1822
+ 'SLE',
1823
+ 'SLL',
1824
+ 'SOS',
1825
+ 'SRD',
1826
+ 'SSP',
1827
+ 'STN',
1828
+ 'SVC',
1829
+ 'SYP',
1830
+ 'SZL',
1831
+ 'THB',
1832
+ 'TJS',
1833
+ 'TMT',
1834
+ 'TND',
1835
+ 'TOP',
1836
+ 'TRY',
1837
+ 'TTD',
1838
+ 'TWD',
1839
+ 'TZS',
1840
+ 'UAH',
1841
+ 'UGX',
1842
+ 'USD',
1843
+ 'USN',
1844
+ 'UYI',
1845
+ 'UYU',
1846
+ 'UYW',
1847
+ 'UZS',
1848
+ 'VED',
1849
+ 'VES',
1850
+ 'VND',
1851
+ 'VUV',
1852
+ 'WST',
1853
+ 'XAF',
1854
+ 'XAG',
1855
+ 'XAU',
1856
+ 'XBA',
1857
+ 'XBB',
1858
+ 'XBC',
1859
+ 'XBD',
1860
+ 'XCD',
1861
+ 'XDR',
1862
+ 'XOF',
1863
+ 'XPD',
1864
+ 'XPF',
1865
+ 'XPT',
1866
+ 'XSU',
1867
+ 'XTS',
1868
+ 'XUA',
1869
+ 'YER',
1870
+ 'ZAR',
1871
+ 'ZMW',
1872
+ 'ZWL',
1873
+ ]);
1874
+ const isISO4217CurrencyCode = makeStringRule('isISO4217CurrencyCode', v => ISO4217_CODES.has(v), (varName, ctx) => {
1875
+ const i = ctx.addRef(ISO4217_CODES);
1876
+ return `if (!refs[${i}].has(${varName})) ${ctx.fail('isISO4217CurrencyCode')};`;
1877
+ });
1878
+ // isPhoneNumber — E.164 international phone number (§4.8 B)
1879
+ const PHONE_E164_RE = /^\+[1-9]\d{6,14}$/;
1880
+ const isPhoneNumber = makeStringRule('isPhoneNumber', v => PHONE_E164_RE.test(v), (varName, ctx) => {
1881
+ const i = ctx.addRegex(PHONE_E164_RE);
1882
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isPhoneNumber')};`;
1883
+ });
1884
+ function isStrongPassword(options) {
1885
+ const minLength = options?.minLength ?? 8;
1886
+ const minLower = options?.minLowercase ?? 1;
1887
+ const minUpper = options?.minUppercase ?? 1;
1888
+ const minNums = options?.minNumbers ?? 1;
1889
+ const minSymbols = options?.minSymbols ?? 1;
1890
+ // Single-pass character classification — counts all categories in one scan.
1891
+ // Replaces 4× v.match(/.../g) which allocates 4 result arrays per call.
1892
+ const validate = (v) => {
1893
+ if (v.length < minLength) {
1894
+ return false;
1895
+ }
1896
+ let lower = 0;
1897
+ let upper = 0;
1898
+ let nums = 0;
1899
+ let symbols = 0;
1900
+ for (let i = 0; i < v.length; i++) {
1901
+ const c = v.charCodeAt(i);
1902
+ if (c >= 97 && c <= 122) {
1903
+ lower++;
1904
+ }
1905
+ else if (c >= 65 && c <= 90) {
1906
+ upper++;
1907
+ }
1908
+ else if (c >= 48 && c <= 57) {
1909
+ nums++;
1910
+ }
1911
+ else {
1912
+ symbols++;
1913
+ }
1914
+ }
1915
+ return lower >= minLower && upper >= minUpper && nums >= minNums && symbols >= minSymbols;
1916
+ };
1917
+ return makeRule({
1918
+ name: 'isStrongPassword',
1919
+ requiresType: 'string',
1920
+ constraints: {},
1921
+ validate: value => typeof value === 'string' && validate(value),
1922
+ emit: (varName, ctx) => {
1923
+ // Inline single-pass scan in the JIT executor — no regex match[] allocations
1924
+ const failExpr = ctx.fail('isStrongPassword');
1925
+ const checks = [];
1926
+ if (minLower > 0) {
1927
+ checks.push(`spLo<${minLower}`);
1928
+ }
1929
+ if (minUpper > 0) {
1930
+ checks.push(`spUp<${minUpper}`);
1931
+ }
1932
+ if (minNums > 0) {
1933
+ checks.push(`spNum<${minNums}`);
1934
+ }
1935
+ if (minSymbols > 0) {
1936
+ checks.push(`spSym<${minSymbols}`);
1937
+ }
1938
+ const guard = checks.length === 0 ? '' : `if(${checks.join('||')}){${failExpr}}`;
1939
+ return (`if(${varName}.length<${minLength}){${failExpr}}else{` +
1940
+ `var spLo=0,spUp=0,spNum=0,spSym=0;` +
1941
+ `for(var spI=0;spI<${varName}.length;spI++){var spC=${varName}.charCodeAt(spI);` +
1942
+ `if(spC>=97&&spC<=122)spLo++;else if(spC>=65&&spC<=90)spUp++;else if(spC>=48&&spC<=57)spNum++;else spSym++;}` +
1943
+ guard +
1944
+ `}`);
1945
+ },
1946
+ });
1947
+ }
1948
+ // isTaxId — locale-specific tax identifier (§4.8 C: factory)
1949
+ const TAX_ID_REGEXES = {
1950
+ US: /^\d{2}-\d{7}$/, // EIN format: XX-XXXXXXX
1951
+ KR: /^\d{3}-\d{2}-\d{5}$/, // Business Registration Number: XXX-XX-XXXXX
1952
+ DE: /^\d{11}$/, // Steuernummer: 11 digits
1953
+ FR: /^[0-9]{13}$/, // SIRET: 13 digits
1954
+ GB: /^\d{10}$/, // UTR: 10 digits
1955
+ IT: /^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/i, // Codice Fiscale
1956
+ ES: /^[0-9A-Z]\d{7}[0-9A-Z]$/i, // NIF/NIE/CIF
1957
+ AU: /^\d{11}$/, // ABN: 11 digits
1958
+ CA: /^\d{9}$/, // BN: 9 digits
1959
+ IN: /^[A-Z]{5}\d{4}[A-Z]$/i, // PAN: XXXXX9999X
1960
+ };
1961
+ function isTaxId(locale) {
1962
+ const re = TAX_ID_REGEXES[locale];
1963
+ return makeRule({
1964
+ name: 'isTaxId',
1965
+ requiresType: 'string',
1966
+ constraints: { locale },
1967
+ validate: value => typeof value === 'string' && !!re && re.test(value),
1968
+ emit: (varName, ctx) => {
1969
+ if (!re) {
1970
+ return ctx.fail('isTaxId') + ';';
1971
+ }
1972
+ const i = ctx.addRegex(re);
1973
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isTaxId')};`;
1974
+ },
1975
+ });
1976
+ }
1977
+ // ─────────────────────────────────────────────────────────────────────────────
1978
+ // ULID
1979
+ // ─────────────────────────────────────────────────────────────────────────────
1980
+ const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
1981
+ function isULID() {
1982
+ return makeStringRule('isULID', v => ULID_RE.test(v), (varName, ctx) => {
1983
+ const i = ctx.addRegex(ULID_RE);
1984
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isULID')};`;
1985
+ }, 'string', { format: 'ulid' });
1986
+ }
1987
+ // ─────────────────────────────────────────────────────────────────────────────
1988
+ // CUID2
1989
+ // ─────────────────────────────────────────────────────────────────────────────
1990
+ // CUID2 spec: length 24-32, lowercase alphanum, starts with a-z.
1991
+ const CUID2_RE = /^[a-z][0-9a-z]{23,31}$/;
1992
+ function isCUID2() {
1993
+ return makeStringRule('isCUID2', v => CUID2_RE.test(v), (varName, ctx) => {
1994
+ const i = ctx.addRegex(CUID2_RE);
1995
+ return `if (!re[${i}].test(${varName})) ${ctx.fail('isCUID2')};`;
1996
+ }, 'string', { format: 'cuid2' });
1997
+ }
1998
+ export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, };