UNPKG

@player-ui/player

Version:

206 lines (169 loc) 5.53 kB
import { setIn } from "timm"; import type { Expression } from "@player-ui/types"; import type { DataModelWithParser } from "../data"; const DOUBLE_OPEN_CURLY = "{{"; const DOUBLE_CLOSE_CURLY = "}}"; export interface Options { /** * The model to use when resolving refs * Passing `false` will skip trying to resolve any direct model refs ({{foo}}) */ model: false | DataModelWithParser; /** * A function to evaluate an expression * Passing `false` will skip trying to evaluate any expressions (@[ foo() ]@) */ evaluate: false | ((exp: Expression) => any); /** * Optionaly resolve binding without formatting in case Type format applies */ formatted?: boolean; } /** Search the given string for the coordinates of the next expression to resolve */ export function findNextExp(str: string) { const expStart = str.indexOf(DOUBLE_OPEN_CURLY); if (expStart === -1) { return undefined; } let count = 1; let offset = expStart + DOUBLE_OPEN_CURLY.length; let workingString = str.substring(expStart + DOUBLE_OPEN_CURLY.length); while (count > 0 && workingString.length > 0) { // Find the next open or close curly const nextCloseCurly = workingString.indexOf(DOUBLE_CLOSE_CURLY); // We can't close anything, so there's no point in going on with life. if (nextCloseCurly === -1) { break; } const nextOpenCurly = workingString.indexOf(DOUBLE_OPEN_CURLY); if (nextOpenCurly !== -1 && nextOpenCurly < nextCloseCurly) { // We've hit another open bracket before closing out the one we want // Move everything over and bump our close count by 1 count++; workingString = workingString.substring( nextOpenCurly + DOUBLE_OPEN_CURLY.length, ); offset += nextOpenCurly + DOUBLE_OPEN_CURLY.length; } else { // We've hit another closing bracket // Decrement our count and updates offsets count--; workingString = workingString.substring( nextCloseCurly + DOUBLE_CLOSE_CURLY.length, ); offset += nextCloseCurly + DOUBLE_CLOSE_CURLY.length; } } if (count !== 0) { throw new Error(`Unbalanced {{ and }} in exp: ${str}`); } return { start: expStart, end: offset, }; } /** Finds any subset of the string wrapped in @[]@ and evaluates it as an expression */ export function resolveExpressionsInString( val: string, { evaluate }: Options, ): string { if (!evaluate) { return val; } const expMatch = /@\[.*?\]@/; let newVal = val; let match = newVal.match(expMatch); while (match !== null) { const expStrWithBrackets = match[0]; const matchStart = newVal.indexOf(expStrWithBrackets); const expString = expStrWithBrackets.substr( "@[".length, expStrWithBrackets.length - "@[".length - "]@".length, ); const expValue = evaluate(expString); // The string is only the expression, return the raw value. if ( matchStart === 0 && expStrWithBrackets === val && typeof expValue !== "string" ) { return expValue; } newVal = newVal.substr(0, matchStart) + expValue + newVal.substr(matchStart + expStrWithBrackets.length); // remove the surrounding @[]@ to get the expression match = newVal.match(expMatch); } return newVal; } /** Return a string with all data model references resolved */ export function resolveDataRefsInString(val: string, options: Options): string { const { model, formatted = true } = options; let workingString = resolveExpressionsInString(val, options); if ( !model || typeof workingString !== "string" || workingString.indexOf(DOUBLE_OPEN_CURLY) === -1 ) { return workingString; } while (workingString.indexOf(DOUBLE_OPEN_CURLY) !== -1) { const expLocation = findNextExp(workingString); if (!expLocation) { return workingString; } const { start, end } = expLocation; // Strip out the wrapping curlies from {{binding}} before passing to the model const binding = workingString .substring( start + DOUBLE_OPEN_CURLY.length, end - DOUBLE_OPEN_CURLY.length, ) .trim(); const evaledVal = model.get(binding, { formatted }); // Exit early if the string is _just_ a model lookup // If the result is a string, we may need further processing for nested bindings if ( start === 0 && end === workingString.length && typeof evaledVal !== "string" ) { return evaledVal; } workingString = workingString.substr(0, start) + evaledVal + workingString.substr(end); } return workingString; } /** Traverse the thing and replace any model refs */ function traverseObject<T>(val: T, options: Options): T { switch (typeof val) { case "string": { return resolveDataRefsInString(val as string, options) as unknown as T; } case "object": { if (!val) return val; // TODO: Do we care refs in keys? const keys = Object.keys(val); let newVal = val; if (keys.length > 0) { keys.forEach((key) => { newVal = setIn( newVal as any, [key], traverseObject((val as any)[key], options), ) as any; }); } return newVal; } default: return val; } } /** Recursively resolve all model refs in whatever you pass in */ export function resolveDataRefs<T>(val: T, options: Options): T { return traverseObject(val, options); }