UNPKG

object-replace-mustache

Version:

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

165 lines (159 loc) 6.12 kB
import { parseScript } from "meriyah"; //#region src/utils.ts const isPlainObject = (value) => !!value && [void 0, Object].includes(value.constructor); function escape(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const delimitersMustache = ["{{", "}}"]; const regexForDelimiters = (delimiters$1, flags) => { return new RegExp(`^${escape(delimiters$1[0])}(.*?)${escape(delimiters$1[1])}$`, flags); }; //#endregion //#region src/is-template-string.ts const isTemplateString = (str, options) => { if (!str || typeof str !== "string") return false; return regexForDelimiters(options?.delimiters ?? delimitersMustache).test(str); }; //#endregion //#region src/replace-string.ts function traverseAST(node, visitor) { if (!node || typeof node !== "object") return; if (visitor(node) === true) return; for (const key in node) { if (key === "type" || key === "loc" || key === "range") continue; const value = node[key]; if (Array.isArray(value)) for (const item of value) traverseAST(item, visitor); else if (value && typeof value === "object") traverseAST(value, visitor); } } const prohibitedKeywordRE = /* @__PURE__ */ 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(/\${/)) { let isValid = true; let inTemplate = false; let escaped = false; for (let i = 0; i < expression.length; i++) { const char = expression[i]; const next = expression[i + 1]; if (escaped) { escaped = false; continue; } if (char === "\\") { escaped = true; continue; } if (char === "`") inTemplate = !inTemplate; if (char === "$" && next === "{" && !inTemplate) { isValid = false; break; } } if (!isValid) 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`); let ast; try { ast = parseScript(expression); } catch (err) { return silentOrThrow(err.message || "Failed to parse expression"); } let unsafe = false; traverseAST(ast, (node) => { if (unsafe) return true; const type = node.type; if (type === "ThisExpression") { unsafe = "this is not defined"; return true; } if (type === "AssignmentExpression") { unsafe = "assignment is not allowed in template string"; return true; } if (node.type === "VariableDeclaration" || node.type === "FunctionDeclaration") { unsafe = true; return true; } else if (node.type === "Identifier" && unsafeMethods.indexOf(node.name) != -1) { unsafe = `${node.name} is not defined`; return true; } }); 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"); if (withoutStrings.replace(/!==|===|==|!=|>=|<=|=>/g, "").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", ` 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 }); //#endregion //#region src/replace-object.ts 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, options); return result; } else if (Array.isArray(item)) return [...item.map((subItem) => recursiveReplace(subItem, view, options))]; return item; }; //#endregion //#region src/render.ts 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; }); //#endregion export { delimiters, isTemplateString, render, replace, replaceString, replaceStringEjs, replaceStringMustache };