schema-shield 0.0.6 → 1.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 (39) hide show
  1. package/README.md +219 -65
  2. package/dist/formats.d.ts.map +1 -1
  3. package/dist/index.d.ts +25 -6
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1837 -484
  6. package/dist/index.min.js +1 -1
  7. package/dist/index.min.js.map +1 -1
  8. package/dist/index.mjs +1837 -484
  9. package/dist/keywords/array-keywords.d.ts.map +1 -1
  10. package/dist/keywords/object-keywords.d.ts.map +1 -1
  11. package/dist/keywords/other-keywords.d.ts.map +1 -1
  12. package/dist/keywords/string-keywords.d.ts.map +1 -1
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/deep-freeze.d.ts +5 -0
  15. package/dist/utils/deep-freeze.d.ts.map +1 -0
  16. package/dist/utils/has-changed.d.ts +2 -0
  17. package/dist/utils/has-changed.d.ts.map +1 -0
  18. package/dist/utils/index.d.ts +5 -0
  19. package/dist/utils/index.d.ts.map +1 -0
  20. package/dist/{utils.d.ts → utils/main-utils.d.ts} +7 -9
  21. package/dist/utils/main-utils.d.ts.map +1 -0
  22. package/dist/utils/pattern-matcher.d.ts +3 -0
  23. package/dist/utils/pattern-matcher.d.ts.map +1 -0
  24. package/lib/formats.ts +468 -155
  25. package/lib/index.ts +702 -107
  26. package/lib/keywords/array-keywords.ts +260 -52
  27. package/lib/keywords/number-keywords.ts +1 -1
  28. package/lib/keywords/object-keywords.ts +295 -88
  29. package/lib/keywords/other-keywords.ts +263 -70
  30. package/lib/keywords/string-keywords.ts +123 -7
  31. package/lib/types.ts +5 -18
  32. package/lib/utils/deep-freeze.ts +208 -0
  33. package/lib/utils/has-changed.ts +51 -0
  34. package/lib/utils/index.ts +4 -0
  35. package/lib/{utils.ts → utils/main-utils.ts} +63 -77
  36. package/lib/utils/pattern-matcher.ts +66 -0
  37. package/package.json +2 -2
  38. package/tsconfig.json +4 -4
  39. package/dist/utils.d.ts.map +0 -1
package/lib/formats.ts CHANGED
@@ -1,194 +1,498 @@
1
1
  import { FormatFunction } from "./index";
2
2
 
