@json-schema-engine/formats 0.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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/idn.d.ts +4 -0
- package/dist/idn.d.ts.map +1 -0
- package/dist/idn.js +304 -0
- package/dist/idn.js.map +1 -0
- package/dist/idna.d.ts +50 -0
- package/dist/idna.d.ts.map +1 -0
- package/dist/idna.js +350 -0
- package/dist/idna.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +799 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/idn.ts +309 -0
- package/src/idna.ts +366 -0
- package/src/index.ts +876 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
// @json-schema-engine/formats: format implementations for the format-assertion vocabulary
|
|
2
|
+
// and the assertFormats configuration (M7). Implemented from the defining
|
|
3
|
+
// RFCs and the JSON Schema specs only (DESIGN.md D15) — no format/IDNA
|
|
4
|
+
// library is consulted.
|
|
5
|
+
//
|
|
6
|
+
// Every definition is type-scoped through FormatDefinition.types (default
|
|
7
|
+
// ["string"]): the KEYWORD treats instances outside a format's types as
|
|
8
|
+
// vacuously valid, per spec. The shape deliberately admits non-string
|
|
9
|
+
// formats (the OpenAPI format registry's number-scoped entries are a
|
|
10
|
+
// planned future table).
|
|
11
|
+
import { isValidALabel } from "./idna.js";
|
|
12
|
+
import { idnEmail, idnHostname } from "./idn.js";
|
|
13
|
+
export { idnEmail, idnHostname } from "./idn.js";
|
|
14
|
+
// EXEMPLAR (trivial tier): a fixed grammar — one anchored regex, no
|
|
15
|
+
// structural interplay. RFC 4122 §3: 8-4-4-4-12 hex digits, any version.
|
|
16
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
export const uuid = {
|
|
18
|
+
test: (value) => typeof value === "string" && UUID_RE.test(value),
|
|
19
|
+
};
|
|
20
|
+
// EXEMPLAR (parser tier): grammar with structural rules a single regex
|
|
21
|
+
// obscures. RFC 4291 §2.2: up to eight 16-bit hex fields; `::` compresses
|
|
22
|
+
// exactly one run (and must compress at least one field when 8 are already
|
|
23
|
+
// present); an embedded dotted-quad IPv4 tail counts as two fields.
|
|
24
|
+
const H16 = /^[0-9a-f]{1,4}$/i;
|
|
25
|
+
const IPV4_TAIL = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}$/;
|
|
26
|
+
function isIpv6(value) {
|
|
27
|
+
// A zone index or other non [0-9a-f:.] characters are out of grammar.
|
|
28
|
+
if (!/^[0-9a-f:.]+$/i.test(value))
|
|
29
|
+
return false;
|
|
30
|
+
const compressions = value.split("::").length - 1;
|
|
31
|
+
if (compressions > 1)
|
|
32
|
+
return false;
|
|
33
|
+
let fields = 0;
|
|
34
|
+
const sides = value.split("::");
|
|
35
|
+
for (let sideIndex = 0; sideIndex < sides.length; sideIndex++) {
|
|
36
|
+
const side = sides[sideIndex];
|
|
37
|
+
if (side === "")
|
|
38
|
+
continue; // "::" edge (leading/trailing)
|
|
39
|
+
const parts = side.split(":");
|
|
40
|
+
for (let i = 0; i < parts.length; i++) {
|
|
41
|
+
const part = parts[i];
|
|
42
|
+
if (part === "")
|
|
43
|
+
return false; // ":::", leading/trailing single ":"
|
|
44
|
+
const isLast = sideIndex === sides.length - 1 && i === parts.length - 1;
|
|
45
|
+
if (isLast && part.includes(".")) {
|
|
46
|
+
if (!IPV4_TAIL.test(part))
|
|
47
|
+
return false;
|
|
48
|
+
fields += 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!H16.test(part))
|
|
52
|
+
return false;
|
|
53
|
+
fields++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (compressions === 1)
|
|
57
|
+
return fields < 8; // "::" must stand for ≥1 field
|
|
58
|
+
return fields === 8;
|
|
59
|
+
}
|
|
60
|
+
export const ipv6 = {
|
|
61
|
+
test: (value) => typeof value === "string" && isIpv6(value),
|
|
62
|
+
};
|
|
63
|
+
// RFC 3339 §5.6 full-date = date-fullyear "-" date-month "-" date-mday.
|
|
64
|
+
// The grammar caps date-mday at 01-31 syntactically, but real month/leap-year
|
|
65
|
+
// lengths are a semantic check the suite exercises (e.g. 2021-02-29 invalid,
|
|
66
|
+
// 2020-02-29 valid) — the ABNF alone under-constrains this format.
|
|
67
|
+
const FULL_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
68
|
+
function isLeapYear(year) {
|
|
69
|
+
// Gregorian leap rule: divisible by 4, except centuries, except
|
|
70
|
+
// centuries divisible by 400 (RFC 3339 appendix C examples: 0100 and
|
|
71
|
+
// 2100 are not leap; 0400 is).
|
|
72
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
73
|
+
}
|
|
74
|
+
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
75
|
+
function isFullDate(value) {
|
|
76
|
+
const m = FULL_DATE_RE.exec(value);
|
|
77
|
+
if (m === null)
|
|
78
|
+
return false;
|
|
79
|
+
const year = Number(m[1]);
|
|
80
|
+
const month = Number(m[2]);
|
|
81
|
+
const day = Number(m[3]);
|
|
82
|
+
if (month < 1 || month > 12)
|
|
83
|
+
return false;
|
|
84
|
+
const maxDay = month === 2 && isLeapYear(year) ? 29 : DAYS_IN_MONTH[month - 1];
|
|
85
|
+
return day >= 1 && day <= maxDay;
|
|
86
|
+
}
|
|
87
|
+
export const date = {
|
|
88
|
+
test: (value) => typeof value === "string" && isFullDate(value),
|
|
89
|
+
};
|
|
90
|
+
// RFC 3339 appendix A duration ABNF:
|
|
91
|
+
// dur-second = 1*DIGIT "S"
|
|
92
|
+
// dur-minute = 1*DIGIT "M" [dur-second]
|
|
93
|
+
// dur-hour = 1*DIGIT "H" [dur-minute]
|
|
94
|
+
// dur-time = "T" (dur-hour / dur-minute / dur-second)
|
|
95
|
+
// dur-day = 1*DIGIT "D"
|
|
96
|
+
// dur-month = 1*DIGIT "M" [dur-day]
|
|
97
|
+
// dur-year = 1*DIGIT "Y" [dur-month]
|
|
98
|
+
// dur-week = 1*DIGIT "W"
|
|
99
|
+
// dur-date = (dur-day / dur-month / dur-year) [dur-time]
|
|
100
|
+
// duration = "P" (dur-date / dur-time / dur-week)
|
|
101
|
+
// The date/time components are each optional but strictly ordered
|
|
102
|
+
// (year-month-day, then hour-minute-second) when present, and the
|
|
103
|
+
// grammar's optional-suffix nesting means at least one numeric component
|
|
104
|
+
// must actually appear after "P" (or after "T"): "P" and "PT" alone don't
|
|
105
|
+
// reduce to any of the three duration alternatives. Weeks are a separate
|
|
106
|
+
// top-level alternative and cannot combine with the other units.
|
|
107
|
+
// Each alternative's trailing unit is truly optional only by nesting
|
|
108
|
+
// inside the *preceding* unit's suffix (dur-year's [dur-month] contains
|
|
109
|
+
// dur-month's own [dur-day]) — so "D" without a preceding "M" is valid
|
|
110
|
+
// only when there's no "Y" either (dur-day standing alone), never as
|
|
111
|
+
// "Y...D" skipping "M". Mirroring that nesting directly (rather than three
|
|
112
|
+
// independently-optional groups) is what makes P1Y2D correctly rejected.
|
|
113
|
+
const DUR_DATE = /^\d+D$|^\d+M(\d+D)?$|^\d+Y(\d+M(\d+D)?)?$/;
|
|
114
|
+
const DUR_TIME = /^\d+S$|^\d+M(\d+S)?$|^\d+H(\d+M(\d+S)?)?$/;
|
|
115
|
+
function isDuration(value) {
|
|
116
|
+
if (!value.startsWith("P"))
|
|
117
|
+
return false;
|
|
118
|
+
const body = value.slice(1);
|
|
119
|
+
if (body === "")
|
|
120
|
+
return false;
|
|
121
|
+
if (/^\d+W$/.test(body))
|
|
122
|
+
return true; // dur-week, no combining
|
|
123
|
+
const tIndex = body.indexOf("T");
|
|
124
|
+
const datePart = tIndex === -1 ? body : body.slice(0, tIndex);
|
|
125
|
+
const timePart = tIndex === -1 ? "" : body.slice(tIndex + 1);
|
|
126
|
+
if (tIndex !== -1 && timePart === "")
|
|
127
|
+
return false; // "PT" with nothing after
|
|
128
|
+
if (datePart === "" && timePart === "")
|
|
129
|
+
return false; // "P" alone
|
|
130
|
+
if (datePart !== "" && !DUR_DATE.test(datePart))
|
|
131
|
+
return false;
|
|
132
|
+
if (timePart !== "" && !DUR_TIME.test(timePart))
|
|
133
|
+
return false;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
export const duration = {
|
|
137
|
+
test: (value) => typeof value === "string" && isDuration(value),
|
|
138
|
+
};
|
|
139
|
+
// RFC 1123 §2.1 relaxes RFC 952's "must start with a letter" to allow
|
|
140
|
+
// leading digits, but the label/hyphen/length rules are unchanged: each
|
|
141
|
+
// label is 1-63 alphanumerics-or-hyphens, no leading/trailing hyphen, and
|
|
142
|
+
// the assembled name is at most 253 octets (excluding a trailing root dot,
|
|
143
|
+
// which this format doesn't accept anyway per the suite).
|
|
144
|
+
const HOSTNAME_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
145
|
+
function isHostname(value) {
|
|
146
|
+
if (value.length === 0 || value.length > 253)
|
|
147
|
+
return false;
|
|
148
|
+
const labels = value.split(".");
|
|
149
|
+
return labels.every((label) => {
|
|
150
|
+
if (!HOSTNAME_LABEL.test(label))
|
|
151
|
+
return false;
|
|
152
|
+
return /^xn--/i.test(label) ? isValidALabel(label) : true;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export const hostname = {
|
|
156
|
+
test: (value) => typeof value === "string" && isHostname(value),
|
|
157
|
+
};
|
|
158
|
+
// RFC 2673 §3.2 dotted-quad: decbyte "." decbyte "." decbyte "." decbyte
|
|
159
|
+
// where decbyte is 1*3DIGIT syntactically, but JSON Schema's `ipv4` format
|
|
160
|
+
// (per the suite) also forbids leading zeros — "087" reads as octal in
|
|
161
|
+
// some parsers, so each octet must be "0" or a non-zero digit run.
|
|
162
|
+
const IPV4_OCTET = "(0|[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-5])";
|
|
163
|
+
const IPV4_RE = new RegExp(`^${IPV4_OCTET}\\.${IPV4_OCTET}\\.${IPV4_OCTET}\\.${IPV4_OCTET}$`);
|
|
164
|
+
export const ipv4 = {
|
|
165
|
+
test: (value) => typeof value === "string" && IPV4_RE.test(value),
|
|
166
|
+
};
|
|
167
|
+
// RFC 6901 §3: a JSON Pointer is either empty or a sequence of "/"-prefixed
|
|
168
|
+
// reference tokens; "~" is only valid as part of the "~0" (~) or "~1" (/)
|
|
169
|
+
// escape sequences.
|
|
170
|
+
const JSON_POINTER_RE = /^(\/([^~/]|~[01])*)*$/;
|
|
171
|
+
export const jsonPointer = {
|
|
172
|
+
test: (value) => typeof value === "string" && JSON_POINTER_RE.test(value),
|
|
173
|
+
};
|
|
174
|
+
// RFC 6901 relative-json-pointer draft: a non-negative integer (no leading
|
|
175
|
+
// zeros, since a leading zero could only be the single digit "0" itself)
|
|
176
|
+
// followed by either a json-pointer or the "#" index/key marker.
|
|
177
|
+
const RELATIVE_JSON_POINTER_RE = /^(0|[1-9][0-9]*)(#|(\/([^~/]|~[01])*)*)$/;
|
|
178
|
+
export const relativeJsonPointer = {
|
|
179
|
+
test: (value) => typeof value === "string" && RELATIVE_JSON_POINTER_RE.test(value),
|
|
180
|
+
};
|
|
181
|
+
// The `regex` format asserts ECMA-262 pattern validity, and the runtime
|
|
182
|
+
// itself is an ECMA-262 engine — `new RegExp` succeeding/throwing IS the
|
|
183
|
+
// authoritative check, not a stand-in for one. Plain (non-`u`) mode is
|
|
184
|
+
// sufficient here: the suite's only invalid case is unclosed parens
|
|
185
|
+
// (rejected in any mode), so there is no case forcing the `u`-mode retry
|
|
186
|
+
// core's `schemaRegExp` uses for `\p{...}` property escapes.
|
|
187
|
+
function isRegex(value) {
|
|
188
|
+
try {
|
|
189
|
+
new RegExp(value);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export const regex = {
|
|
197
|
+
test: (value) => typeof value === "string" && isRegex(value),
|
|
198
|
+
};
|
|
199
|
+
// RFC 3986 §3 URI grammar, generalized for §2 percent-encoding and the
|
|
200
|
+
// unreserved/sub-delims/pchar character classes. Implemented as a small
|
|
201
|
+
// hand-rolled parser (rather than one grammar-sized regex) because the
|
|
202
|
+
// production nesting (authority host-forms, path-forms keyed on whether a
|
|
203
|
+
// scheme/authority preceded them) is easier to get right — and to keep
|
|
204
|
+
// correct under later IRI generalization (D-block below) — as explicit
|
|
205
|
+
// steps than as regex alternation.
|
|
206
|
+
//
|
|
207
|
+
// pct-encoded = "%" HEXDIG HEXDIG
|
|
208
|
+
const PCT_ENCODED = /^%[0-9a-fA-F]{2}/;
|
|
209
|
+
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
|
210
|
+
const UNRESERVED = /[A-Za-z0-9\-._~]/;
|
|
211
|
+
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
|
|
212
|
+
const SUB_DELIMS = /[!$&'()*+,;=]/;
|
|
213
|
+
/**
|
|
214
|
+
* Consumes the longest prefix of `s` matching the repeated class
|
|
215
|
+
* "unreserved / pct-encoded / sub-delims / extra" (a generalization
|
|
216
|
+
* covering pchar when extra is the two characters colon and at-sign,
|
|
217
|
+
* userinfo when extra is just colon, and reg-name/query/fragment
|
|
218
|
+
* variants) plus — via `extraChar`, an additional single-codepoint
|
|
219
|
+
* predicate — the IRI ucschar/iprivate extension. Returns the number of
|
|
220
|
+
* consumed UTF-16 code units, or -1 on hitting a character/escape that
|
|
221
|
+
* doesn't fit.
|
|
222
|
+
*/
|
|
223
|
+
function consumePctEncodedClass(s, extra, extraChar) {
|
|
224
|
+
let i = 0;
|
|
225
|
+
while (i < s.length) {
|
|
226
|
+
const rest = s.slice(i);
|
|
227
|
+
const pct = PCT_ENCODED.exec(rest);
|
|
228
|
+
if (pct !== null) {
|
|
229
|
+
i += 3;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const ch = s[i];
|
|
233
|
+
if (UNRESERVED.test(ch) || SUB_DELIMS.test(ch) || extra.includes(ch)) {
|
|
234
|
+
i += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (extraChar !== undefined) {
|
|
238
|
+
const cp = s.codePointAt(i);
|
|
239
|
+
if (extraChar(cp)) {
|
|
240
|
+
i += cp > 0xffff ? 2 : 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return i === 0 ? -1 : i;
|
|
245
|
+
}
|
|
246
|
+
return i;
|
|
247
|
+
}
|
|
248
|
+
/** True if the whole string is consumed by {@link consumePctEncodedClass}. */
|
|
249
|
+
function isWholeClass(s, extra, extraChar) {
|
|
250
|
+
if (s.length === 0)
|
|
251
|
+
return true;
|
|
252
|
+
const n = consumePctEncodedClass(s, extra, extraChar);
|
|
253
|
+
return n === s.length;
|
|
254
|
+
}
|
|
255
|
+
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
|
256
|
+
const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+\-.]*$/;
|
|
257
|
+
/**
|
|
258
|
+
* Splits into scheme/authority/path/query/fragment without validating
|
|
259
|
+
* component contents (RFC 3986 Appendix B's parsing regex, restated as
|
|
260
|
+
* explicit scanning). Returns undefined only for the impossible case of
|
|
261
|
+
* a "://" with no scheme delimiter reachable — in practice this always
|
|
262
|
+
* succeeds structurally; content validation happens in the caller.
|
|
263
|
+
*/
|
|
264
|
+
function splitUriReference(value) {
|
|
265
|
+
let rest = value;
|
|
266
|
+
let fragment;
|
|
267
|
+
const hashIndex = rest.indexOf("#");
|
|
268
|
+
if (hashIndex !== -1) {
|
|
269
|
+
fragment = rest.slice(hashIndex + 1);
|
|
270
|
+
rest = rest.slice(0, hashIndex);
|
|
271
|
+
}
|
|
272
|
+
let query;
|
|
273
|
+
const qIndex = rest.indexOf("?");
|
|
274
|
+
if (qIndex !== -1) {
|
|
275
|
+
query = rest.slice(qIndex + 1);
|
|
276
|
+
rest = rest.slice(0, qIndex);
|
|
277
|
+
}
|
|
278
|
+
// A scheme is present only when a ":" appears before any "/", "?", or
|
|
279
|
+
// "#" and what precedes it matches the scheme grammar (this also keeps
|
|
280
|
+
// "a:b" — a scheme — distinct from a relative path segment containing
|
|
281
|
+
// ":", which RFC 3986 §3.3 forbids in the first segment of a
|
|
282
|
+
// scheme-less relative-ref).
|
|
283
|
+
let scheme;
|
|
284
|
+
const colonIndex = rest.indexOf(":");
|
|
285
|
+
if (colonIndex > 0) {
|
|
286
|
+
const candidate = rest.slice(0, colonIndex);
|
|
287
|
+
if (SCHEME_RE.test(candidate)) {
|
|
288
|
+
scheme = candidate;
|
|
289
|
+
rest = rest.slice(colonIndex + 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let authority;
|
|
293
|
+
if (rest.startsWith("//")) {
|
|
294
|
+
rest = rest.slice(2);
|
|
295
|
+
const slashIndex = rest.search(/[/]/);
|
|
296
|
+
if (slashIndex === -1) {
|
|
297
|
+
authority = rest;
|
|
298
|
+
rest = "";
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
authority = rest.slice(0, slashIndex);
|
|
302
|
+
rest = rest.slice(slashIndex);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { scheme, authority, path: rest, query, fragment };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* IPv4address for the host production (RFC 3986 §3.2.2), distinct from
|
|
309
|
+
* the standalone `ipv4` format: a URI host that fails this still parses
|
|
310
|
+
* as a valid `reg-name` (unreserved/pct-encoded/sub-delims), which is why
|
|
311
|
+
* "http://999.999.999.999/" and "http://087.10.0.1/" are structurally
|
|
312
|
+
* valid URIs per the suite even though neither is a valid `ipv4`.
|
|
313
|
+
*/
|
|
314
|
+
const URI_IPV4_RE = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}$/;
|
|
315
|
+
/**
|
|
316
|
+
* IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) — an
|
|
317
|
+
* escape hatch for host address formats beyond IPv4/IPv6 that the suite
|
|
318
|
+
* doesn't exercise but the grammar admits.
|
|
319
|
+
*/
|
|
320
|
+
const IPVFUTURE_RE = /^[vV][0-9a-fA-F]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+$/;
|
|
321
|
+
function isValidHost(host, regNameExtraChar) {
|
|
322
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
323
|
+
const inner = host.slice(1, -1);
|
|
324
|
+
return isIpv6(inner) || IPVFUTURE_RE.test(inner);
|
|
325
|
+
}
|
|
326
|
+
// host = IP-literal / IPv4address / reg-name — IPv4address is tried
|
|
327
|
+
// first only in the sense that reg-name is the universal fallback;
|
|
328
|
+
// either way an IPv4-shaped reg-name is accepted (see URI_IPV4_RE doc).
|
|
329
|
+
if (URI_IPV4_RE.test(host))
|
|
330
|
+
return true;
|
|
331
|
+
return isWholeClass(host, "", regNameExtraChar);
|
|
332
|
+
}
|
|
333
|
+
function isValidAuthority(authority, extraChar) {
|
|
334
|
+
let rest = authority;
|
|
335
|
+
const at = rest.lastIndexOf("@");
|
|
336
|
+
if (at !== -1) {
|
|
337
|
+
const userinfo = rest.slice(0, at);
|
|
338
|
+
if (!isWholeClass(userinfo, ":", extraChar))
|
|
339
|
+
return false;
|
|
340
|
+
rest = rest.slice(at + 1);
|
|
341
|
+
}
|
|
342
|
+
// host may be an IP-literal ("[...]"), which can itself contain ":" —
|
|
343
|
+
// so the port separator must be sought outside any bracketed literal.
|
|
344
|
+
let host = rest;
|
|
345
|
+
let port;
|
|
346
|
+
if (rest.startsWith("[")) {
|
|
347
|
+
const close = rest.indexOf("]");
|
|
348
|
+
if (close === -1)
|
|
349
|
+
return false;
|
|
350
|
+
host = rest.slice(0, close + 1);
|
|
351
|
+
const afterBracket = rest.slice(close + 1);
|
|
352
|
+
if (afterBracket.startsWith(":"))
|
|
353
|
+
port = afterBracket.slice(1);
|
|
354
|
+
else if (afterBracket !== "")
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const colon = rest.indexOf(":");
|
|
359
|
+
if (colon !== -1) {
|
|
360
|
+
host = rest.slice(0, colon);
|
|
361
|
+
port = rest.slice(colon + 1);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (port !== undefined && !/^[0-9]*$/.test(port))
|
|
365
|
+
return false;
|
|
366
|
+
return isValidHost(host, extraChar);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Validates the parsed components of a URI/URI-reference (or, via
|
|
370
|
+
* `extraChar`, an IRI/IRI-reference). `requireScheme` distinguishes `uri`
|
|
371
|
+
* (must be absolute, RFC 3986 §4.3) from `uri-reference` (§4.1, scheme
|
|
372
|
+
* optional).
|
|
373
|
+
*/
|
|
374
|
+
function isValidUriReference(value, requireScheme, extraChar) {
|
|
375
|
+
const { scheme, authority, path, query, fragment } = splitUriReference(value);
|
|
376
|
+
if (requireScheme && scheme === undefined)
|
|
377
|
+
return false;
|
|
378
|
+
if (authority !== undefined && !isValidAuthority(authority, extraChar)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
// path-noscheme's first segment forbids ":" (already enforced by
|
|
382
|
+
// splitUriReference treating a pre-"/" colon as a scheme delimiter
|
|
383
|
+
// whenever it matches scheme grammar); pchar covers the rest.
|
|
384
|
+
if (!isWholeClass(path, "/:@", extraChar))
|
|
385
|
+
return false;
|
|
386
|
+
if (query !== undefined && !isWholeClass(query, "/:@?", extraChar)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (fragment !== undefined && !isWholeClass(fragment, "/:@?", extraChar)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
export const uri = {
|
|
395
|
+
test: (value) => typeof value === "string" && isValidUriReference(value, true),
|
|
396
|
+
};
|
|
397
|
+
export const uriReference = {
|
|
398
|
+
test: (value) => typeof value === "string" && isValidUriReference(value, false),
|
|
399
|
+
};
|
|
400
|
+
// RFC 6570 §2 URI Template grammar:
|
|
401
|
+
// URI-Template = *( literal / expression )
|
|
402
|
+
// expression = "{" [ operator ] variable-list "}"
|
|
403
|
+
// operator = op-level2 / op-level3 / op-reserve
|
|
404
|
+
// op-level2 = "+" / "#"
|
|
405
|
+
// op-level3 = "." / "/" / ";" / "?" / "&"
|
|
406
|
+
// op-reserve = "=" / "," / "!" / "@" / "|"
|
|
407
|
+
// variable-list = varspec *( "," varspec )
|
|
408
|
+
// varspec = varname [ modifier-level4 ]
|
|
409
|
+
// varname = varchar *( ["."] varchar )
|
|
410
|
+
// varchar = ALPHA / DIGIT / "_" / pct-encoded
|
|
411
|
+
// modifier-level4 = prefix / explode
|
|
412
|
+
// prefix = ":" max-length ; max-length = %x31-39 0*3DIGIT
|
|
413
|
+
// explode = "*"
|
|
414
|
+
// `literal` is any character except operator/expression delimiters,
|
|
415
|
+
// backslash, quotes, and control/space; unlike plain URI references it
|
|
416
|
+
// admits raw ucschar/iprivate (this format doesn't distinguish an ASCII
|
|
417
|
+
// vs. internationalized tier the way uri/iri do).
|
|
418
|
+
const VARCHAR = /[A-Za-z0-9_]/;
|
|
419
|
+
function isValidVarname(varname) {
|
|
420
|
+
if (varname === "")
|
|
421
|
+
return false;
|
|
422
|
+
const parts = varname.split(".");
|
|
423
|
+
// "." only ever separates two varchar runs — a leading/trailing/doubled
|
|
424
|
+
// "." would produce an empty part here.
|
|
425
|
+
return parts.every((part) => {
|
|
426
|
+
if (part === "")
|
|
427
|
+
return false;
|
|
428
|
+
let i = 0;
|
|
429
|
+
while (i < part.length) {
|
|
430
|
+
if (VARCHAR.test(part[i])) {
|
|
431
|
+
i += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const pct = PCT_ENCODED.exec(part.slice(i));
|
|
435
|
+
if (pct === null)
|
|
436
|
+
return false;
|
|
437
|
+
i += 3;
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function isValidVarspec(varspec) {
|
|
443
|
+
const prefixMatch = /^(.*):([1-9][0-9]{0,3})$/.exec(varspec);
|
|
444
|
+
if (prefixMatch !== null)
|
|
445
|
+
return isValidVarname(prefixMatch[1]);
|
|
446
|
+
if (varspec.endsWith("*"))
|
|
447
|
+
return isValidVarname(varspec.slice(0, -1));
|
|
448
|
+
return isValidVarname(varspec);
|
|
449
|
+
}
|
|
450
|
+
const OPERATORS = new Set([
|
|
451
|
+
"+",
|
|
452
|
+
"#",
|
|
453
|
+
".",
|
|
454
|
+
"/",
|
|
455
|
+
";",
|
|
456
|
+
"?",
|
|
457
|
+
"&",
|
|
458
|
+
"=",
|
|
459
|
+
",",
|
|
460
|
+
"!",
|
|
461
|
+
"@",
|
|
462
|
+
"|",
|
|
463
|
+
]);
|
|
464
|
+
function isValidExpression(inner) {
|
|
465
|
+
if (inner === "")
|
|
466
|
+
return false;
|
|
467
|
+
const body = OPERATORS.has(inner[0]) ? inner.slice(1) : inner;
|
|
468
|
+
if (body === "")
|
|
469
|
+
return false;
|
|
470
|
+
return body.split(",").every(isValidVarspec);
|
|
471
|
+
}
|
|
472
|
+
// literal excludes: SP DQUOTE "'" "%" (unless a valid pct-encoded) "<" ">"
|
|
473
|
+
// "\" "^" "`" "{" "|" "}" and C0/DEL control characters (RFC 6570 §2.1's
|
|
474
|
+
// ucschar/iprivate carve-outs are additive on top of this base set — the
|
|
475
|
+
// IRI-facing `extraChar` predicate below covers those, same shared-helper
|
|
476
|
+
// shape as the URI/IRI generalization above).
|
|
477
|
+
// The control-character range is spelled with explicit hex escapes (rather
|
|
478
|
+
// than source control bytes) so the pattern stays legible in an editor.
|
|
479
|
+
// eslint-disable-next-line no-control-regex -- RFC 6570's excluded set is C0/DEL by definition
|
|
480
|
+
const LITERAL_EXCLUDED = /["'%<>\\^`{|}\x00-\x1f\x7f ]/;
|
|
481
|
+
function isValidLiteralRun(s, extraChar) {
|
|
482
|
+
let i = 0;
|
|
483
|
+
while (i < s.length) {
|
|
484
|
+
const ch = s[i];
|
|
485
|
+
if (ch === "%") {
|
|
486
|
+
const pct = PCT_ENCODED.exec(s.slice(i));
|
|
487
|
+
if (pct === null)
|
|
488
|
+
return false;
|
|
489
|
+
i += 3;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!LITERAL_EXCLUDED.test(ch)) {
|
|
493
|
+
i += 1;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (extraChar !== undefined) {
|
|
497
|
+
const cp = s.codePointAt(i);
|
|
498
|
+
if (extraChar(cp)) {
|
|
499
|
+
i += cp > 0xffff ? 2 : 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
function isUriTemplate(value, extraChar) {
|
|
508
|
+
let i = 0;
|
|
509
|
+
while (i < value.length) {
|
|
510
|
+
const open = value.indexOf("{", i);
|
|
511
|
+
if (open === -1)
|
|
512
|
+
return isValidLiteralRun(value.slice(i), extraChar);
|
|
513
|
+
if (!isValidLiteralRun(value.slice(i, open), extraChar))
|
|
514
|
+
return false;
|
|
515
|
+
const close = value.indexOf("}", open);
|
|
516
|
+
if (close === -1)
|
|
517
|
+
return false; // unclosed brace
|
|
518
|
+
if (!isValidExpression(value.slice(open + 1, close)))
|
|
519
|
+
return false;
|
|
520
|
+
i = close + 1;
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
export const uriTemplate = {
|
|
525
|
+
test: (value) => typeof value === "string" && isUriTemplate(value),
|
|
526
|
+
};
|
|
527
|
+
// RFC 5321 §4.1.2 Mailbox grammar:
|
|
528
|
+
// Mailbox = Local-part "@" ( Domain / address-literal )
|
|
529
|
+
// Local-part = Dot-string / Quoted-string
|
|
530
|
+
// Dot-string = Atom *("." Atom)
|
|
531
|
+
// Atom = 1*atext
|
|
532
|
+
// atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
533
|
+
// / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{"
|
|
534
|
+
// / "|" / "}" / "~"
|
|
535
|
+
// Quoted-string = DQUOTE *QcontentSMTP DQUOTE
|
|
536
|
+
// QcontentSMTP = qtextSMTP / quoted-pairSMTP
|
|
537
|
+
// qtextSMTP = %d32-33 / %d35-91 / %d93-126 ; printable US-ASCII, no
|
|
538
|
+
// ; unescaped '"' or '\'
|
|
539
|
+
// quoted-pairSMTP = %d92 %d32-126
|
|
540
|
+
// Local-part's Dot-string disallows leading/trailing/doubled "." because
|
|
541
|
+
// each Atom is 1*atext with no embedded ".": the dots are pure separators
|
|
542
|
+
// between non-empty atoms, so an empty atom (adjacent dots or a dot at
|
|
543
|
+
// either end) is a structural rejection, not a special case.
|
|
544
|
+
const ATEXT = /^[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]+$/;
|
|
545
|
+
function isDotString(localPart) {
|
|
546
|
+
if (localPart === "")
|
|
547
|
+
return false;
|
|
548
|
+
return localPart.split(".").every((atom) => ATEXT.test(atom));
|
|
549
|
+
}
|
|
550
|
+
function isQuotedString(localPart) {
|
|
551
|
+
if (localPart.length < 2)
|
|
552
|
+
return false;
|
|
553
|
+
if (!localPart.startsWith('"') || !localPart.endsWith('"')) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
const inner = localPart.slice(1, -1);
|
|
557
|
+
let i = 0;
|
|
558
|
+
while (i < inner.length) {
|
|
559
|
+
const code = inner.codePointAt(i);
|
|
560
|
+
if (code === 0x5c) {
|
|
561
|
+
// quoted-pairSMTP: "\" followed by any printable ASCII (32-126).
|
|
562
|
+
const next = inner.codePointAt(i + 1);
|
|
563
|
+
if (next === undefined || next < 32 || next > 126)
|
|
564
|
+
return false;
|
|
565
|
+
i += 2;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
// qtextSMTP: printable ASCII except '"' (34) and '\' (92).
|
|
569
|
+
if (code < 32 || code > 126 || code === 0x22)
|
|
570
|
+
return false;
|
|
571
|
+
i += 1;
|
|
572
|
+
}
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* RFC 5321 §4.1.3 address-literal contents (without the surrounding
|
|
577
|
+
* "[" "]", already stripped by the caller): IPv4-address-literal is the
|
|
578
|
+
* plain `ipv4` grammar; IPv6-address-literal is "IPv6:" plus the `ipv6`
|
|
579
|
+
* grammar. General-address-literal (other tags) isn't exercised by the
|
|
580
|
+
* suite and is intentionally not accepted here.
|
|
581
|
+
*/
|
|
582
|
+
function isAddressLiteral(inner) {
|
|
583
|
+
if (inner.startsWith("IPv6:"))
|
|
584
|
+
return isIpv6(inner.slice(5));
|
|
585
|
+
return IPV4_RE.test(inner);
|
|
586
|
+
}
|
|
587
|
+
function isValidEmailDomain(domain) {
|
|
588
|
+
if (domain.startsWith("[") && domain.endsWith("]")) {
|
|
589
|
+
return isAddressLiteral(domain.slice(1, -1));
|
|
590
|
+
}
|
|
591
|
+
return isHostname(domain);
|
|
592
|
+
}
|
|
593
|
+
function isEmail(value) {
|
|
594
|
+
// The local part ends at the last unquoted "@"; a quoted local part can
|
|
595
|
+
// itself contain "@" (RFC 5321's qtextSMTP admits it), so splitting on
|
|
596
|
+
// the first "@" would wrongly truncate `"joe@bloggs"@example.com`.
|
|
597
|
+
let localPart;
|
|
598
|
+
let domain;
|
|
599
|
+
if (value.startsWith('"')) {
|
|
600
|
+
// Find the closing quote, respecting quoted-pairSMTP escapes, then
|
|
601
|
+
// the domain is whatever follows "@" immediately after it.
|
|
602
|
+
let i = 1;
|
|
603
|
+
while (i < value.length && value[i] !== '"') {
|
|
604
|
+
i += value[i] === "\\" ? 2 : 1;
|
|
605
|
+
}
|
|
606
|
+
if (i >= value.length || value[i + 1] !== "@")
|
|
607
|
+
return false;
|
|
608
|
+
localPart = value.slice(0, i + 1);
|
|
609
|
+
domain = value.slice(i + 2);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const at = value.lastIndexOf("@");
|
|
613
|
+
if (at === -1)
|
|
614
|
+
return false;
|
|
615
|
+
localPart = value.slice(0, at);
|
|
616
|
+
domain = value.slice(at + 1);
|
|
617
|
+
}
|
|
618
|
+
if (domain === "")
|
|
619
|
+
return false;
|
|
620
|
+
const validLocal = localPart.startsWith('"')
|
|
621
|
+
? isQuotedString(localPart)
|
|
622
|
+
: isDotString(localPart);
|
|
623
|
+
return validLocal && isValidEmailDomain(domain);
|
|
624
|
+
}
|
|
625
|
+
export const email = {
|
|
626
|
+
test: (value) => typeof value === "string" && isEmail(value),
|
|
627
|
+
};
|
|
628
|
+
// RFC 3987 §2.2 IRI grammar: RFC 3986's URI grammar with `ucschar` and
|
|
629
|
+
// `iprivate` admitted alongside ASCII wherever `unreserved` is admitted in
|
|
630
|
+
// URI (host reg-name, userinfo, pchar-derived path/query/fragment) — query
|
|
631
|
+
// additionally admits `iprivate`. `ucschar` is (in RFC 3987's block-listed
|
|
632
|
+
// form) most of the non-ASCII Unicode range excluding surrogates,
|
|
633
|
+
// noncharacters, and a handful of reserved blocks; JS Unicode regex
|
|
634
|
+
// classes make the surrogate/noncharacter exclusions the practical way to
|
|
635
|
+
// state it without hand-copying RFC 3987's forty-odd range triples.
|
|
636
|
+
const IPRIVATE_RANGES = [
|
|
637
|
+
[0xe000, 0xf8ff],
|
|
638
|
+
[0xf0000, 0xffffd],
|
|
639
|
+
[0x100000, 0x10fffd],
|
|
640
|
+
];
|
|
641
|
+
function isIprivate(cp) {
|
|
642
|
+
return IPRIVATE_RANGES.some(([lo, hi]) => cp >= lo && cp <= hi);
|
|
643
|
+
}
|
|
644
|
+
// ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF / %x10000-1FFFD / ...
|
|
645
|
+
// (one contiguous supplementary-plane range per plane through 10). Encoded
|
|
646
|
+
// as explicit ranges (rather than trying to express "surrogates and
|
|
647
|
+
// noncharacters excluded" as a single predicate) so it matches the RFC
|
|
648
|
+
// text directly and stays auditable against it.
|
|
649
|
+
const UCSCHAR_BMP_RANGES = [
|
|
650
|
+
[0xa0, 0xd7ff],
|
|
651
|
+
[0xf900, 0xfdcf],
|
|
652
|
+
[0xfdf0, 0xfffd],
|
|
653
|
+
];
|
|
654
|
+
function isUcschar(cp) {
|
|
655
|
+
if (UCSCHAR_BMP_RANGES.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
if (cp < 0x10000)
|
|
659
|
+
return false;
|
|
660
|
+
// Supplementary planes 1-14 (0x10000-0xDFFFD), each plane's last two
|
|
661
|
+
// code points (...FFFE/...FFFF) are noncharacters and excluded, mirroring
|
|
662
|
+
// the BMP exclusion of FFFE/FFFF via the 0xFFEF upper bound above.
|
|
663
|
+
const plane = cp >>> 16;
|
|
664
|
+
if (plane < 1 || plane > 14)
|
|
665
|
+
return false;
|
|
666
|
+
const inPlane = cp & 0xffff;
|
|
667
|
+
return inPlane <= 0xfffd;
|
|
668
|
+
}
|
|
669
|
+
const isIriExtraChar = (cp) => isUcschar(cp) || isIprivate(cp);
|
|
670
|
+
function isValidIriReference(value, requireScheme) {
|
|
671
|
+
return isValidUriReference(value, requireScheme, isIriExtraChar);
|
|
672
|
+
}
|
|
673
|
+
export const iri = {
|
|
674
|
+
test: (value) => typeof value === "string" && isValidIriReference(value, true),
|
|
675
|
+
};
|
|
676
|
+
export const iriReference = {
|
|
677
|
+
test: (value) => typeof value === "string" && isValidIriReference(value, false),
|
|
678
|
+
};
|
|
679
|
+
// RFC 3339 §5.6:
|
|
680
|
+
// full-date = date-fullyear "-" date-month "-" date-mday
|
|
681
|
+
// full-time = partial-time time-offset
|
|
682
|
+
// partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
|
|
683
|
+
// time-offset = "Z" / time-numoffset
|
|
684
|
+
// time-numoffset = ("+" / "-") time-hour ":" time-minute
|
|
685
|
+
// date-time = full-date "T" full-time
|
|
686
|
+
// time-second's grammar allows "60" for a positive leap second, but RFC
|
|
687
|
+
// 3339 §5.7 restricts *actual* leap seconds to the UTC instant 23:59:60 —
|
|
688
|
+
// so a local time with a ":60" second is only valid when the offset shift
|
|
689
|
+
// back to UTC lands exactly on 23:59. "T"/"Z" are case-insensitive per
|
|
690
|
+
// §5.6's ABNF note.
|
|
691
|
+
const PARTIAL_TIME_RE = /^([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$/;
|
|
692
|
+
function isValidTimeOffset(hour, minute, offset) {
|
|
693
|
+
if (offset === "Z" || offset === "z") {
|
|
694
|
+
return { ok: true, isLeapCandidate: true, utcMinutes: hour * 60 + minute };
|
|
695
|
+
}
|
|
696
|
+
const sign = offset.startsWith("+") ? 1 : -1;
|
|
697
|
+
const offHour = Number(offset.slice(1, 3));
|
|
698
|
+
const offMinute = Number(offset.slice(4, 6));
|
|
699
|
+
if (offHour > 23 || offMinute > 59) {
|
|
700
|
+
return { ok: false, isLeapCandidate: false, utcMinutes: 0 };
|
|
701
|
+
}
|
|
702
|
+
const localMinutes = hour * 60 + minute;
|
|
703
|
+
const utcMinutes = (((localMinutes - sign * (offHour * 60 + offMinute)) % 1440) + 1440) % 1440;
|
|
704
|
+
return { ok: true, isLeapCandidate: true, utcMinutes };
|
|
705
|
+
}
|
|
706
|
+
function isValidPartialTime(value) {
|
|
707
|
+
const m = PARTIAL_TIME_RE.exec(value);
|
|
708
|
+
if (m === null)
|
|
709
|
+
return false;
|
|
710
|
+
const hour = Number(m[1]);
|
|
711
|
+
const minute = Number(m[2]);
|
|
712
|
+
const second = Number(m[3]);
|
|
713
|
+
const offset = m[5];
|
|
714
|
+
if (hour > 23 || minute > 59)
|
|
715
|
+
return false;
|
|
716
|
+
const { ok, utcMinutes } = isValidTimeOffset(hour, minute, offset);
|
|
717
|
+
if (!ok)
|
|
718
|
+
return false;
|
|
719
|
+
if (second < 60)
|
|
720
|
+
return true;
|
|
721
|
+
if (second > 60)
|
|
722
|
+
return false;
|
|
723
|
+
// A ":60" second is only real at the UTC instant 23:59:60 — i.e. the
|
|
724
|
+
// local time, shifted to UTC through its offset, must equal 23:59.
|
|
725
|
+
return utcMinutes === 23 * 60 + 59;
|
|
726
|
+
}
|
|
727
|
+
export const time = {
|
|
728
|
+
test: (value) => typeof value === "string" && isValidPartialTime(value),
|
|
729
|
+
};
|
|
730
|
+
function isValidDateTime(value) {
|
|
731
|
+
if (value.length < 20)
|
|
732
|
+
return false;
|
|
733
|
+
const tIndex = 10;
|
|
734
|
+
if (value[tIndex] !== "T" && value[tIndex] !== "t")
|
|
735
|
+
return false;
|
|
736
|
+
const datePart = value.slice(0, tIndex);
|
|
737
|
+
const timePart = value.slice(tIndex + 1);
|
|
738
|
+
return isFullDate(datePart) && isValidPartialTime(timePart);
|
|
739
|
+
}
|
|
740
|
+
export const dateTime = {
|
|
741
|
+
test: (value) => typeof value === "string" && isValidDateTime(value),
|
|
742
|
+
};
|
|
743
|
+
/**
|
|
744
|
+
* The draft 2020-12 format table. 2019-09 shares it; draft-07/06 subsets
|
|
745
|
+
* are derived below. Entries land format-by-format during M7 — each with
|
|
746
|
+
* its optional-suite file green before the next begins.
|
|
747
|
+
*/
|
|
748
|
+
export const FORMATS_2020_12 = {
|
|
749
|
+
uuid,
|
|
750
|
+
ipv6,
|
|
751
|
+
date,
|
|
752
|
+
duration,
|
|
753
|
+
hostname,
|
|
754
|
+
ipv4,
|
|
755
|
+
"json-pointer": jsonPointer,
|
|
756
|
+
"relative-json-pointer": relativeJsonPointer,
|
|
757
|
+
regex,
|
|
758
|
+
uri,
|
|
759
|
+
"uri-reference": uriReference,
|
|
760
|
+
"uri-template": uriTemplate,
|
|
761
|
+
email,
|
|
762
|
+
iri,
|
|
763
|
+
"iri-reference": iriReference,
|
|
764
|
+
"date-time": dateTime,
|
|
765
|
+
time,
|
|
766
|
+
"idn-hostname": idnHostname,
|
|
767
|
+
"idn-email": idnEmail,
|
|
768
|
+
};
|
|
769
|
+
/** 2019-09 shares 2020-12's format list. */
|
|
770
|
+
export const FORMATS_2019_09 = FORMATS_2020_12;
|
|
771
|
+
// draft-07 lacks uuid/duration; draft-06 additionally lacks iri/idn
|
|
772
|
+
// formats. Subset tables are derived once the relevant entries exist.
|
|
773
|
+
const subset = (table, omit) => Object.fromEntries(Object.entries(table).filter(([name]) => !omit.includes(name)));
|
|
774
|
+
/** draft-07 format table (no uuid, no duration). */
|
|
775
|
+
export const FORMATS_DRAFT_07 = subset(FORMATS_2020_12, [
|
|
776
|
+
"uuid",
|
|
777
|
+
"duration",
|
|
778
|
+
]);
|
|
779
|
+
/** draft-06 format table (draft-07's minus iri/iri-reference/idn-*). */
|
|
780
|
+
export const FORMATS_DRAFT_06 = subset(FORMATS_DRAFT_07, [
|
|
781
|
+
"iri",
|
|
782
|
+
"iri-reference",
|
|
783
|
+
"idn-email",
|
|
784
|
+
"idn-hostname",
|
|
785
|
+
]);
|
|
786
|
+
/**
|
|
787
|
+
* draft-04 format table (for the draft-04 dialect package): only date-time,
|
|
788
|
+
* email, hostname, ipv4, ipv6, and uri are defined by the draft-04 spec.
|
|
789
|
+
*/
|
|
790
|
+
export const FORMATS_DRAFT_04 = subset(FORMATS_DRAFT_06, [
|
|
791
|
+
"date",
|
|
792
|
+
"time",
|
|
793
|
+
"json-pointer",
|
|
794
|
+
"relative-json-pointer",
|
|
795
|
+
"regex",
|
|
796
|
+
"uri-reference",
|
|
797
|
+
"uri-template",
|
|
798
|
+
]);
|
|
799
|
+
//# sourceMappingURL=index.js.map
|