Files
powerpoint-toolbox/node_modules/@exodus/schemasafe/src/javascript.js
T
2025-03-07 19:22:02 +01:00

171 lines
7.6 KiB
JavaScript

'use strict'
const { format, safe } = require('./safe-format')
const { scopeMethods } = require('./scope-utils')
const functions = require('./scope-functions')
// for building into the validation function
const types = new Map(
Object.entries({
null: (name) => format('%s === null', name),
boolean: (name) => format('typeof %s === "boolean"', name),
array: (name) => format('Array.isArray(%s)', name),
object: (n) => format('typeof %s === "object" && %s && !Array.isArray(%s)', n, n, n),
number: (name) => format('typeof %s === "number"', name),
integer: (name) => format('Number.isInteger(%s)', name),
string: (name) => format('typeof %s === "string"', name),
})
)
const buildName = ({ name, parent, keyval, keyname }) => {
if (name) {
if (parent || keyval || keyname) throw new Error('name can be used only stand-alone')
return name // top-level
}
if (!parent) throw new Error('Can not use property of undefined parent!')
const parentName = buildName(parent)
if (keyval !== undefined) {
if (keyname) throw new Error('Can not use key value and name together')
if (!['string', 'number'].includes(typeof keyval)) throw new Error('Invalid property path')
if (/^[a-z][a-z0-9_]*$/i.test(keyval)) return format('%s.%s', parentName, safe(keyval))
return format('%s[%j]', parentName, keyval)
} else if (keyname) {
return format('%s[%s]', parentName, keyname)
}
/* c8 ignore next */
throw new Error('Unreachable')
}
const jsonProtoKeys = new Set(
[].concat(
...[Object, Array, String, Number, Boolean].map((c) => Object.getOwnPropertyNames(c.prototype))
)
)
const jsHelpers = (fun, scope, propvar, { unmodifiedPrototypes, isJSON }, noopRegExps) => {
const { gensym, genpattern, genloop } = scopeMethods(scope, propvar)
const present = (obj) => {
const name = buildName(obj) // also checks for coherence, do not remove
const { parent, keyval, keyname, inKeys, checked } = obj
/* c8 ignore next */
if (checked || (inKeys && isJSON)) throw new Error('Unreachable: useless check for undefined')
if (inKeys) return format('%s !== undefined', name)
if (parent && keyname) {
scope.hasOwn = functions.hasOwn
const pname = buildName(parent)
if (isJSON) return format('%s !== undefined && hasOwn(%s, %s)', name, pname, keyname)
return format('%s in %s && hasOwn(%s, %s)', keyname, pname, pname, keyname)
} else if (parent && keyval !== undefined) {
// numbers must be converted to strings for this check, hence `${keyval}` in check below
if (unmodifiedPrototypes && isJSON && !jsonProtoKeys.has(`${keyval}`))
return format('%s !== undefined', name)
scope.hasOwn = functions.hasOwn
const pname = buildName(parent)
if (isJSON) return format('%s !== undefined && hasOwn(%s, %j)', name, pname, keyval)
return format('%j in %s && hasOwn(%s, %j)', keyval, pname, pname, keyval)
}
/* c8 ignore next */
throw new Error('Unreachable: present() check without parent')
}
const forObjectKeys = (obj, writeBody) => {
const key = gensym('key')
fun.block(format('for (const %s of Object.keys(%s))', key, buildName(obj)), () => {
writeBody(propvar(obj, key, true), key) // always own property here
})
}
const forArray = (obj, start, writeBody) => {
const i = genloop()
const name = buildName(obj)
fun.block(format('for (let %s = %s; %s < %s.length; %s++)', i, start, i, name, i), () => {
writeBody(propvar(obj, i, unmodifiedPrototypes, true), i) // own property in Array if proto not mangled
})
}
const patternTest = (pat, key) => {
// Convert common patterns to string checks, makes generated code easier to read (and a tiny perf bump)
const r = pat.replace(/[.^$|*+?(){}[\]\\]/gu, '') // Special symbols: .^$|*+?(){}[]\
if (pat === `^${r}$`) return format('(%s === %j)', key, pat.slice(1, -1)) // ^abc$ -> === abc
if (noopRegExps.has(pat)) return format('true') // known noop
// All of the below will cause warnings in enforced string validation mode, but let's make what they actually do more visible
// note that /^.*$/u.test('\n') is false, so don't combine .* with anchors here!
if ([r, `${r}+`, `${r}.*`, `.*${r}.*`].includes(pat)) return format('%s.includes(%j)', key, r)
if ([`^${r}`, `^${r}+`, `^${r}.*`].includes(pat)) return format('%s.startsWith(%j)', key, r)
if ([`${r}$`, `.*${r}$`].includes(pat)) return format('%s.endsWith(%j)', key, r)
const subr = [...r].slice(0, -1).join('') // without the last symbol, astral plane aware
if ([`${r}*`, `${r}?`].includes(pat))
return subr.length === 0 ? format('true') : format('%s.includes(%j)', key, subr) // abc*, abc? -> includes(ab)
if ([`^${r}*`, `^${r}?`].includes(pat))
return subr.length === 0 ? format('true') : format('%s.startsWith(%j)', key, subr) // ^abc*, ^abc? -> startsWith(ab)
// A normal reg-exp test
return format('%s.test(%s)', genpattern(pat), key)
}
const compare = (name, val) => {
if (!val || typeof val !== 'object') return format('%s === %j', name, val)
let type // type is needed for speedup only, deepEqual rechecks that
// small plain object/arrays are fast cases and we inline those instead of calling deepEqual
const shouldInline = (arr) => arr.length <= 3 && arr.every((x) => !x || typeof x !== 'object')
if (Array.isArray(val)) {
type = types.get('array')(name)
if (shouldInline(val)) {
let k = format('%s.length === %d', name, val.length)
for (let i = 0; i < val.length; i++) k = format('%s && %s[%d] === %j', k, name, i, val[i])
return format('%s && %s', type, k)
}
} else {
type = types.get('object')(name)
const [keys, values] = [Object.keys(val), Object.values(val)]
if (shouldInline(values)) {
let k = format('Object.keys(%s).length === %d', name, keys.length)
if (keys.length > 0) scope.hasOwn = functions.hasOwn
for (const key of keys) k = format('%s && hasOwn(%s, %j)', k, name, key)
for (const key of keys) k = format('%s && %s[%j] === %j', k, name, key, val[key])
return format('%s && %s', type, k)
}
}
scope.deepEqual = functions.deepEqual
return format('%s && deepEqual(%s, %j)', type, name, val)
}
return { present, forObjectKeys, forArray, patternTest, compare, propvar }
}
// Stringifcation of functions and regexps, for scope
const isArrowFnWithParensRegex = /^\([^)]*\) *=>/
const isArrowFnWithoutParensRegex = /^[^=]*=>/
const toJayString = Symbol.for('toJayString')
function jaystring(item) {
if (typeof item === 'function') {
if (item[toJayString]) return item[toJayString] // this is supported only for functions
if (Object.getPrototypeOf(item) !== Function.prototype)
throw new Error('Can not stringify: a function with unexpected prototype')
const stringified = `${item}`
if (item.prototype) {
if (!/^function[ (]/.test(stringified)) throw new Error('Unexpected function')
return stringified // normal function
}
if (isArrowFnWithParensRegex.test(stringified) || isArrowFnWithoutParensRegex.test(stringified))
return stringified // Arrow function
// Shortened ES6 object method declaration
throw new Error('Can not stringify: only either normal or arrow functions are supported')
} else if (typeof item === 'object') {
const proto = Object.getPrototypeOf(item)
if (item instanceof RegExp && proto === RegExp.prototype) return format('%r', item)
throw new Error('Can not stringify: an object with unexpected prototype')
}
throw new Error(`Can not stringify: unknown type ${typeof item}`)
}
module.exports = { types, buildName, jsHelpers, jaystring }