Skip to content

Commit

Permalink
refactor(macros)!: use isolated-vm to enhance safety performance (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
wibus-wee authored Aug 27, 2023
1 parent 08d671f commit ed55d1c
Show file tree
Hide file tree
Showing 9 changed files with 1,028 additions and 788 deletions.
164 changes: 81 additions & 83 deletions benchmark/mx.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,111 @@
import { safeEval } from "../src/utils/safe-eval.util"
import { replaceAsync } from "../src/utils/replace-async.util";
import { safeEval } from "../src/utils/safe-eval.util";

const RegMap = {
'#': /^#(.*?)$/g,
"#": /^#(.*?)$/g,
$: /^\$(.*?)$/g,
'?': /^\?\??(.*?)\??\?$/g,
} as const
"?": /^\?\??(.*?)\??\?$/g,
} as const;

class TextMacroService {

private ifConditionGrammar<T extends object>(text: string, model: T) {
const conditionSplitter = text.split('|')
const conditionSplitter = text.split("|");
conditionSplitter.forEach((item: string, index: string | number) => {
conditionSplitter[index as number] = item.replace(/"/g, '')
conditionSplitter[index as number] = conditionSplitter[index as number].replace(/\s/g, '')
conditionSplitter[0] = conditionSplitter[0].replace(/\?/g, '')
conditionSplitter[index as number] = item.replace(/"/g, "");
conditionSplitter[index as number] = conditionSplitter[
index as number
].replace(/\s/g, "");
conditionSplitter[0] = conditionSplitter[0].replace(/\?/g, "");
conditionSplitter[conditionSplitter.length - 1] = conditionSplitter[
conditionSplitter.length - 1
].replace(/\?/g, '')
})
].replace(/\?/g, "");
});

let output: any
const condition = conditionSplitter[0].replace('$', '')
let output: any;
const condition = conditionSplitter[0].replace("$", "");
// eslint-disable-next-line no-useless-escape
const operator = condition.match(/>|==|<|\!=/g)
const operator = condition.match(/>|==|<|\!=/g);
if (!operator) {
throw new Error('Invalid condition')
throw new Error("Invalid condition");
}

const left = condition.split(operator[0])[0]
const right = condition.split(operator[0])[1]
const Value = model[left as keyof T]
const left = condition.split(operator[0])[0];
const right = condition.split(operator[0])[1];
const Value = model[left as keyof T];
switch (operator[0]) {
case '>':
output = Value > right ? conditionSplitter[1] : conditionSplitter[2]
break
case '==':
output = Value == right ? conditionSplitter[1] : conditionSplitter[2]
break
case '<':
output = Value < right ? conditionSplitter[1] : conditionSplitter[2]
break
case '!=':
output = Value != right ? conditionSplitter[1] : conditionSplitter[2]
break
case '&&':
output = Value && right ? conditionSplitter[1] : conditionSplitter[2]
break
case '||':
output = Value || right ? conditionSplitter[1] : conditionSplitter[2]
break
case ">":
output = Value > right ? conditionSplitter[1] : conditionSplitter[2];
break;
case "==":
output = Value == right ? conditionSplitter[1] : conditionSplitter[2];
break;
case "<":
output = Value < right ? conditionSplitter[1] : conditionSplitter[2];
break;
case "!=":
output = Value != right ? conditionSplitter[1] : conditionSplitter[2];
break;
case "&&":
output = Value && right ? conditionSplitter[1] : conditionSplitter[2];
break;
case "||":
output = Value || right ? conditionSplitter[1] : conditionSplitter[2];
break;
default:
output = conditionSplitter[1]
break
output = conditionSplitter[1];
break;
}
return output
return output;
}

