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
JavaScript
;
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;