3
- export const Formats: Record<string, FormatFunction | false> = {
4
- ["date-time"](data) {
5
- const match = data.match(
6
- /^(\d{4})-(0[0-9]|1[0-2])-(\d{2})T(0[0-9]|1\d|2[0-3]):([0-5]\d):((?:[0-5]\d|60))(?:.\d+)?(?:([+-])(0[0-9]|1\d|2[0-3]):([0-5]\d)|Z)?$/i
7
- );
3
+ // Regex helpers
4
+ const UUID_REGEX =
5
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ // ISO 8601 Duration (P3Y6M4DT12H30M5S)
7
+ const DURATION_REGEX =
8
+ /^P(?!$)((\d+Y)?(\d+M)?(\d+W)?(\d+D)?)(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
9
+ const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/;
10
+ const EMAIL_REGEX =
11
+ /^(?!\.)(?!.*\.$)[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,20}(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,21}){0,2}@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,60}[a-z0-9])?){0,3}$/i;
12
+ const HOSTNAME_REGEX =
13
+ /^[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9][a-z0-9-]{0,62})*[a-z0-9]$/i;
14
+ const DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
15
+ const TIME_REGEX =
16
+ /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(\.\d+)?(Z|([+-])([01]\d|2[0-3]):([0-5]\d))$/;
17
+ const URI_REFERENCE_REGEX =
18
+ /^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#((?![^#]*\\)[^#]*))?/i;
19
+ const IRI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/;
20
+ const IRI_REFERENCE_REGEX =
21
+ /^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#((?![^#]*\\)[^#]*))?/i;
22
+ const IDN_EMAIL_REGEX = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
23
+ const IDN_HOSTNAME_REGEX = /^[^\s!@#$%^&*()_+\=\[\]{};':"\\|,<>\/?]+$/;
24
+
25
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
26
+
27
+ function isDigitCharCode(code: number) {
28
+ return code >= 48 && code <= 57;
29
+ }
30
+
31
+ function parseTwoDigits(data: string, index: number) {
32
+ const first = data.charCodeAt(index) - 48;
33
+ const second = data.charCodeAt(index + 1) - 48;
34
+
35
+ if (first < 0 || first > 9 || second < 0 || second > 9) {
36
+ return -1;
37
+ }
38
+
39
+ return first * 10 + second;
40
+ }
41
+
42
+ function parseFourDigits(data: string, index: number) {
43
+ const a = data.charCodeAt(index) - 48;
44
+ const b = data.charCodeAt(index + 1) - 48;
45
+ const c = data.charCodeAt(index + 2) - 48;
46
+ const d = data.charCodeAt(index + 3) - 48;
47
+
48
+ if (
49
+ a < 0 ||
50
+ a > 9 ||
51
+ b < 0 ||
52
+ b > 9 ||
53
+ c < 0 ||
54
+ c > 9 ||
55
+ d < 0 ||
56
+ d > 9
57
+ ) {
58
+ return -1;
59
+ }
60
+
61
+ return a * 1000 + b * 100 + c * 10 + d;
62
+ }
63
+
64
+ function isValidIpv4Range(data: string, start: number, end: number) {
65
+ let segmentCount = 0;
66
+ let segmentStart = start;
67
+
68
+ for (let i = start; i <= end; i++) {
69
+ if (i !== end && data.charCodeAt(i) !== 46) {
70
+ continue;
71
+ }
8
72
 
9
- if (!match) {
73
+ const segmentLength = i - segmentStart;
74
+ if (segmentLength < 1 || segmentLength > 3) {
10
75
  return false;
11
76
  }
12
77
 
13
- let day = Number(match[3]);
14
-
15
- if (match[2] === "02" && day > 29) {
78
+ if (segmentLength > 1 && data.charCodeAt(segmentStart) === 48) {
16
79
  return false;
17
80
  }
18
81
 
19
- const [
20
- ,
21
- yearStr,
22
- monthStr,
23
- ,
24
- hourStr,
25
- minuteStr,
26
- secondStr,
27
- timezoneSign,
28
- timezoneHourStr,
29
- timezoneMinuteStr
30
- ] = match;
82
+ let value = 0;
83
+ for (let j = segmentStart; j < i; j++) {
84
+ const digit = data.charCodeAt(j) - 48;
85
+ if (digit < 0 || digit > 9) {
86
+ return false;
87
+ }
31
88
 
32
- let year = Number(yearStr);
33
- let month = Number(monthStr);
34
- let hour = Number(hourStr);
35
- let minute = Number(minuteStr);
36
- let second = Number(secondStr);
89
+ value = value * 10 + digit;
90
+ }
37
91
 
38
- if (timezoneSign === "-" || timezoneSign === "+") {
39
- const timezoneHour = Number(timezoneHourStr);
40
- const timezoneMinute = Number(timezoneMinuteStr);
92
+ if (value > 255) {
93
+ return false;
94
+ }
41
95
 
42
- if (timezoneSign === "-") {
43
- hour += timezoneHour;
44
- minute += timezoneMinute;
45
- } else if (timezoneSign === "+") {
46
- hour -= timezoneHour;
47
- minute -= timezoneMinute;
96
+ segmentCount++;
97
+ segmentStart = i + 1;
98
+ }
99
+
100
+ return segmentCount === 4;
101
+ }
102
+
103
+ function isValidIpv4(data: string) {
104
+ return isValidIpv4Range(data, 0, data.length);
105
+ }
106
+
107
+ function isHexCharCode(code: number) {
108
+ return (
109
+ (code >= 48 && code <= 57) ||
110
+ (code >= 65 && code <= 70) ||
111
+ (code >= 97 && code <= 102)
112
+ );
113
+ }
114
+
115
+ function isValidIpv6(data: string) {
116
+ const length = data.length;
117
+ if (length === 0) {
118
+ return false;
119
+ }
120
+
121
+ let hasColon = false;
122
+ let hasDoubleColon = false;
123
+ let hextetCount = 0;
124
+ let i = 0;
125
+
126
+ while (i < length) {
127
+ if (data.charCodeAt(i) === 58) {
128
+ hasColon = true;
129
+
130
+ if (i + 1 < length && data.charCodeAt(i + 1) === 58) {
131
+ if (hasDoubleColon) {
132
+ return false;
133
+ }
134
+
135
+ hasDoubleColon = true;
136
+ i += 2;
137
+
138
+ if (i === length) {
139
+ break;
140
+ }
141
+
142
+ continue;
48
143
  }
49
144
 
50
- if (minute > 59) {
51
- hour += 1;
52
- minute -= 60;
53
- } else if (minute < 0) {
54
- hour -= 1;
55
- minute += 60;
145
+ return false;
146
+ }
147
+
148
+ const segmentStart = i;
149
+ let segmentLength = 0;
150
+
151
+ while (i < length && isHexCharCode(data.charCodeAt(i))) {
152
+ segmentLength++;
153
+ if (segmentLength > 4) {
154
+ return false;
56
155
  }
57
156
 
58
- if (hour > 23) {
59
- day += 1;
60
- hour -= 24;
61
- } else if (hour < 0) {
62
- day -= 1;
63
- hour += 24;
157
+ i++;
158
+ }
159
+
160
+ if (segmentLength === 0) {
161
+ return false;
162
+ }
163
+
164
+ if (i < length && data.charCodeAt(i) === 46) {
165
+ if (!hasColon) {
166
+ return false;
64
167
  }
65
168
 
66
- if (day > 31) {
67
- month += 1;
68
- day -= 31;
69
- } else if (day < 1) {
70
- month -= 1;
71
- day += 31;
169
+ if (!isValidIpv4Range(data, segmentStart, length)) {
170
+ return false;
72
171
  }
73
172
 
74
- if (month > 12) {
75
- year += 1;
76
- month -= 12;
77
- } else if (month < 1) {
78
- year -= 1;
79
- month += 12;
173
+ if (hasDoubleColon) {
174
+ return hextetCount < 6;
80
175
  }
81
176
 
82
- if (year < 0) {
177
+ return hextetCount === 6;
178
+ }
179
+
180
+ hextetCount++;
181
+ if (hextetCount > 8) {
182
+ return false;
183
+ }
184
+
185
+ if (i === length) {
186
+ break;
187
+ }
188
+
189
+ if (data.charCodeAt(i) !== 58) {
190
+ return false;
191
+ }
192
+
193
+ hasColon = true;
194
+ i++;
195
+
196
+ if (i === length) {
197
+ return false;
198
+ }
199
+
200
+ if (data.charCodeAt(i) === 58) {
201
+ if (hasDoubleColon) {
83
202
  return false;
84
203
  }
204
+
205
+ hasDoubleColon = true;
206
+ i++;
207
+
208
+ if (i === length) {
209
+ break;
210
+ }
85
211
  }
212
+ }
86
213
 
87
- const daysInMonth = [31, , 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
88
- const maxDays =
89
- month === 2
90
- ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
91
- ? 29
92
- : 28
93
- : daysInMonth[month - 1];
214
+ if (!hasColon) {
215
+ return false;
216
+ }
94
217
 
95
- if (day > maxDays) {
96
- return false;
218
+ if (hasDoubleColon) {
219
+ return hextetCount < 8;
220
+ }
221
+
222
+ return hextetCount === 8;
223
+ }
224
+
225
+ function isValidJsonPointer(data: string) {
226
+ if (data === "") {
227
+ return true;
228
+ }
229
+
230
+ if (data.charCodeAt(0) !== 47) {
231
+ return false;
232
+ }
233
+
234
+ for (let i = 1; i < data.length; i++) {
235
+ if (data.charCodeAt(i) !== 126) {
236
+ continue;
97
237
  }
98
238
 
99
- // Leap seconds
100
- if (second === 60 && (minute !== 59 || hour !== 23)) {
239
+ const next = data.charCodeAt(i + 1);
240
+ if (next !== 48 && next !== 49) {
101
241
  return false;
102
242
  }
103
243
 
244
+ i++;
245
+ }
246
+
247
+ return true;
248
+ }
249
+
250
+ function isValidRelativeJsonPointer(data: string) {
251
+ if (data.length === 0) {
104
252
  return true;
105
- },
106
- uri(data) {
107
- return /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/.test(data);
108
- },
109
- email(data) {
110
- return /^(?!\.)(?!.*\.$)[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,20}(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,21}){0,2}@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,60}[a-z0-9])?){0,3}$/i.test(
111
- data
112
- );
113
- },
114
- ipv4(data) {
115
- // Matches a string formed by 4 numbers between 0 and 255 separated by dots without leading zeros
116
- // /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$/
117
- return /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$/.test(
118
- data
119
- );
120
- },
253
+ }
121
254
 
122
- // ipv6: isMyIpValid({ version: 6 }),
123
- ipv6(data) {
124
- if (data === "::") {
125
- return true;
255
+ let i = 0;
256
+ while (i < data.length) {
257
+ const code = data.charCodeAt(i);
258
+ if (code < 48 || code > 57) {
259
+ break;
260
+ }
261
+ i++;
262
+ }
263
+
264
+ if (i === 0) {
265
+ return false;
266
+ }
267
+
268
+ if (i === data.length) {
269
+ return true;
270
+ }
271
+
272
+ if (data.charCodeAt(i) === 35) {
273
+ return i + 1 === data.length;
274
+ }
275
+
276
+ if (data.charCodeAt(i) !== 47) {
277
+ return false;
278
+ }
279
+
280
+ for (i = i + 1; i < data.length; i++) {
281
+ if (data.charCodeAt(i) !== 126) {
282
+ continue;
283
+ }
284
+
285
+ const next = data.charCodeAt(i + 1);
286
+ if (next !== 48 && next !== 49) {
287
+ return false;
288
+ }
289
+
290
+ i++;
291
+ }
292
+
293
+ return true;
294
+ }
295
+
296
+ function isValidUriTemplate(data: string) {
297
+ for (let i = 0; i < data.length; i++) {
298
+ const code = data.charCodeAt(i);
299
+
300
+ if (code === 125) {
301
+ return false;
302
+ }
303
+
304
+ if (code !== 123) {
305
+ continue;
306
+ }
307
+
308
+ const closeIndex = data.indexOf("}", i + 1);
309
+ if (closeIndex === -1 || closeIndex === i + 1) {
310
+ return false;
311
+ }
312
+
313
+ i = closeIndex;
314
+ }
315
+
316
+ return true;
317
+ }
318
+
319
+ export const Formats: Record<string, FormatFunction | false> = {
320
+ ["date-time"](data) {
321
+ const length = data.length;
322
+ if (length < 19) {
323
+ return false;
126
324
  }
127
325
 
128
326
  if (
129
- data.indexOf(":") === -1 ||
130
- /(?:\s+|:::+|^\w{5,}|\w{5}$|^:{1}\w|\w:{1}$)/.test(data)
327
+ data.charCodeAt(4) !== 45 ||
328
+ data.charCodeAt(7) !== 45 ||
329
+ data.charCodeAt(13) !== 58 ||
330
+ data.charCodeAt(16) !== 58
131
331
  ) {
132
332
  return false;
133
333
  }
134
334
 
135
- const hasIpv4 = data.indexOf(".") !== -1;
136
- let addressParts = data;
335
+ const tCode = data.charCodeAt(10);
336
+ if (tCode !== 84 && tCode !== 116) {
337
+ return false;
338
+ }
137
339
 
138
- if (hasIpv4) {
139
- addressParts = data.split(":");
140
- const ipv4Part = addressParts.pop();
141
- if (
142
- !/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$/.test(
143
- ipv4Part
144
- )
145
- ) {
146
- return false;
147
- }
340
+ const year = parseFourDigits(data, 0);
341
+ const month = parseTwoDigits(data, 5);
342
+ const day = parseTwoDigits(data, 8);
343
+ const hour = parseTwoDigits(data, 11);
344
+ const minute = parseTwoDigits(data, 14);
345
+ const second = parseTwoDigits(data, 17);
346
+
347
+ if (
348
+ year < 0 ||
349
+ month < 0 ||
350
+ day < 0 ||
351
+ hour < 0 ||
352
+ minute < 0 ||
353
+ second < 0
354
+ ) {
355
+ return false;
356
+ }
357
+
358
+ if (hour > 23 || minute > 59 || second > 60) {
359
+ return false;
148
360
  }
149
361
 
150
- const isShortened = data.indexOf("::") !== -1;
151
- const ipv6Part = hasIpv4 ? addressParts.join(":") : data;
362
+ let cursor = 19;
363
+ let offsetSign: "+" | "-" | null = null;
364
+ let offsetHour = 0;
365
+ let offsetMinute = 0;
366
+
367
+ if (cursor < length && data.charCodeAt(cursor) === 46) {
368
+ cursor++;
369
+ const fracStart = cursor;
370
+ while (cursor < length && isDigitCharCode(data.charCodeAt(cursor))) {
371
+ cursor++;
372
+ }
152
373
 
153
- if (isShortened) {
154
- if (ipv6Part.split("::").length - 1 > 1) {
374
+ if (cursor === fracStart) {
155
375
  return false;
156
376
  }
377
+ }
378
+
379
+ if (cursor < length) {
380
+ const tzCode = data.charCodeAt(cursor);
381
+
382
+ if (tzCode === 90 || tzCode === 122) {
383
+ cursor++;
384
+ } else if (tzCode === 43 || tzCode === 45) {
385
+ offsetSign = tzCode === 43 ? "+" : "-";
386
+
387
+ if (cursor + 6 > length || data.charCodeAt(cursor + 3) !== 58) {
388
+ return false;
389
+ }
390
+
391
+ offsetHour = parseTwoDigits(data, cursor + 1);
392
+ offsetMinute = parseTwoDigits(data, cursor + 4);
157
393
 
158
- if (!/^[0-9a-fA-F:.]*$/.test(ipv6Part)) {
394
+ if (
395
+ offsetHour < 0 ||
396
+ offsetMinute < 0 ||
397
+ offsetHour > 23 ||
398
+ offsetMinute > 59
399
+ ) {
400
+ return false;
401
+ }
402
+
403
+ cursor += 6;
404
+ } else {
159
405
  return false;
160
406
  }
407
+ }
161
408
 
162
- return /^(?:(?:(?:[0-9a-fA-F]{1,4}(?::|$)){1,6}))|(?:::(?:[0-9a-fA-F]{1,4})){0,5}$/.test(
163
- ipv6Part
164
- );
409
+ if (cursor !== length) {
410
+ return false;
165
411
  }
166
412
 
167
- const isIpv6Valid =
168
- /^(?:(?:[0-9a-fA-F]{1,4}:){7}(?:[0-9a-fA-F]{1,4}|:))$/.test(ipv6Part);
413
+ // Mes 1–12
414
+ if (month < 1 || month > 12) {
415
+ return false;
416
+ }
417
+ // Día >= 1
418
+ if (day < 1) {
419
+ return false;
420
+ }
169
421
 
170
- const hasInvalidChar = /(?:[0-9a-fA-F]{5,}|\D[0-9a-fA-F]{3}:)/.test(
171
- ipv6Part
172
- );
422
+ const maxDays =
423
+ month === 2
424
+ ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
425
+ ? 29
426
+ : 28
427
+ : DAYS_IN_MONTH[month - 1];
428
+
429
+ if (!maxDays || day > maxDays) {
430
+ return false;
431
+ }
432
+
433
+ if (second === 60) {
434
+ let utcTotalMinutes = hour * 60 + minute;
435
+
436
+ if (offsetSign) {
437
+ const offsetTotalMinutes = offsetHour * 60 + offsetMinute;
438
+ utcTotalMinutes +=
439
+ offsetSign === "+" ? -offsetTotalMinutes : offsetTotalMinutes;
440
+ utcTotalMinutes %= 24 * 60;
441
+ if (utcTotalMinutes < 0) {
442
+ utcTotalMinutes += 24 * 60;
443
+ }
444
+ }
173
445
 
174
- if (hasIpv4) {
175
- return isIpv6Valid || !hasInvalidChar;
446
+ if (utcTotalMinutes !== 23 * 60 + 59) {
447
+ return false;
448
+ }
176
449
  }
177
450
 
178
- return isIpv6Valid && !hasInvalidChar;
451
+ return true;
452
+ },
453
+ uri(data) {
454
+ return URI_REGEX.test(data);
455
+ },
456
+ email(data) {
457
+ return EMAIL_REGEX.test(data);
458
+ },
459
+ ipv4(data) {
460
+ return isValidIpv4(data);
461
+ },
462
+
463
+ ipv6(data) {
464
+ return isValidIpv6(data);
179
465
  },
180
466
 
181
467
  hostname(data) {
182
- return /^[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9][a-z0-9-]{0,62})*[a-z0-9]$/i.test(
183
- data
184
- );
468
+ return HOSTNAME_REGEX.test(data);
185
469
  },
186
470
  date(data) {
187
- if (/^(\d{4})-(\d{2})-(\d{2})$/.test(data) === false) {
471
+ const match = DATE_REGEX.exec(data);
472
+ if (!match) {
188
473
  return false;
189
474
  }
190
475
 
191
- return !isNaN(new Date(data).getTime());
476
+ const [, yearStr, monthStr, dayStr] = match;
477
+ const year = Number(yearStr);
478
+ const month = Number(monthStr);
479
+ const day = Number(dayStr);
480
+
481
+ if (month < 1 || month > 12) {
482
+ return false;
483
+ }
484
+ if (day < 1) {
485
+ return false;
486
+ }
487
+
488
+ const maxDays =
489
+ month === 2
490
+ ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
491
+ ? 29
492
+ : 28
493
+ : DAYS_IN_MONTH[month - 1];
494
+
495
+ return !!maxDays && day <= maxDays;
192
496
  },
193
497
  regex(data) {
194
498
  try {
@@ -199,44 +503,53 @@ export const Formats: Record<string, FormatFunction | false> = {
199
503
  }
200
504
  },
201
505
  "json-pointer"(data) {
202
- if (data === "") {
203
- return true;
204
- }
205
-
206
- return /^\/(?:[^~]|~0|~1)*$/.test(data);
506
+ return isValidJsonPointer(data);
207
507
  },
208
508
  "relative-json-pointer"(data) {
209
- if (data === "") {
210
- return true;
211
- }
212
-
213
- return /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/.test(data);
509
+ return isValidRelativeJsonPointer(data);
214
510
  },
215
511
  time(data) {
216
- return /^(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|([+-])(\d{2}):(\d{2}))$/.test(
217
- data
218
- );
512
+ return TIME_REGEX.test(data);
219
513
  },
220
514
  "uri-reference"(data) {
221
- if (/\\/.test(data)) {
515
+ if (data.includes("\\")) {
222
516
  return false;
223
517
  }
224
518
 
225
- return /^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#((?![^#]*\\)[^#]*))?/i.test(
226
- data
227
- );
519
+ return URI_REFERENCE_REGEX.test(data);
228
520
  },
521
+
229
522
  "uri-template"(data) {
230
- return /^(?:(?:https?:\/\/[\w.-]+)?\/?)?[\w- ;,.\/?%&=]*(?:\{[\w-]+(?::\d+)?\}[\w- ;,.\/?%&=]*)*\/?$/.test(
231
- data
232
- );
233
- },
234
-
235
- // Not supported yet
236
- duration: false,
237
- uuid: false,
238
- "idn-email": false,
239
- "idn-hostname": false,
240
- iri: false,
241
- "iri-reference": false
523
+ return isValidUriTemplate(data);
524
+ },
525
+
526
+ duration(data) {
527
+ return DURATION_REGEX.test(data);
528
+ },
529
+
530
+ uuid(data) {
531
+ return UUID_REGEX.test(data);
532
+ },
533
+
534
+ // IRI is like URI but allows Unicode. We reuse a permissive logic.
535
+ iri(data) {
536
+ return IRI_REGEX.test(data);
537
+ },
538
+
539
+ "iri-reference"(data) {
540
+ if (data.includes("\\")) {
541
+ return false;
542
+ }
543
+ return IRI_REFERENCE_REGEX.test(data);
544
+ },
545
+
546
+ // Best-effort structural validation for IDN (no punycode/tables)
547
+ "idn-email"(data) {
548
+ return IDN_EMAIL_REGEX.test(data);
549
+ },
550
+
551
+ "idn-hostname"(data) {
552
+ // Allows unicode, forbids spaces and typical invalid URL chars
553
+ return IDN_HOSTNAME_REGEX.test(data);
554
+ }
242
555
  };