UNPKG

auto-parse

Version:

Automatically convert any value to its best matching JavaScript type. Supports numbers, booleans, objects, arrays, BigInt, Symbol, comma-separated numbers, prefix stripping, allowed type enforcement and a plugin API.

542 lines (541 loc) 16.9 kB
// index.js module.exports = autoParse; var plugins = []; var globalOnError = null; function isType(value, type) { if (typeof type === "string") { if (type.toLowerCase() === "array") return Array.isArray(value); if (type.toLowerCase() === "null") return value === null; if (type.toLowerCase() === "undefined") return value === void 0; return typeof value === type.toLowerCase(); } if (type === Array) return Array.isArray(value); if (type === Number) return typeof value === "number" && !Number.isNaN(value); if (type === String) return typeof value === "string"; if (type === Boolean) return typeof value === "boolean"; if (type === Object) return typeof value === "object" && value !== null && !Array.isArray(value); if (type === null) return value === null; return value instanceof type; } function runPlugins(value, type, options) { for (let i = 0; i < plugins.length; i++) { const res = plugins[i](value, type, options); if (res !== void 0) return res; } return void 0; } function getTypeName(value) { if (value === null) return "null"; if (Array.isArray(value)) return "array"; if (value instanceof Date) return "date"; if (value instanceof RegExp) return "regexp"; if (typeof value === "bigint") return "bigint"; if (typeof value === "symbol") return "symbol"; return typeof value; } function returnIfAllowed(val, options, fallback) { if (options && Array.isArray(options.allowedTypes)) { const type = getTypeName(val); if (!options.allowedTypes.includes(type)) { return fallback; } } return val; } autoParse.use = function(fn) { if (typeof fn === "function") plugins.push(fn); }; autoParse.setErrorHandler = function(fn) { globalOnError = typeof fn === "function" ? fn : null; }; var _stripCache = /* @__PURE__ */ new Map(); var QUOTE_RE = /['"]/g; function getStripRegex(chars) { let re = _stripCache.get(chars); if (!re) { const escaped = chars.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); re = new RegExp("^[" + escaped + "]+"); _stripCache.set(chars, re); } return re; } function stripTrimLower(value, options = {}) { if (options.stripStartChars && typeof value === "string") { const chars = Array.isArray(options.stripStartChars) ? options.stripStartChars.join("") : String(options.stripStartChars); value = value.replace(getStripRegex(chars), ""); } return value.replace(QUOTE_RE, "").trim().toLowerCase(); } function toBoolean(value, options) { return checkBoolean(value, options) || false; } function checkBoolean(value, options) { if (!value) { return false; } if (typeof value === "number" || typeof value === "boolean") { return !!value; } value = stripTrimLower(value, options); const extras = options && options.booleanSynonyms; if (value === "true" || value === "1" || extras && (value === "yes" || value === "on")) return true; if (value === "false" || value === "0" || extras && (value === "no" || value === "off")) return false; return null; } function parseObject(value, options) { if (Array.isArray(value)) { return value.map(function(n, key) { return autoParse(n, options); }); } else if (typeof value === "object" || value.constructor === void 0) { for (const n in value) { value[n] = autoParse(value[n], options); } return value; } return {}; } function parseFunction(value, options) { return autoParse(value(), options); } var CURRENCY_SYMBOLS = { "$": "USD", "\u20AC": "EUR", "\xA3": "GBP", "\xA5": "JPY", "A$": "AUD", "C$": "CAD", "CHF": "CHF", "HK$": "HKD", "\u20B9": "INR", "\u20A9": "KRW" }; function parseCurrencyString(str, options) { const symbols = Object.assign({}, CURRENCY_SYMBOLS, options && options.currencySymbols); for (const sym of Object.keys(symbols)) { const re = new RegExp("^" + sym.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&") + "\\s?([0-9]+(?:[.,][0-9]+)?)$"); const m = re.exec(str); if (m) { const num = parseFloat(m[1].replace(",", ".")); if (options && options.currencyAsObject) { return { value: num, currency: symbols[sym] }; } return num; } } return null; } function parsePercentString(str, options) { const m = /^([-+]?\d+(?:\.\d+)?)%$/.exec(str); if (m) { const val = Number(m[1]) / 100; if (options && options.percentAsObject) return { value: val, percent: true }; return val; } return null; } function parseUnitString(str) { if (/^0[box]/i.test(str)) return null; const m = /^(-?\d+(?:\.\d+)?)([a-z%]+)$/i.exec(str); if (m) return { value: Number(m[1]), unit: m[2] }; return null; } function parseRangeString(str, options) { const m = /^(-?\d+(?:\.\d+)?)\s*(?:\.\.|-)\s*(-?\d+(?:\.\d+)?)$/.exec(str); if (m) { const start = Number(m[1]); const end = Number(m[2]); if (options && options.rangeAsObject) return { start, end }; const arr = []; const step = start <= end ? 1 : -1; for (let i = start; step > 0 ? i <= end : i >= end; i += step) arr.push(i); return arr; } return null; } function parseTypedArrayString(str, options) { const m = /^([A-Za-z0-9]+Array)\[(.*)\]$/.exec(str); if (m && typeof globalThis[m[1]] === "function") { const arr = autoParse(`[${m[2]}]`, options); if (Array.isArray(arr)) return new globalThis[m[1]](arr); } return null; } function parseMapSetString(str, options) { if (/^Map:/i.test(str)) { const inner = str.slice(4).trim(); const arr = autoParse(inner, options); return new Map(arr); } if (/^Set:/i.test(str)) { const inner = str.slice(4).trim(); const arr = autoParse(inner, options); return new Set(arr); } return null; } function parseDateTimeString(str) { const iso = /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/; if (iso.test(str)) { const d = new Date(str); if (!Number.isNaN(d.getTime())) return d; } let m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?)?$/i.exec(str); if (m) { let [, month, day, year, h, min, sec, ap] = m; const date = new Date(Number(year), Number(month) - 1, Number(day)); if (h !== void 0) { h = Number(h); if (ap) { ap = ap.toLowerCase(); if (ap === "pm" && h < 12) h += 12; if (ap === "am" && h === 12) h = 0; } date.setHours(h, Number(min), Number(sec || 0), 0); } return date; } m = /^(\d{1,2})-(\d{1,2})-(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/.exec(str); if (m) { const [, day, month, year, h, min, sec] = m; const date = new Date(Number(year), Number(month) - 1, Number(day)); if (h !== void 0) { date.setHours(Number(h), Number(min), Number(sec || 0), 0); } return date; } m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\s*([AP]M))?$/i.exec(str); if (m) { let [, h, min, sec, ap] = m; h = Number(h); if (ap) { ap = ap.toLowerCase(); if (ap === "pm" && h < 12) h += 12; if (ap === "am" && h === 12) h = 0; } const date = /* @__PURE__ */ new Date(); date.setHours(h, Number(min), Number(sec || 0), 0); return date; } return null; } function parseUrlString(str) { try { return new URL(str); } catch (e) { return null; } } function parseFilePathString(str) { const re = /^(?:[A-Za-z]:[\\/]|\\\\|\.{1,2}[\\/]|~[\\/]|\/)/; if (re.test(str)) { return str.replace(/\\+/g, "/").replace(/\/+/g, "/"); } return null; } function parseExpressionString(str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { return Function("return (" + str + ")")(); } catch (e) { } } return null; } function parseFunctionString(str) { if (/^\s*(\(?\w*\)?\s*=>)/.test(str)) { try { return Function("return (" + str + ")")(); } catch (e) { } } return null; } function expandEnvVars(str) { return str.replace(/\$([A-Z0-9_]+)/gi, function(m, name) { return process.env[name] || ""; }); } function parseType(value, type, options = {}) { let typeName = type; try { if (value && value.constructor === type || isType(value, type) && typeName !== "object" && typeName !== "array") { return value; } if (type && type.name) { typeName = type.name.toLowerCase(); } typeName = stripTrimLower(typeName); switch (typeName) { case "string": if (typeof value === "object") return JSON.stringify(value); return String(value); case "function": if (isType(value, Function)) { return value; } return function(cb) { if (typeof cb === "function") { cb(value); } return value; }; case "date": return new Date(value); case "object": let jsonParsed; if (typeof value === "string" && /^['"]?[[{]/.test(value.trim())) { try { jsonParsed = JSON.parse(value); } catch (e) { } } if (isType(jsonParsed, Object) || isType(jsonParsed, Array)) { return autoParse(jsonParsed, options); } else if (!isType(jsonParsed, "undefined")) { return {}; } return parseObject(value, options); case "boolean": return toBoolean(value, options); case "number": if (options.parseCommaNumbers && typeof value === "string") { const normalized = value.replace(/,/g, ""); if (!Number.isNaN(Number(normalized))) return Number(normalized); } return Number(value); case "bigint": return BigInt(value); case "symbol": return Symbol(value); case "undefined": return void 0; case "null": return null; case "array": return [value]; case "map": return new Map(autoParse(value, options)); case "set": return new Set(autoParse(value, options)); case "url": return new URL(value); case "path": case "filepath": return parseFilePathString(String(value)) || String(value); default: if (typeof type === "function") { if (/Array$/.test(type.name)) { const arr = autoParse(value, options); if (Array.isArray(arr)) return new type(arr); } return new type(value); } throw new Error("Unsupported type."); } } catch (err) { if (options && typeof options.onError === "function") { return returnIfAllowed(options.onError(err, value, type), options, value); } if (typeof globalOnError === "function") { return returnIfAllowed(globalOnError(err, value, type), options, value); } throw err; } } function autoParse(value, typeOrOptions) { let type; let options; if (typeOrOptions && typeof typeOrOptions === "object" && !Array.isArray(typeOrOptions) && !(typeOrOptions instanceof Function)) { options = typeOrOptions; type = options.type; } else { type = typeOrOptions; options = {}; } try { const pluginVal = runPlugins(value, type, options); if (pluginVal !== void 0) { return returnIfAllowed(pluginVal, options, value); } if (type) { return returnIfAllowed(parseType(value, type, options), options, value); } const originalValue = value; if (typeof value === "string" && options.stripStartChars) { const chars = Array.isArray(options.stripStartChars) ? options.stripStartChars.join("") : String(options.stripStartChars); value = value.replace(getStripRegex(chars), ""); } if (value === null) { return returnIfAllowed(null, options, originalValue); } if (value === void 0) { return returnIfAllowed(void 0, options, originalValue); } if (value instanceof Date || value instanceof RegExp) { return returnIfAllowed(value, options, originalValue); } if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") { return returnIfAllowed(value, options, originalValue); } if (typeof value === "function") { return returnIfAllowed(parseFunction(value, options), options, originalValue); } if (typeof value === "object") { return returnIfAllowed(parseObject(value, options), options, originalValue); } if (value === "NaN") { return returnIfAllowed(NaN, options, originalValue); } let jsonParsed = null; const trimmed = typeof value === "string" ? value.trim() : ""; if (options.expandEnv) { const expanded = expandEnvVars(trimmed); if (expanded !== trimmed) { return returnIfAllowed(autoParse(expanded, options), options, originalValue); } } let mapSet; if (options.parseMapSets) { mapSet = parseMapSetString(trimmed, options); if (mapSet) return returnIfAllowed(mapSet, options, originalValue); } if (/^['"]?[[{]/.test(trimmed)) { try { jsonParsed = JSON.parse(trimmed); } catch (e) { try { jsonParsed = JSON.parse( trimmed.replace(/(\\\\")|(\\")/gi, '"').replace(/(\\n|\\\\n)/gi, "").replace(/(^"|"$)|(^'|'$)/gi, "") ); } catch (e2) { try { jsonParsed = JSON.parse(trimmed.replace(/'/gi, '"')); } catch (e3) { } } } } if (jsonParsed && typeof jsonParsed === "object") { return returnIfAllowed(autoParse(jsonParsed, options), options, originalValue); } if (options.parseTypedArrays) { const typedArr = parseTypedArrayString(trimmed, options); if (typedArr) return returnIfAllowed(typedArr, options, originalValue); } if (options.parseCurrency) { const currency = parseCurrencyString(trimmed, options); if (currency !== null) return returnIfAllowed(currency, options, originalValue); } if (options.parsePercent) { const percent = parsePercentString(trimmed, options); if (percent !== null) return returnIfAllowed(percent, options, originalValue); } if (options.parseUnits) { const unit = parseUnitString(trimmed); if (unit) return returnIfAllowed(unit, options, originalValue); } if (options.parseRanges) { const range = parseRangeString(trimmed, options); if (range) return returnIfAllowed(range, options, originalValue); } if (options.parseExpressions) { const expr = parseExpressionString(trimmed); if (expr !== null) return returnIfAllowed(expr, options, originalValue); } if (options.parseFunctionStrings) { const fn = parseFunctionString(trimmed); if (fn) return returnIfAllowed(fn, options, originalValue); } if (options.parseDates) { const dt = parseDateTimeString(trimmed); if (dt) return returnIfAllowed(dt, options, originalValue); } if (options.parseUrls) { const u = parseUrlString(trimmed); if (u) return returnIfAllowed(u, options, originalValue); } if (options.parseFilePaths) { const p = parseFilePathString(trimmed); if (p) return returnIfAllowed(p, options, originalValue); } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })); if (value === "undefined" || value === "") { return returnIfAllowed(void 0, options, originalValue); } if (value === "null") { return returnIfAllowed(null, options, originalValue); } let numValue = value; if (options.parseCommaNumbers && typeof numValue === "string" && numValue.includes(",")) { const normalized = numValue.replace(/,/g, ""); if (!Number.isNaN(Number(normalized))) { numValue = normalized; } } const num = Number(numValue); if (!Number.isNaN(num)) { if (options.preserveLeadingZeros && /^0+\d+$/.test(value)) { return returnIfAllowed(String(originalValue), options, originalValue); } return returnIfAllowed(num, options, originalValue); } const boo = checkBoolean(value, options); if (boo !== null) { return returnIfAllowed(boo, options, originalValue); } return returnIfAllowed(String(originalValue), options, originalValue); } catch (err) { if (options && typeof options.onError === "function") { return returnIfAllowed(options.onError(err, value, type), options, value); } if (typeof globalOnError === "function") { return returnIfAllowed(globalOnError(err, value, type), options, value); } throw err; } }