Skip to content

Commit

Permalink
feat: text macro support (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
wibus-wee authored Mar 26, 2023
1 parent 7b76823 commit 27133ce
Show file tree
Hide file tree
Showing 10 changed files with 538 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:

strategy:
matrix:
node: [14.x, 16.x]
node: [16.x]
os: [ubuntu-latest]
fail-fast: false

Expand Down
176 changes: 176 additions & 0 deletions benchmark/mx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { safeEval } from "../src/utils/safe-eval.util"

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

class TextMacroService {

private ifConditionGrammar<T extends object>(text: string, model: T) {
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[conditionSplitter.length - 1] = conditionSplitter[
conditionSplitter.length - 1
].replace(/\?/g, '')
})

let output: any
const condition = conditionSplitter[0].replace('$', '')
// eslint-disable-next-line no-useless-escape
const operator = condition.match(/>|==|<|\!=/g)
if (!operator) {
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]
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
default:
output = conditionSplitter[1]
break
}
return output
}

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

// styling
opacity: (text: string, opacity = 0.8) => {
return `<span style="opacity: ${opacity}">${text}</span>`
},
blur: (text: string, blur = 1) => {
return `<span style="filter: blur(${blur}px)">${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>`
},

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

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

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

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

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

text = text.replace(matchedReg, (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') {
// return i.raw.includes(condition)
// }
// })

// if (isInCodeBlock) {
// return match
// }

condition = condition?.trim()
if (condition.search(RegMap['?']) != -1) {
return this.ifConditionGrammar(condition, model)
}
if (condition.search(RegMap['$']) != -1) {
const variable = condition
.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) {
// eslint-disable-next-line no-useless-escape
const functions = condition.replace(RegMap['#'], '$1')

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(
`return ${functions}`,
this.generateFunctionContext({ ...variables, ...extraContext }),

{ timeout: 1000 },
)
cacheMap[functions] = result
return result
} catch {
return match
}
}
})

return text
} catch (err) {

return text
}
}
}

interface Variables {
title: string;
created: string;
slug: string;
nid: string;
_id: string;
}

export async function textMacro(text: string, record: Variables) {
const service = new TextMacroService();
return await service.replaceTextMacro(text, record);
}
34 changes: 34 additions & 0 deletions benchmark/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { textMacro } from "../src/functions/macros";
import { textMacro as mxTextMacro } from "./mx";

const Benchmark = require("benchmark");
const suite = new Benchmark.Suite();

const record = {
title: "My First Blog Post",
created: "2020-01-01T00:00:00.000Z",
slug: "my-first-blog-post",
nid: "1",
_id: "abc123",
};

const text = `
[[ $title ]]. The slug is [[ $slug ]] and the nid is [[ $nid ]].
if _id is abc123, then it will return "yes" [[ #($_id === "abc123" ? "yes" : "no") ]]
`;

suite
.add("textMacro (Mog)", () => {
textMacro(text, record);
})
.add("textMacro (Mix)", () => {
mxTextMacro(text, record);
})
.on("cycle", (event: { target: any; }) => {
console.log(String(event.target));
})
.on("complete", function () {
// @ts-ignore
console.log(`Fastest is ${this.filter("fastest").map("name")}. ${(this[0].hz / this[1].hz).toFixed(2)}x faster`);
})
.run({ async: true });
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"@innei/eslint-config-ts": "0.9.8",
"@types/marked": "^4.0.8",
"@types/node": "18.15.8",
"@vitest/ui": "^0.29.7",
"benchmark": "^2.1.4",
"bumpp": "8.2.1",
"eslint": "8.36.0",
"esno": "0.16.3",
Expand All @@ -77,7 +79,9 @@
"@djot/djot": "^0.2.2",
"chalk": "^5.2.0",
"consola": "^2.15.3",
"dayjs": "^1.11.7",
"marked": "^4.3.0",
"redis": "^4.6.5"
"redis": "^4.6.5",
"vm2": "^3.9.14"
}
}
Loading

0 comments on commit 27133ce

Please sign in to comment.