UNPKG

expand-value

Version:

Get deeply nested values from an object, like dot-prop and get-value, but with support for advanced features like bracket-notation and more.

856 lines (846 loc) 24.8 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/utils.ts import isNumber from "is-number"; import { Segmenter } from "intl-segmenter"; var { defineProperty } = Reflect; var isObject = /* @__PURE__ */ __name((val) => val && typeof val === "object" && !Array.isArray(val), "isObject"); var unquote = /* @__PURE__ */ __name((str) => { if (!str) return ""; return str.replace(/^['"`]|['"`]$/g, ""); }, "unquote"); var define = /* @__PURE__ */ __name((node, key, value) => { defineProperty(node, key, { configurable: true, enumerable: false, writable: true, value }); }, "define"); var size = /* @__PURE__ */ __name((value) => { if (value == null) return 0; if (isNumber(value)) return String(value).length; if (isObject(value)) return Object.keys(value).length; if (typeof value.length === "number") return value.length; if (typeof value.size === "number") return value.size; return null; }, "size"); var isValidObject = /* @__PURE__ */ __name((val) => { return isObject(val) || Array.isArray(val) || typeof val === "function"; }, "isValidObject"); var isSafeKey = /* @__PURE__ */ __name((key) => { return key !== "__proto__" && key !== "constructor" && key !== "prototype"; }, "isSafeKey"); var isValid = /* @__PURE__ */ __name((key, data, options) => { if (!isSafeKey(key)) { return false; } if (typeof options.isValid === "function") { return options.isValid(key, data); } return true; }, "isValid"); var findSafeBreakPoint = /* @__PURE__ */ __name((input) => { for (let i = input.length - 1; i >= 0; i--) { if (/\s/.test(input[i]) || /^[\x20-\x7E]$/.test(input[i])) { return i + 1; } } return input.length; }, "findSafeBreakPoint"); var getSegments = /* @__PURE__ */ __name((input, language = "en", granularity) => { const segmenter = new Segmenter(language, { granularity, localeMatcher: "best fit" }); return Array.from(segmenter.segment(input)).map((segment) => segment.segment); }, "getSegments"); var getGraphemes = /* @__PURE__ */ __name((input, language = "en", maxChunkLength = 500) => { const graphemes = []; let position = 0; while (position < input.length) { const remainingText = input.slice(position); const chunkSize = Math.min(maxChunkLength, remainingText.length); const potentialChunk = remainingText.slice(0, chunkSize); const breakPoint = findSafeBreakPoint(potentialChunk); const chunk = potentialChunk.slice(0, breakPoint); const chunkSegments = getSegments(chunk, language, "grapheme"); graphemes.push(...chunkSegments); position += breakPoint; } return graphemes; }, "getGraphemes"); // src/expression.ts function parse(expression) { let pos = 0; function peek() { return expression[pos]; } __name(peek, "peek"); function consume() { return expression[pos++]; } __name(consume, "consume"); function skipWs() { while (pos < expression.length && /\s/.test(peek())) { pos++; } } __name(skipWs, "skipWs"); function parseNum() { let n = ""; while (pos < expression.length && /\d/.test(peek())) { n += consume(); } return { type: "number", val: parseInt(n, 10) }; } __name(parseNum, "parseNum"); function parseIdent() { let id = ""; while (pos < expression.length && /[\w$]/.test(peek())) { id += consume(); } return id; } __name(parseIdent, "parseIdent"); function parsePath() { const parts = [parseIdent()]; while (peek() === ".") { consume(); parts.push(parseIdent()); } return { type: "path", parts }; } __name(parsePath, "parsePath"); function parsePrimary() { skipWs(); const ch = peek(); if (/\d/.test(ch)) { return parseNum(); } if (/[\w$]/.test(ch)) { return parsePath(); } if (ch === "(") { consume(); const node = parseExpr(); skipWs(); if (peek() === ")") consume(); return node; } return null; } __name(parsePrimary, "parsePrimary"); function parseExpr() { let left = parsePrimary(); while (true) { skipWs(); const op = peek(); if (op !== "+" && op !== "-") { break; } consume(); const right = parsePrimary(); left = { type: "binary", op, left, right }; } return left; } __name(parseExpr, "parseExpr"); return parseExpr(); } __name(parse, "parse"); function evaluateExpression(node, data) { if (!node) return void 0; switch (node.type) { case "number": { return node.val; } case "path": { let val = data; for (const p of node.parts) { if (val == null) return void 0; val = val[p]; } return val; } case "binary": { const l = evaluateExpression(node.left, data); const r = evaluateExpression(node.right, data); if (node.op === "+") return l + r; if (node.op === "-") return l - r; return void 0; } default: { return void 0; } } } __name(evaluateExpression, "evaluateExpression"); function evaluate(expression, data) { const ast = parse(expression); return evaluateExpression(ast, data); } __name(evaluate, "evaluate"); // src/nodes/Location.ts var Position = class { static { __name(this, "Position"); } index; line; col; constructor(loc) { this.index = loc.index; this.line = loc.line; this.col = loc.col; } }; var Location = class { static { __name(this, "Location"); } start; end; constructor(start, end) { this.start = start; this.end = end; } slice(input) { return input.slice(...this.range); } get range() { return [this.start.index, this.end.index]; } get lines() { return [this.start.line, this.end.line]; } }; var location = /* @__PURE__ */ __name((loc) => { const start = new Position(loc); return (node) => { node.loc = new Location(start, new Position(loc)); return node; }; }, "location"); location.Position = Position; location.Location = Location; location.location = location; // src/nodes/Node.ts var Node = class { static { __name(this, "Node"); } type; value; output; symbol; parent; constructor(node) { this.type = node.type; this.value = node.value || ""; if (node.output != null && node.output !== "") { this.output = node.output; } if (node.symbol) { this.symbol = node.symbol; } define(this, "alt", node.alt); define(this, "match", node.match); define(this, "loc", node.loc); } get siblings() { return this.parent?.nodes || []; } }; // src/nodes/Block.ts var Block = class extends Node { static { __name(this, "Block"); } nodes; constructor(node) { super(node); this.nodes = node.nodes || []; } append(input) { this.parent && this.parent.append(input); this.output = this.output || ""; this.output += input; } push(node) { define(node, "parent", this); this.nodes.push(node); } }; // src/nodes/Token.ts var { defineProperty: defineProperty2 } = Reflect; var Token = class { static { __name(this, "Token"); } type; value; loc; match; constructor(token) { this.type = token.type; this.value = token.value; defineProperty2(this, "loc", { value: token.loc, writable: true }); defineProperty2(this, "match", { value: token.match }); } }; // src/parse.ts var QUOTED_STRING = /^(['"`])((?:\\.|(?!\1)[\s\S])*?)(\1)/; var IDENT_DOT = /^([a-zA-Z_][-a-zA-Z0-9_.]*(?<!\.))/; var IDENT = /^([a-zA-Z_$][a-zA-Z0-9_$-]*(?<!-))/; var NUMBER = /^(-?[0-9]+(?:\.[0-9]+)?|-?Infinity|NaN)/; var RANGE = /^\.\.(?=[0-9a-z])/i; var SYMBOL = /^Symbol\((.*?)\)/; var parse2 = /* @__PURE__ */ __name((input = "", options = {}) => { if (!isObject(options)) options = {}; const loc = { index: 0, line: 1, col: 0, row: 1 }; let pos = location(loc); const str = String(input); const ast = new Block({ type: "root" }); ast.output = ""; const stack = [ast]; const stash = []; const tokens = []; let remaining = str; let block = ast; let token; let match; let prev; let separator = /^\./; let IDENTITY = IDENT; if (typeof options.separator === "string") { separator = new RegExp(`^\\${options.separator.replace(/^[\\^]+/, "")}`); IDENTITY = IDENT_DOT; } const eos = /* @__PURE__ */ __name(() => remaining === void 0 || remaining === "", "eos"); const scan = /* @__PURE__ */ __name((regex, type = "text") => { if (match = regex.exec(remaining)) { consume(match[0]); return new Token({ type, value: match[0], match }); } }, "scan"); const updateLocation = /* @__PURE__ */ __name((value, len) => { const i = value.lastIndexOf("\n"); loc.index += len; loc.col = ~i ? len - i : loc.col + len; loc.row += Math.max(0, value.split("\n").length - 1); }, "updateLocation"); const consume = /* @__PURE__ */ __name((value, len = value.length) => { updateLocation(value, len); remaining = remaining.slice(len); return value; }, "consume"); const drop = /* @__PURE__ */ __name(() => { if (stash.length) { block.push(stash.shift()); } }, "drop"); const shouldPush = /* @__PURE__ */ __name((node) => { return node.type !== "newline" || options.newlines !== false; }, "shouldPush"); const push = /* @__PURE__ */ __name((node) => { pos(node); if (prev?.type === "ident" && node.type === "ident") { block.append(node.output || node.match[0]); prev.value += node.value; return; } if (!shouldPush(node)) return; block.push(node); if (node.nodes) { stack.push(node); block = node; } else { block.append(node.output || node.match[0]); tokens.push(node); } pos = location(loc); prev = node; if (block.type === "root") { drop(); } }, "push"); const pop = /* @__PURE__ */ __name(() => { const parent = stack.pop(); block = stack[stack.length - 1]; return parent; }, "pop"); const advance = /* @__PURE__ */ __name(() => { if (token = scan(/^\\+/, "escaped")) { if (token.value.length % 2 === 1) { token.value += consume(remaining[0]); } if (token.value === "\\.") { token.type = "ident"; token.value = "."; token.output = "\\."; } push(new Node(token)); return; } if (token = scan(SYMBOL, "symbol")) { token.value = token.match[1]; token.symbol = Symbol.for(token.value); push(new Node(token)); return; } if (token = scan(/^\[/, "left_bracket")) { token.index = tokens.length; push(new Block({ type: "bracket" })); push(new Node(token)); return; } if (token = scan(/^\]/, "right_bracket")) { push(new Node(token)); if (block.type === "bracket") { pop(); } return; } if (token = scan(/^\(/, "left_paren")) { token.index = tokens.length; push(new Block({ type: "paren" })); push(new Node(token)); return; } if (token = scan(/^\)/, "right_paren")) { push(new Node(token)); if (block.type === "paren") { pop(); } return; } if (block.type === "bracket" && (token = scan(RANGE, "range"))) { push(new Node(token)); return; } if (!options.separator && (token = scan(separator, "separator"))) { push(new Node(token)); return; } if (token = scan(/^[0-9]+/, "integer")) { push(new Node(token)); return; } if (token = scan(IDENTITY, "ident")) { push(new Node(token)); return; } if (options.separator && options.separator !== ".") { if (token = scan(separator, "separator")) { push(new Node(token)); return; } } if (token = scan(QUOTED_STRING, "quoted")) { push(new Node(token)); return; } if (token = scan(NUMBER, "number")) { if (token.value === "-0") { token.alt = "0"; } push(new Node(token)); return; } push(new Node(scan(/^(.)/, "text"))); }, "advance"); while (!eos()) advance(); return { ast, tokens, output: ast.output }; }, "parse"); // src/async/expand.ts var METHOD_REGEX = /(\[[^[\]]+?\]|\.(?:blank|empty|first|last|length|nil|size)(\.|$))/; var expand = /* @__PURE__ */ __name(async (data, path, options = {}) => { if (!isObject(options)) { options = { default: options }; } const fallback = options.default !== void 0 ? options.default : options.fallback; const helpers = options.helpers; const resolveValue = /* @__PURE__ */ __name(async (target, prop, value2, state) => { return options.resolve?.(target, prop, value2, state) ?? value2; }, "resolveValue"); const readValue = /* @__PURE__ */ __name(async (receiver, key) => { const value2 = await receiver?.[key]; return value2 === void 0 && typeof receiver?.get === "function" ? receiver.get(key) : value2; }, "readValue"); if (data && typeof path === "string") { if (path.startsWith("[") && path.endsWith("]") && !path.slice(1).includes("[")) { const prop = path.slice(1, -1); const value3 = await readValue(data, prop); if (isValid(prop, data, options)) { const resolved = await resolveValue(data, prop, value3, { segments: [prop], index: 0 }); if (resolved !== void 0) { return resolved; } } } const value2 = await readValue(data, path); const hasSegments = /(?<!\\)\.(?!$)/.test(path); if ((!hasSegments || value2 !== void 0) && isValid(path, data, options)) { const resolved = await resolveValue(data, path, value2, { segments: [path], index: 0 }); if (resolved !== void 0) { return resolved; } } } if ((typeof path === "symbol" || typeof path === "number") && isValid(path, data, options)) { return resolveValue(data, path, await readValue(data, path), { segments: [path], index: 0 }); } if (typeof path !== "string" && !Array.isArray(path)) { return data; } if (!isValidObject(data)) { return fallback; } const value = await readValue(data, path); if ((value !== void 0 || path in data) && isValid(path, data, options)) { return resolveValue(data, path, value, { segments: [path], index: 0 }); } if ((Array.isArray(path) || !METHOD_REGEX.test(path)) && !options.separator) { const segs = Array.isArray(path) ? path : path.split(/(?<!\\)\.(?!$)/); let ctx = data; let prev = ctx; let i = 0; if (segs.length === 0) { return fallback; } for (; i < segs.length; i++) { if (ctx === void 0) { return fallback; } let key = String(segs[i]).replace(/\\(.)/g, "$1"); const helper = helpers?.[key]; if (!isValidObject(ctx)) { if (!helper) { return fallback; } ctx = await helper(ctx); continue; } if (options.onResolve) { await options.onResolve(ctx, key); } if (Array.isArray(ctx) || typeof ctx === "string") { const index = Number(key); if (!Number.isNaN(index)) { key = index; } } else if (key === "-0") { key = -0; } else if (key === "NaN") { key = NaN; } else if (key === "Infinity" || key === "-Infinity") { key = Number(key); } if (!isValid(key, ctx, options)) { return fallback; } let val = await resolveValue(ctx, key, await readValue(ctx, key), { segments: segs, index: i }); if (val === void 0 && helper) { val = await helper(ctx); } if (val !== void 0) { if (!isValid(key, ctx, options)) { return fallback; } prev = ctx; ctx = val; continue; } let temp = ctx; let next = segs[i + 1]; let found = false; while (next) { i++; key += `.${next}`; if (!isValid(key, ctx, options)) { return fallback; } temp = await resolveValue(ctx, key, await readValue(ctx, key), { segments: segs, index: i }); next = segs[i + 1]; if (temp !== void 0) { prev = ctx; ctx = temp; found = true; break; } } if (!found) { prev = ctx; ctx = fallback; break; } } if (i < segs.length) { if (options.strict === true && fallback === void 0) { throw new Error(`Variable is undefined: "${segs[i - 1]}"`); } return fallback; } if (typeof ctx === "function" && isObject(prev)) { ctx.context = prev; } return ctx; } const { ast } = parse2(path, options); const output = await compile(ast, data, options); if (output === void 0) { return fallback; } return output; }, "expand"); expand.parse = parse2; expand.compile = compile; // src/helpers.ts var helpers_exports = {}; __export(helpers_exports, { first: () => first, last: () => last, length: () => length, size: () => size2 }); var first = /* @__PURE__ */ __name((value) => { if (!value) return; if (value instanceof Set || value instanceof Map) { value = [...value]; } if (isObject(value)) { return typeof value.first === "function" ? value.first() : value.first; } if (Array.isArray(value)) { return value[0]; } if (typeof value === "string") { const graphemes = getGraphemes(value.slice(0, 20)); return graphemes[0]; } return value[0]; }, "first"); var last = /* @__PURE__ */ __name((value) => { if (!value) return; if (value instanceof Set || value instanceof Map) { value = [...value]; } if (isObject(value)) { return typeof value.last === "function" ? value.last() : value.last; } if (Array.isArray(value)) { return value[value.length - 1]; } if (typeof value === "string") { const graphemes = getGraphemes(value.slice(-20)); return graphemes[graphemes.length - 1]; } return value[value.length - 1]; }, "last"); var length = /* @__PURE__ */ __name((value) => { if (typeof value?.length === "number") { return value.length; } if (typeof value?.size === "number") { return value.size; } return size2(value); }, "length"); var size2 = /* @__PURE__ */ __name((value) => { if (value === null) { return 1; } if (typeof value?.size === "number") { return value.size; } if (typeof value?.length === "number") { return value.length; } return size(value); }, "size"); // src/async/compile.ts var compile = /* @__PURE__ */ __name(async (ast, data = {}, options = {}) => { const orig = { ...data }; let context = orig; let prev = context; const segments = []; const fns = options.helpers ? { ...helpers_exports, ...options.helpers } : helpers_exports; const resolveValue = /* @__PURE__ */ __name(async (target, prop, value) => { const index = segments.push(prop) - 1; return options.resolve?.(target, prop, value, { segments, index }) ?? value; }, "resolveValue"); const resolve = /* @__PURE__ */ __name(async (node) => { if (node.skip || node.type === "separator") { return; } if (context === void 0) { return; } if (node.type === "paren") { const args = []; for (let i = 1; i < node.nodes.length - 1; i++) { const child = node.nodes[i]; switch (child.type) { case "integer": args.push(Number(child.value)); break; case "quoted": args.push(unquote(child.value)); break; case "symbol": args.push(Symbol.for(child.value)); break; case "ident": args.push(await expand(context, child.value, options)); break; default: { break; } } } context = `(${args.join("..")})`; return; } if (node.nodes) { if (node.type !== "root") { const inner = node.nodes.slice(1, -1); if (inner.some((n) => n.value === " ")) { try { const text = inner.map((n) => n.value).join(""); await resolve({ type: "ident", value: await evaluate(text, data) }); return; } catch { } } if (inner.length === 1 && inner[0].type === "ident") { await resolve(inner[0]); return; } if (node.type === "bracket" && inner.some((n) => n.type === "bracket")) { const value = await expand(orig, node.output.slice(1, -1), options); if (value === void 0) { context = void 0; return; } prev = context; const raw = await context?.[value]; context = await resolveValue(context, value, raw); return; } } for (const child of node.nodes) { await resolve(child); } return; } if (node.type === "symbol") { prev = context; for (const symbol2 of Object.getOwnPropertySymbols(context)) { if (symbol2 === node.symbol || symbol2.toString() === node.symbol.toString()) { const raw2 = await context[symbol2]; context = await resolveValue(context, symbol2, raw2); return; } } const symbol = node.symbol || Symbol.for(node.value); const raw = await context[symbol]; context = await resolveValue(context, symbol, raw); return; } if (node.type === "ident") { if (!isSafeKey(node.value)) { context = void 0; return; } let value = node.value; if (node.parent?.type === "bracket") { let temp = orig; value = await expand(temp, value, options); if (value === void 0) { context = void 0; return; } if (typeof value === "number") { prev = context; const raw2 = await context[value]; context = await resolveValue(context, value, raw2); return; } if (isObject(value)) { const siblings = node.siblings.filter((n) => ["ident", "quoted", "symbol"].includes(n.type)); let index = siblings.indexOf(node) + 1; let next = siblings[index]; while (isObject(value) && isObject(next) && temp) { const key = next.value; value = await expand(value, key, options); next.skip = true; temp = await expand(temp, value, options); next = siblings[++index]; } } } const target = context; prev = target; const raw = await target?.[value]; context = await resolveValue(target, value, raw); if (context !== void 0) { if (typeof context === "function" && value in fns) { context = await context.call(prev); } return; } context = target; const helper = await fns[value]; if (typeof helper === "function") { context = await helper(context); } if (context === void 0 && options.strict === true) { throw new Error(`Variable is undefined: "${node.value}"`); } return; } if (node.type === "integer" || node.type === "number") { if (node.parent.type === "bracket") { const index = node.parent.nodes.indexOf(node); const next = node.parent.nodes[index + 1]; const after = node.parent.nodes[index + 2]; if (next?.type === "range" && (after?.type === "integer" || after?.type === "number")) { next.skip = true; after.skip = true; const start = Number(node.value); const end = Number(after.value); const range = Array.from({ length: end - start + 1 }, (_, i) => start + i); context = await Promise.all(range.map(async (i) => resolveValue(context, i, await context[i]))); return; } } prev = context; const key = Number(node.value); const raw = await context[key]; context = await resolveValue(context, key, raw); return; } if (node.type === "quoted") { prev = context; const key = node.match[2]; const raw = await context[key]; context = await resolveValue(context, key, raw); } }, "resolve"); await resolve(ast); if (typeof context === "function") { context.context = prev; } if (ast.nodes?.length > 0 && context === orig) { return void 0; } return context; }, "compile"); export { compile, expand, parse2 as parse }; //# sourceMappingURL=index.mjs.map