private generateFunctionContext = (variables: object) => {
return {
// time utils
dayjs: require('dayjs'),
dayjs: require("dayjs"),
// typography
center: (text: string) => {
return `<p align="center">${text}</p>`
return `<p align="center">${text}</p>`;
},
right: (text: string) => {
return `<p align="right">${text}</p>`
return `<p align="right">${text}</p>`;
},

// styling
opacity: (text: string, opacity = 0.8) => {
return `<span style="opacity: ${opacity}">${text}</span>`
return `<span style="opacity: ${opacity}">${text}</span>`;
},
blur: (text: string, blur = 1) => {
return `<span style="filter: blur(${blur}px)">${text}</span>`
return `<span style="filter: blur(${blur}px)">${text}</span>`;
},
color: (text: string, color = '') => {
return `<span style="color: ${color}">${text}</span>`
color: (text: string, color = "") => {
return `<span style="color: ${color}">${text}</span>`;
},
size: (text: string, size = '1em') => {
return `<span style="font-size: ${size}">${text}</span>`
size: (text: string, size = "1em") => {
return `<span style="font-size: ${size}">${text}</span>`;
},

...variables,
}
}
};
};
public async replaceTextMacro<T extends object>(
text: string,
model: T,

extraContext: Record<string, any> = {},
extraContext: Record<string, any> = {}
): Promise<string> {
try {
const matchedReg = /\[\[\s(.*?)\s\]\]/g
const matchedReg = /\[\[\s(.*?)\s\]\]/g;

const matched = text.search(matchedReg) != -1
const matched = text.search(matchedReg) != -1;

if (!matched) {
return text
return text;
}
// const ast = marked.lexer(text)

const cacheMap = {} as Record<string, any>
const cacheMap = {} as Record<string, any>;

text = text.replace(matchedReg, (match, condition) => {
text = await replaceAsync(text, matchedReg, async (match, condition) => {
// FIXME: shallow find, if same text both in code block and paragraph, the macro in paragraph also will not replace
// const isInCodeBlock = ast.some((i) => {
// if (i.type === 'code' || i.type === 'codespan') {
Expand All @@ -115,49 +117,45 @@ class TextMacroService {
// return match
// }

condition = condition?.trim()
if (condition.search(RegMap['?']) != -1) {
return this.ifConditionGrammar(condition, model)
condition = condition?.trim();
if (condition.search(RegMap["?"]) != -1) {
return this.ifConditionGrammar(condition, model);
}
if (condition.search(RegMap['$']) != -1) {
if (condition.search(RegMap["$"]) != -1) {
const variable = condition
.replace(RegMap['$'], '$1')
.replace(/\s/g, '')
return model[variable as keyof T] || match
.replace(RegMap["$"], "$1")
.replace(/\s/g, "");
return model[variable as keyof T] || match;
}
// eslint-disable-next-line no-useless-escape
if (condition.search(RegMap['#']) != -1) {
if (condition.search(RegMap["#"]) != -1) {
// eslint-disable-next-line no-useless-escape
const functions = condition.replace(RegMap['#'], '$1')
const functions = condition.replace(RegMap["#"], "$1");

if (typeof cacheMap[functions] != 'undefined') {
return cacheMap[functions]
if (typeof cacheMap[functions] != "undefined") {
return cacheMap[functions];
}

const variables = Object.keys(model).reduce(
(acc, key) => ({ [`$${key}`]: model[key as keyof T], ...acc }),
{},
)
{}
);

try {
const result = safeEval(
const result = await safeEval(
`return ${functions}`,
this.generateFunctionContext({ ...variables, ...extraContext }),

{ timeout: 1000 },
)
cacheMap[functions] = result
return result
this.generateFunctionContext({ ...variables, ...extraContext })
);
cacheMap[functions] = result;
return result;
} catch {
return match
return match;
}
}
})

return text
});
return text;
} catch (err) {

return text
return text;
}
}
}
Expand All @@ -173,4 +171,4 @@ interface Variables {
export async function textMacro(text: string, record: Variables) {
const service = new TextMacroService();
return await service.replaceTextMacro(text, record);
}
}
6 changes: 3 additions & 3 deletions benchmark/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ if _id is abc123, then it will return "yes" [[ #($_id === "abc123" ? "yes" : "no
`;

suite
.add("textMacro (Mog)", () => {
textMacro(text, record);
.add("textMacro (Mog)", async () => {
await textMacro(text, record);
})
.add("textMacro (Mix)", () => {
.add("textMacro (Mix Space)", () => {
mxTextMacro(text, record);
})
.on("cycle", (event: { target: any; }) => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"chalk": "4.1.2",
"consola": "^2.15.3",
"dayjs": "^1.11.7",
"isolated-vm": "^4.6.0",
"marked": "^4.3.0",
"redis": "^4.6.5",
"vm2": "^3.9.14"
Expand Down
Loading

0 comments on commit ed55d1c

Please sign in to comment.