ata-validator 0.18.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +67 -0
- package/build.d.ts +28 -0
- package/build.mjs +3 -0
- package/index.browser.mjs +1 -1
- package/index.d.ts +58 -3
- package/index.js +101 -370
- package/index.mjs +1 -1
- package/lib/aot-build.js +30 -1
- package/lib/aot.js +410 -0
- package/lib/error-messages.js +88 -0
- package/lib/native-load.browser.js +8 -0
- package/lib/native-load.js +21 -0
- package/lib/refine.js +68 -0
- package/lib/safe-regex-source.js +8 -0
- package/lib/t.js +149 -0
- package/lib/version.js +10 -0
- package/package.json +14 -5
- package/scripts/regen-safe-regex-source.js +33 -0
- package/t.d.ts +205 -0
- package/t.js +9 -0
- package/t.mjs +6 -0
package/lib/aot.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// All ahead-of-time (AOT) code generation lives here so the default and browser
|
|
4
|
+
// entries can stay free of `fs`, `path`, `__dirname`, and `pkg-prebuilds`.
|
|
5
|
+
// Browser bundles get `lib/aot.browser.js` instead (see package.json `browser`
|
|
6
|
+
// field), which throws if anyone tries to call an AOT function from the browser.
|
|
7
|
+
//
|
|
8
|
+
// Public surface (called by index.js and ata-validator/build):
|
|
9
|
+
// toStandalone(validator)
|
|
10
|
+
// toStandaloneModule(validator, opts)
|
|
11
|
+
// bundle(Validator, schemas, opts)
|
|
12
|
+
// bundleStandalone(Validator, schemas, opts)
|
|
13
|
+
// bundleCompact(Validator, schemas, opts)
|
|
14
|
+
// loadBundle(Validator, mods, schemas, opts)
|
|
15
|
+
|
|
16
|
+
const { compileToJSCodegenWithErrors } = require('./js-compiler');
|
|
17
|
+
const SAFE_REGEX_SOURCE = require('./safe-regex-source');
|
|
18
|
+
|
|
19
|
+
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
20
|
+
// dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
|
|
21
|
+
const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
22
|
+
const len = s.length;
|
|
23
|
+
for (let i = 0; i < len; i++) {
|
|
24
|
+
if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
|
|
25
|
+
let n = 0; for (const _ of s) n++; return n;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return len;
|
|
29
|
+
}`;
|
|
30
|
+
|
|
31
|
+
// The linear-time regex engine, inlined verbatim into standalone output so a
|
|
32
|
+
// compiled module that uses safe `pattern` matchers has no runtime dependency
|
|
33
|
+
// on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
|
|
34
|
+
// at build time (see `scripts/regen-safe-regex-source.js`), so this path has
|
|
35
|
+
// no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
|
|
36
|
+
// strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
|
|
37
|
+
// alias the emitted code calls. The engine has no eval/new Function, so the
|
|
38
|
+
// embed is CSP-safe.
|
|
39
|
+
let _safeRegexEmbed = null;
|
|
40
|
+
function getSafeRegexEmbed() {
|
|
41
|
+
if (_safeRegexEmbed === null) {
|
|
42
|
+
const body = SAFE_REGEX_SOURCE
|
|
43
|
+
.replace(/^'use strict'\s*\n/, '')
|
|
44
|
+
.replace(/\nmodule\.exports[^\n]*\n?/, '\n');
|
|
45
|
+
_safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
|
|
46
|
+
}
|
|
47
|
+
return _safeRegexEmbed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Returns the engine embed when any supplied compiled function references the
|
|
51
|
+
// safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
|
|
52
|
+
// pay zero bytes.
|
|
53
|
+
function safeRePrelude(...fns) {
|
|
54
|
+
return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Standalone pre-compilation ---
|
|
58
|
+
// Generate a JS module string that can be written to a file.
|
|
59
|
+
// On next startup, load with Validator.fromStandalone() -- zero compile time.
|
|
60
|
+
function toStandalone(validator) {
|
|
61
|
+
validator._ensureCompiled();
|
|
62
|
+
const jsFn = validator._jsFn;
|
|
63
|
+
if (!jsFn || !jsFn._source) return null;
|
|
64
|
+
const src = jsFn._source;
|
|
65
|
+
const hybridSrc = jsFn._hybridSource || '';
|
|
66
|
+
const preambleSrc = jsFn._preambleSource || '';
|
|
67
|
+
|
|
68
|
+
// Also capture error function source for zero-compile standalone load
|
|
69
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
70
|
+
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
71
|
+
);
|
|
72
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
73
|
+
|
|
74
|
+
return `// Auto-generated by ata-validator — do not edit
|
|
75
|
+
'use strict';
|
|
76
|
+
${_CP_LEN_SOURCE}
|
|
77
|
+
${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
|
|
78
|
+
const boolFn = function(d) {
|
|
79
|
+
${src}
|
|
80
|
+
};
|
|
81
|
+
const hybridFactory = function(R, E) {
|
|
82
|
+
return function(d) {
|
|
83
|
+
${hybridSrc}
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : 'const errFn = null;'}
|
|
87
|
+
module.exports = { boolFn, hybridFactory, errFn };
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Fully standalone module ---
|
|
92
|
+
// Generates a self-contained module that can be imported directly without
|
|
93
|
+
// pulling in ata-validator at runtime. Browser bundle gets only the
|
|
94
|
+
// generated validator (~2 KB typical) instead of the 165 KB compiler.
|
|
95
|
+
//
|
|
96
|
+
// import { validate, isValid } from './user-validator.mjs'
|
|
97
|
+
// if (isValid(data)) { ... }
|
|
98
|
+
//
|
|
99
|
+
// format: 'esm' | 'cjs'. Default 'esm'.
|
|
100
|
+
// abortEarly: if true, invalid result is a shared stub; smaller output.
|
|
101
|
+
function toStandaloneModule(validator, opts) {
|
|
102
|
+
validator._ensureCompiled();
|
|
103
|
+
const jsFn = validator._jsFn;
|
|
104
|
+
if (!jsFn || !jsFn._source) return null;
|
|
105
|
+
const format = (opts && opts.format) || 'esm';
|
|
106
|
+
const abortEarly = !!(opts && opts.abortEarly);
|
|
107
|
+
const source = !!(opts && opts.source);
|
|
108
|
+
const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
|
|
109
|
+
const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
|
|
110
|
+
const src = jsFn._source;
|
|
111
|
+
|
|
112
|
+
let errCore = '';
|
|
113
|
+
let jsErrFn = null;
|
|
114
|
+
if (!abortEarly) {
|
|
115
|
+
jsErrFn = compileToJSCodegenWithErrors(
|
|
116
|
+
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
117
|
+
null,
|
|
118
|
+
validator._userFormats,
|
|
119
|
+
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
|
|
120
|
+
);
|
|
121
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
122
|
+
if (errSrc) {
|
|
123
|
+
errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Schema-source frames are baked as literals inside each emitted error so
|
|
128
|
+
// consumers don't need a runtime lookup. We still expose the schema file
|
|
129
|
+
// as a sentinel constant when --source is on — handy for introspection
|
|
130
|
+
// and visible in source graphs. With --no-source, the constant is omitted
|
|
131
|
+
// entirely so size budgets and grep-based "is this source-mapped?" checks
|
|
132
|
+
// both work.
|
|
133
|
+
const schemaSourceConst = (source && schemaFile)
|
|
134
|
+
? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
|
|
135
|
+
: '';
|
|
136
|
+
|
|
137
|
+
// Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
|
|
138
|
+
let closureDecls = '';
|
|
139
|
+
if (jsFn._closures && jsFn._closures.length > 0) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
for (const { name, val } of jsFn._closures) {
|
|
142
|
+
if (Array.isArray(val)) {
|
|
143
|
+
lines.push(`const ${name} = ${JSON.stringify(val)};`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (val && val.__ataSafe) {
|
|
147
|
+
lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
|
|
148
|
+
} else if (val instanceof RegExp) {
|
|
149
|
+
const flags = val.flags;
|
|
150
|
+
lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
|
|
151
|
+
} else if (val instanceof Set) {
|
|
152
|
+
lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
|
|
153
|
+
} else if (typeof val === 'function') {
|
|
154
|
+
// new Function('_ppv', body) — extract body from toString()
|
|
155
|
+
const str = val.toString();
|
|
156
|
+
// Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
|
|
157
|
+
const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
|
|
158
|
+
const body = m ? m[1].trim() : str;
|
|
159
|
+
lines.push(`const ${name} = function(_ppv) { ${body} };`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (lines.length) closureDecls = lines.join('\n') + '\n';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
|
|
166
|
+
// runtime emits them before the function). The standalone module must declare
|
|
167
|
+
// them at module scope too, or _fn references undefined names (e.g. _af1_b0).
|
|
168
|
+
const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
|
|
169
|
+
|
|
170
|
+
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
171
|
+
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
172
|
+
// so the standalone module stays self-contained.
|
|
173
|
+
let formatDecls = '';
|
|
174
|
+
if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
|
|
175
|
+
formatDecls = jsFn._formatClosures
|
|
176
|
+
.map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
|
|
177
|
+
.join('\n') + '\n';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const validBody = errCore
|
|
181
|
+
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
182
|
+
: 'return _fn(data) ? VALID : ABORT';
|
|
183
|
+
|
|
184
|
+
const exports = format === 'esm'
|
|
185
|
+
? `export { validate, isValid };\nexport default { validate, isValid };\n`
|
|
186
|
+
: `module.exports = { validate, isValid };\nmodule.exports.default = module.exports;\n`;
|
|
187
|
+
|
|
188
|
+
return `// Auto-generated by ata-validator — do not edit.
|
|
189
|
+
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
190
|
+
'use strict';
|
|
191
|
+
${_CP_LEN_SOURCE}
|
|
192
|
+
${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
193
|
+
const ABORT = Object.freeze({
|
|
194
|
+
valid: false,
|
|
195
|
+
errors: Object.freeze([Object.freeze({
|
|
196
|
+
code: 'ATA9000',
|
|
197
|
+
message: 'validation failed',
|
|
198
|
+
keyword: '__abort_early__',
|
|
199
|
+
path: '',
|
|
200
|
+
})]),
|
|
201
|
+
});
|
|
202
|
+
${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
203
|
+
${src}
|
|
204
|
+
};
|
|
205
|
+
${errCore}function isValid(data) { return _fn(data); }
|
|
206
|
+
function validate(data) { ${validBody}; }
|
|
207
|
+
${exports}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Bundle multiple validators into a single JS file for fast startup.
|
|
211
|
+
// Usage:
|
|
212
|
+
// const bundle = Validator.bundle([schema1, schema2, ...]);
|
|
213
|
+
// fs.writeFileSync('validators.js', bundle);
|
|
214
|
+
// // On startup:
|
|
215
|
+
// const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
|
|
216
|
+
function bundle(Validator, schemas, opts) {
|
|
217
|
+
const parts = schemas.map((schema) => {
|
|
218
|
+
const v = new Validator(schema, opts);
|
|
219
|
+
const standalone = toStandalone(v);
|
|
220
|
+
if (!standalone) return 'null';
|
|
221
|
+
return (
|
|
222
|
+
'(function(){' +
|
|
223
|
+
standalone
|
|
224
|
+
.replace("'use strict';", '')
|
|
225
|
+
.replace('module.exports = ', 'return ') +
|
|
226
|
+
'})()'
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
return "'use strict';\nmodule.exports = [\n" + parts.join(',\n') + '\n];\n';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
|
|
233
|
+
// opts.format: 'cjs' (default) or 'esm'.
|
|
234
|
+
// opts.formats: { name: fn } — embedded in the output via Function#toString.
|
|
235
|
+
function bundleStandalone(Validator, schemas, opts) {
|
|
236
|
+
// Cross-schema $ref resolution: only meaningful when at least one schema has
|
|
237
|
+
// an $id. Skip the schemas-as-map plumbing when none of them do.
|
|
238
|
+
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
239
|
+
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
240
|
+
const format = (opts && opts.format) || 'cjs';
|
|
241
|
+
const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
|
|
242
|
+
let bundleUsesSafeRe = false;
|
|
243
|
+
const fns = schemas.map((schema) => {
|
|
244
|
+
const v = new Validator(schema, bundleOpts);
|
|
245
|
+
v._ensureCompiled();
|
|
246
|
+
const jsFn = v._jsFn;
|
|
247
|
+
if (!jsFn || !jsFn._hybridSource) return 'null';
|
|
248
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
249
|
+
typeof schema === 'string' ? JSON.parse(schema) : schema,
|
|
250
|
+
v._schemaMap,
|
|
251
|
+
v._userFormats,
|
|
252
|
+
);
|
|
253
|
+
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
254
|
+
const errBody =
|
|
255
|
+
jsErrFn && jsErrFn._errSource
|
|
256
|
+
? jsErrFn._errSource
|
|
257
|
+
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
258
|
+
// Serialize custom format closures so the bundle has no runtime dep on ata.
|
|
259
|
+
let preamble = '';
|
|
260
|
+
if (jsFn._formatClosures) {
|
|
261
|
+
preamble = jsFn._formatClosures
|
|
262
|
+
.map(({ name, fn }) => `var ${name}=${fn.toString()};`)
|
|
263
|
+
.join('\n');
|
|
264
|
+
}
|
|
265
|
+
// Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
|
|
266
|
+
// bundle output is self-contained. `toStandalone` emits this same source
|
|
267
|
+
// for single-schema standalone output.
|
|
268
|
+
if (jsFn._preambleSource) {
|
|
269
|
+
preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
|
|
270
|
+
}
|
|
271
|
+
if (opts && opts.verbose) {
|
|
272
|
+
// Embed the schema and a small resolver so errors carry parentSchema.
|
|
273
|
+
const schemaLit = JSON.stringify(typeof schema === 'string' ? JSON.parse(schema) : schema);
|
|
274
|
+
return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
|
|
275
|
+
}
|
|
276
|
+
return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
277
|
+
});
|
|
278
|
+
const arr = `[${fns.join(',')}]`;
|
|
279
|
+
const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
|
|
280
|
+
if (format === 'esm') {
|
|
281
|
+
return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}const R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
|
|
282
|
+
}
|
|
283
|
+
return `'use strict';\n${safeEmbed}var R=${R};\nmodule.exports=[${fns.join(',')}];\n`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
287
|
+
// Much smaller file → faster V8 parse → faster startup.
|
|
288
|
+
// opts.format: 'cjs' (default) or 'esm'.
|
|
289
|
+
function bundleCompact(Validator, schemas, opts) {
|
|
290
|
+
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
291
|
+
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
292
|
+
const format = (opts && opts.format) || 'cjs';
|
|
293
|
+
let bundleUsesSafeRe = false;
|
|
294
|
+
// Analyze schemas and group by structure
|
|
295
|
+
const entries = schemas.map((schema) => {
|
|
296
|
+
const v = new Validator(schema, bundleOpts);
|
|
297
|
+
v._ensureCompiled();
|
|
298
|
+
const jsFn = v._jsFn;
|
|
299
|
+
if (!jsFn || !jsFn._hybridSource) return null;
|
|
300
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
301
|
+
typeof schema === 'string' ? JSON.parse(schema) : schema,
|
|
302
|
+
v._schemaMap,
|
|
303
|
+
v._userFormats,
|
|
304
|
+
);
|
|
305
|
+
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
306
|
+
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
|
307
|
+
// hybrid body or it references undefined names. Prepending keeps dedup honest:
|
|
308
|
+
// schemas with different branch sets no longer collide on body alone.
|
|
309
|
+
const hybrid = jsFn._preambleSource
|
|
310
|
+
? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
|
|
311
|
+
: jsFn._hybridSource;
|
|
312
|
+
return {
|
|
313
|
+
hybrid,
|
|
314
|
+
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
315
|
+
fmt: jsFn._formatClosures || null,
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Deduplicate function bodies — many schemas produce identical or near-identical code
|
|
320
|
+
const bodyMap = new Map(); // body → index
|
|
321
|
+
const bodies = [];
|
|
322
|
+
const errMap = new Map();
|
|
323
|
+
const errBodies = [];
|
|
324
|
+
|
|
325
|
+
const indices = entries.map((e) => {
|
|
326
|
+
if (!e) return [-1, -1];
|
|
327
|
+
let hi = bodyMap.get(e.hybrid);
|
|
328
|
+
if (hi === undefined) {
|
|
329
|
+
hi = bodies.length;
|
|
330
|
+
bodies.push(e.hybrid);
|
|
331
|
+
bodyMap.set(e.hybrid, hi);
|
|
332
|
+
}
|
|
333
|
+
let ei = -1;
|
|
334
|
+
if (e.err) {
|
|
335
|
+
ei = errMap.get(e.err);
|
|
336
|
+
if (ei === undefined) {
|
|
337
|
+
ei = errBodies.length;
|
|
338
|
+
errBodies.push(e.err);
|
|
339
|
+
errMap.set(e.err, ei);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return [hi, ei];
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Generate compact bundle
|
|
346
|
+
const isEsm = format === 'esm';
|
|
347
|
+
let out = isEsm
|
|
348
|
+
? '// Auto-generated by ata-validator — do not edit\n'
|
|
349
|
+
: "'use strict';\n";
|
|
350
|
+
if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
|
|
351
|
+
const declKW = isEsm ? 'const' : 'var';
|
|
352
|
+
out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
|
|
353
|
+
|
|
354
|
+
// User format functions are referenced as _uf_<name> by the hybrid and error
|
|
355
|
+
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
356
|
+
// Function#toString so the bundle stays self-contained.
|
|
357
|
+
const fmtSeen = new Set();
|
|
358
|
+
for (const e of entries) {
|
|
359
|
+
if (!e || !e.fmt) continue;
|
|
360
|
+
for (const { name, fn } of e.fmt) {
|
|
361
|
+
if (fmtSeen.has(name)) continue;
|
|
362
|
+
fmtSeen.add(name);
|
|
363
|
+
out += `${declKW} ${name}=${fn.toString()};\n`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Shared hybrid factories
|
|
368
|
+
out += `${declKW} H=[\n`;
|
|
369
|
+
out += bodies
|
|
370
|
+
.map((b) => `function(R,E){return function(d){${b}}}`)
|
|
371
|
+
.join(',\n');
|
|
372
|
+
out += '\n];\n';
|
|
373
|
+
|
|
374
|
+
// Shared error functions
|
|
375
|
+
out += `${declKW} EF=[\n`;
|
|
376
|
+
out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(',\n');
|
|
377
|
+
out += '\n];\n';
|
|
378
|
+
|
|
379
|
+
// Build validators from shared templates
|
|
380
|
+
const arrBody = indices
|
|
381
|
+
.map(([hi, ei]) => {
|
|
382
|
+
if (hi < 0) return 'null';
|
|
383
|
+
if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
|
|
384
|
+
return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
|
|
385
|
+
})
|
|
386
|
+
.join(',');
|
|
387
|
+
if (isEsm) {
|
|
388
|
+
out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
|
|
389
|
+
} else {
|
|
390
|
+
out += `module.exports=[${arrBody}];\n`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function loadBundle(Validator, mods, schemas, opts) {
|
|
397
|
+
return schemas.map((schema, i) => {
|
|
398
|
+
if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
|
|
399
|
+
return new Validator(schema, opts);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
toStandalone,
|
|
405
|
+
toStandaloneModule,
|
|
406
|
+
bundle,
|
|
407
|
+
bundleStandalone,
|
|
408
|
+
bundleCompact,
|
|
409
|
+
loadBundle,
|
|
410
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Custom error messages via the `errorMessage` keyword.
|
|
4
|
+
//
|
|
5
|
+
// A subschema may declare an `errorMessage` keyword to override the generated
|
|
6
|
+
// `message` on any error it owns:
|
|
7
|
+
//
|
|
8
|
+
// errorMessage: "single message for any failure of this subschema"
|
|
9
|
+
// errorMessage: {
|
|
10
|
+
// <keyword>: "msg", // e.g. minimum, type, pattern, format
|
|
11
|
+
// required: "msg" | { prop: "msg" },// keyed by missing property, or one string
|
|
12
|
+
// additionalProperties: "msg",
|
|
13
|
+
// _: "fallback msg", // used when no keyword-specific entry matches
|
|
14
|
+
// }
|
|
15
|
+
//
|
|
16
|
+
// Resolution: each error's owning subschema is located from its schemaPath (the
|
|
17
|
+
// keyword is the last pointer segment, the owner is everything before it). If the
|
|
18
|
+
// owner declares an errorMessage, the error's `message` is replaced. Errors are
|
|
19
|
+
// cloned, never mutated, so frozen/shared results stay intact. When no schema in
|
|
20
|
+
// the tree carries an errorMessage the decorator is never installed, so the
|
|
21
|
+
// validate hot path keeps its original cost.
|
|
22
|
+
|
|
23
|
+
// Locate the schema object that owns the failing keyword. Mirrors
|
|
24
|
+
// resolveSchemaByPath() in index.js: the last pointer segment is the keyword,
|
|
25
|
+
// so the owner is the node reached by walking every segment before it.
|
|
26
|
+
function resolveOwner (rootSchema, schemaPath) {
|
|
27
|
+
if (!schemaPath || typeof schemaPath !== 'string' || schemaPath[0] !== '#') return undefined;
|
|
28
|
+
const stripped = schemaPath.slice(1);
|
|
29
|
+
if (!stripped || stripped === '/') return rootSchema;
|
|
30
|
+
const parts = stripped.split('/').filter(Boolean).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
31
|
+
let target = rootSchema;
|
|
32
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
33
|
+
if (target == null || typeof target !== 'object') return undefined;
|
|
34
|
+
target = target[parts[i]];
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pick the override string for a single error from its owner's errorMessage.
|
|
40
|
+
// Returns undefined when nothing matches, leaving the default message in place.
|
|
41
|
+
function pickMessage (em, err) {
|
|
42
|
+
if (em == null) return undefined;
|
|
43
|
+
if (typeof em === 'string') return em;
|
|
44
|
+
if (typeof em !== 'object') return undefined;
|
|
45
|
+
|
|
46
|
+
const kw = err.keyword;
|
|
47
|
+
|
|
48
|
+
if (kw === 'required') {
|
|
49
|
+
const r = em.required;
|
|
50
|
+
if (typeof r === 'string') return r;
|
|
51
|
+
if (r && typeof r === 'object') {
|
|
52
|
+
const prop = err.params && err.params.missingProperty;
|
|
53
|
+
if (prop != null && typeof r[prop] === 'string') return r[prop];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (kw != null && typeof em[kw] === 'string') return em[kw];
|
|
58
|
+
if (typeof em._ === 'string') return em._;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Returns true when the schema string is worth scanning at validate time.
|
|
63
|
+
function schemaHasErrorMessages (schemaStr) {
|
|
64
|
+
return typeof schemaStr === 'string' && schemaStr.indexOf('"errorMessage"') !== -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply overrides across an error array. Returns the same array reference when
|
|
68
|
+
// nothing changed (so callers can cheaply detect a no-op), or a new array with
|
|
69
|
+
// cloned-and-overridden entries.
|
|
70
|
+
function applyErrorMessages (errors, rootSchema) {
|
|
71
|
+
if (!errors || !errors.length) return errors;
|
|
72
|
+
let changed = false;
|
|
73
|
+
const out = new Array(errors.length);
|
|
74
|
+
for (let i = 0; i < errors.length; i++) {
|
|
75
|
+
const err = errors[i];
|
|
76
|
+
out[i] = err;
|
|
77
|
+
if (!err || typeof err.schemaPath !== 'string') continue;
|
|
78
|
+
const owner = resolveOwner(rootSchema, err.schemaPath);
|
|
79
|
+
if (!owner || typeof owner !== 'object') continue;
|
|
80
|
+
const msg = pickMessage(owner.errorMessage, err);
|
|
81
|
+
if (msg == null) continue;
|
|
82
|
+
out[i] = Object.assign({}, err, { message: msg });
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
return changed ? out : errors;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { schemaHasErrorMessages, applyErrorMessages, resolveOwner, pickMessage };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Locate and load the optional native addon (pkg-prebuilds). Lives in its own
|
|
4
|
+
// module so the default and browser entries can stay free of `pkg-prebuilds`,
|
|
5
|
+
// `__dirname`, and `path` references. In browser bundles this file is replaced
|
|
6
|
+
// with `native-load.browser.js` via the package.json `browser` field, so the
|
|
7
|
+
// `pkg-prebuilds` import never enters the bundle in the first place.
|
|
8
|
+
//
|
|
9
|
+
// Returns the loaded native binding object, or `null` if no prebuild matches
|
|
10
|
+
// (musl/Alpine, unsupported arch, devs running before `npm run build`, etc.).
|
|
11
|
+
// The JS codegen path covers all core validation when this returns null.
|
|
12
|
+
|
|
13
|
+
module.exports = function loadNative() {
|
|
14
|
+
try {
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const pkg = require('pkg-prebuilds');
|
|
17
|
+
return pkg(path.join(__dirname, '..'), require('../binding-options'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
package/lib/refine.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Async refinement support for the `/t` builder.
|
|
4
|
+
//
|
|
5
|
+
// JSON Schema is synchronous by design and cannot express a check that needs to
|
|
6
|
+
// await (uniqueness, a remote lookup, cross-field business rules). Those live on
|
|
7
|
+
// the builder via `t.refine(schema, check, opts)`. A refinement is carried on a
|
|
8
|
+
// Symbol key (like the OPTIONAL marker), so it is invisible to Object.keys,
|
|
9
|
+
// JSON.stringify, and ata's codegen: `new Validator(schema)` still performs
|
|
10
|
+
// plain structural validation and ignores refinements entirely.
|
|
11
|
+
//
|
|
12
|
+
// `validateAsync()` (in index.js) runs structural validation first and only
|
|
13
|
+
// awaits the refinements when the value is structurally valid — a refinement
|
|
14
|
+
// body may assume the value already has the right shape.
|
|
15
|
+
|
|
16
|
+
const REFINE = Symbol.for('ata.t.refine');
|
|
17
|
+
|
|
18
|
+
// Pull the refinement list off a schema node, or null when there is none.
|
|
19
|
+
function getRefinements (schema) {
|
|
20
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
21
|
+
const r = schema[REFINE];
|
|
22
|
+
return Array.isArray(r) && r.length ? r : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Attach a refinement to a schema, composing with any already present. Returns a
|
|
26
|
+
// new object; the input is not mutated. Enumerable Symbol keys (OPTIONAL, prior
|
|
27
|
+
// refinements) are copied by the spread, then the refinement list is replaced.
|
|
28
|
+
function attach (schema, check, opts) {
|
|
29
|
+
if (typeof check !== 'function') {
|
|
30
|
+
throw new TypeError('t.refine(schema, check, opts?) — check must be a function');
|
|
31
|
+
}
|
|
32
|
+
const prev = (schema && Array.isArray(schema[REFINE])) ? schema[REFINE] : [];
|
|
33
|
+
const o = opts || {};
|
|
34
|
+
const entry = { check, message: o.message, path: o.path || '' };
|
|
35
|
+
return Object.assign({}, schema, { [REFINE]: prev.concat(entry) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build a single refinement issue in the validator's error shape.
|
|
39
|
+
function issue (entry, message) {
|
|
40
|
+
return {
|
|
41
|
+
keyword: 'refine',
|
|
42
|
+
instancePath: entry.path || '',
|
|
43
|
+
path: entry.path || '',
|
|
44
|
+
schemaPath: '',
|
|
45
|
+
params: {},
|
|
46
|
+
message: message != null ? message : (entry.message || 'value failed refinement'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Evaluate every refinement against `data`, awaiting async checks concurrently.
|
|
51
|
+
// A check that returns falsy — or throws — yields an issue. Returns the issues
|
|
52
|
+
// array (empty when all pass).
|
|
53
|
+
async function runRefinements (refinements, data) {
|
|
54
|
+
const issues = [];
|
|
55
|
+
await Promise.all(refinements.map(async (entry) => {
|
|
56
|
+
let passed;
|
|
57
|
+
try {
|
|
58
|
+
passed = await entry.check(data);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
issues.push(issue(entry, entry.message || (e && e.message) || 'refinement threw'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!passed) issues.push(issue(entry));
|
|
64
|
+
}));
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { REFINE, getRefinements, attach, runRefinements };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated by scripts/regen-safe-regex-source.js — do not edit.
|
|
4
|
+
// Mirrors `lib/safe-regex.js` verbatim so `lib/aot.js` can embed the engine
|
|
5
|
+
// into standalone output without a runtime `fs` read. Kept in sync by
|
|
6
|
+
// `tests/test_safe_regex_source_sync.js`.
|
|
7
|
+
|
|
8
|
+
module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\nfunction matchClass (instr, c) {\n let inside = false\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n return instr.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n const stack = []\n\n function addThread (list, pc, pos, len) {\n stack.length = 0\n stack.push(pc)\n while (stack.length) {\n const p = stack.pop()\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n const I = prog[p]\n switch (I.op) {\n case 'jmp': stack.push(I.x); break\n case 'split': stack.push(I.y); stack.push(I.x); break\n case 'bol': if (pos === 0) stack.push(p + 1); break\n case 'eol': if (pos === len) stack.push(p + 1); break\n default: list.push(p)\n }\n }\n }\n\n return function test (s) {\n const len = s.length\n let clist = []\n let nlist = []\n gen++\n addThread(clist, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlist.length = 0\n for (let k = 0; k < clist.length; k++) {\n const pc = clist[k]\n const I = prog[pc]\n if (I.op === 'match') return true\n else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }\n }\n if (pos < len) addThread(nlist, 0, pos + 1, len)\n const tmp = clist; clist = nlist; nlist = tmp\n }\n return false\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";
|