UNPKG

object-replace-mustache

Version:

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

158 lines (149 loc) 5.53 kB
'use strict'; const parser = require('@babel/parser'); const _traverse = require('@babel/traverse'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const _traverse__default = /*#__PURE__*/_interopDefaultCompat(_traverse); const isPlainObject = (value) => value && [void 0, Object].includes(value.constructor); function escape(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const regexForDelimiters = (delimiters, flags) => { return new RegExp( `^${escape(delimiters[0])}(.*?)${escape(delimiters[1])}$`, flags ); }; const traverse = typeof _traverse__default === "function" ? _traverse__default : _traverse__default.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 ?? ["{{", "}}"] ).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 = parser.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) => replaceString(m, view, { handleError: options?.handleError ?? "ignore", delimiters: options?.delimiters ?? ["{{", "}}"] }) ); exports.delimiters = delimiters; exports.render = render; exports.replace = replace; exports.replaceString = replaceString; exports.replaceStringEjs = replaceStringEjs; exports.replaceStringMustache = replaceStringMustache;