UNPKG

object-replace-mustache

Version:

replace placeholders of an object with a view like you would use mustache.render for strings

165 lines (158 loc) 5.59 kB
import { parse } from '@babel/parser'; import _traverse from '@babel/traverse'; const isPlainObject = (value) => value && [void 0, Object].includes(value.constructor); function escape(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const delimitersMustache = ["{{", "}}"]; const regexForDelimiters = (delimiters, flags) => { return new RegExp( `^${escape(delimiters[0])}(.*?)${escape(delimiters[1])}$`, flags ); }; const isTemplateString = (str, options) => { if (!str || typeof str !== "string") { return false; } const regex = regexForDelimiters(options?.delimiters ?? delimitersMustache); return regex.test(str); }; const traverse = typeof _traverse === "function" ? _traverse : _traverse.default; const prohibitedKeywordRE = new RegExp( "\\b" + "arguments,await,break,case,catch,class,const,continue,debugger,default,delete,do,else,enum,export,extends,finally,for,function,if,import,let,new,return,static,super,switch,throw,try,var,void,while,with,yield".split(",").join("\\b|\\b") + "\\b" ); const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g; const defineReplaceTemplateString = (options) => (template, view = {}) => replaceString(template, view, options); const replaceString = (template, view = {}, options) => { const expression = regexForDelimiters( options?.delimiters ?? delimitersMustache ).exec(template)?.[1]; if (!expression) { return template; } function silentOrThrow(err) { if (options?.handleError !== "ignore") { throw typeof err === "string" ? new Error(err) : err; } return template; } if (expression.match(/\${.*?}/)) { throw new Error("nested expression is not allowed in template string"); } const withoutStrings = expression.replace(stripStringRE, ""); const allowedVariables = Object.keys(view); const unsafeMethods = ["require", "eval", "console", "this", "window", "document", "global", "process", "module", "exports", "self", "globalThis"].filter((x) => allowedVariables.indexOf(x) === -1); const keywordMatch = withoutStrings.match(prohibitedKeywordRE); if (keywordMatch) { throw new Error(`${keywordMatch} is not allowed in template string`); } const ast = parse(expression); if (ast.errors?.length) { return silentOrThrow(ast.errors[0].reasonCode); } let unsafe = false; traverse(ast, { enter(path) { if (unsafe) { return; } const type = path.node.type; if (type === "ThisExpression") { unsafe = "this is not defined"; return; } if (type === "AssignmentExpression") { unsafe = "assignment is not allowed in template string"; return; } if (path.node.type === "VariableDeclaration" || path.node.type === "FunctionDeclaration") { unsafe = true; } else if (path.node.type === "Identifier" && unsafeMethods.indexOf(path.node.name) != -1) { unsafe = `${path.node.name} is not defined`; } } }); if (expression.includes(";")) { return silentOrThrow("; is not allowed in template string"); } if (withoutStrings.includes("=")) { if (withoutStrings.match(/<<=|>>=|>>>=/)) { return silentOrThrow("assignment is not allowed in template string"); } const strippedCompare = withoutStrings.replace(/!==|===|==|!=|>=|<=|=>/g, ""); if (strippedCompare.match(/=/g)) { return silentOrThrow("assignment is not allowed in template string"); } } if (withoutStrings.match(/\+\+|--/g)) { throw new Error("assignment is not allowed in template string"); } if (unsafe) { return silentOrThrow(typeof unsafe === "string" ? unsafe : "unsafe operation is not allowed in template string"); } else { try { const result = new Function( "view", /* js */ ` const tagged = ( ${allowedVariables.join(", ")} ) => ${expression} return tagged(...Object.values(view)) ` )(view); if (typeof result === "function") { return silentOrThrow("function is not allowed in template string"); } return result; } catch (err) { return silentOrThrow(err); } } }; const delimiters = { mustache: ["{{", "}}"], ejs: ["<%=", "%>"] }; const replaceStringMustache = defineReplaceTemplateString({ delimiters: delimiters.mustache }); const replaceStringEjs = defineReplaceTemplateString({ delimiters: delimiters.ejs }); const replace = (item, view, options) => { return recursiveReplace(item, view, options); }; const recursiveReplace = (item, view, options) => { if (typeof item === "string") { return replaceString(item, view, { delimiters: options?.delimiters, handleError: options?.handleError ?? "ignore" }); } else if (isPlainObject(item)) { const result = {}; for (const key in item) { result[key] = recursiveReplace(item[key], view); } return result; } else if (Array.isArray(item)) { return [...item.map((subItem) => recursiveReplace(subItem, view))]; } return item; }; const render = (str, view, options) => str.replace( /\{\{(.*?)\}\}/g, (m) => { let result = replaceString(m, view, { handleError: options?.handleError ?? "ignore", delimiters: options?.delimiters }); if (options?.format) { result = options.format(result); } if (result == null) { return ""; } return result; } ); export { delimiters, isTemplateString, render, replace, replaceString, replaceStringEjs, replaceStringMustache };