feat: function eval marco

This commit is contained in:
Innei
2022-04-21 22:07:03 +08:00
committed by
parent b09f2319f0
commit 5c335363cb
6 changed files with 215 additions and 14 deletions

View File

@@ -1,6 +1,10 @@
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.locale('zh-cn')
dayjs.extend(localizedFormat)
dayjs.extend(relativeTime)

View File

@@ -1,6 +1,5 @@
import { isURL } from 'class-validator'
import fs, { mkdir, stat } from 'fs/promises'
import { cloneDeep } from 'lodash'
import path from 'path'
import { nextTick } from 'process'
@@ -22,7 +21,7 @@ import { AssetService } from '~/processors/helper/helper.asset.service'
import { HttpService } from '~/processors/helper/helper.http.service'
import { InjectModel } from '~/transformers/model.transformer'
import { UniqueArray } from '~/ts-hepler/unique'
import { getRedisKey, safePathJoin } from '~/utils'
import { deepCloneWithFunction, getRedisKey, safePathJoin } from '~/utils'
import { safeEval } from '~/utils/safe-eval.util'
import { isBuiltinModule } from '~/utils/system.util'
@@ -302,15 +301,8 @@ export class ServerlessService {
} else {
this.requireModuleIdSet.add(resolvePath)
}
const clonedModule = cloneDeep(module)
return typeof module === 'function'
? (() => {
const newFunc = module.bind()
Object.setPrototypeOf(newFunc, clonedModule)
return newFunc
})()
: clonedModule
const clonedModule = deepCloneWithFunction(module)
return clonedModule
}
const __requireNoCache = (id: string) => {

View File

@@ -0,0 +1,103 @@
import dayjs from 'dayjs'
import { BadRequestException, Injectable } from '@nestjs/common'
import { deepCloneWithFunction } from '~/utils'
import { safeEval } from '~/utils/safe-eval.util'
@Injectable()
export class TextMarcoService {
static readonly Reg = {
'#': /^#(.*?)$/g,
$: /^\$(.*?)$/g,
'?': /^\?\??(.*?)\??\?$/g,
}
private ifConditionGrammar<T extends object>(text: string, model: T) {
const conditionSplitter = text.split('|')
conditionSplitter.forEach((item: string, index: string | number) => {
conditionSplitter[index] = item.replace(/"/g, '')
conditionSplitter[index] = conditionSplitter[index].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 BadRequestException('Invalid condition')
}
const left = condition.split(operator[0])[0]
const right = condition.split(operator[0])[1]
const Value = model[left]
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
}
public async replaceTextMarco<T extends object>(
text: string,
model: T,
): Promise<string> {
const matchedReg = /\[\[\s(.*?)\s\]\]/g
if (text.search(matchedReg) != -1) {
text = text.replace(matchedReg, (match, condition) => {
condition = condition?.trim()
if (condition.search(TextMarcoService.Reg['?']) != -1) {
return this.ifConditionGrammar(condition, model)
}
if (condition.search(TextMarcoService.Reg['$']) != -1) {
const variable = condition
.replace(TextMarcoService.Reg['$'], '$1')
.replace(/\s/g, '')
return model[variable]
}
// eslint-disable-next-line no-useless-escape
if (condition.search(TextMarcoService.Reg['#']) != -1) {
// eslint-disable-next-line no-useless-escape
const functions = condition.replace(TextMarcoService.Reg['#'], '$1')
const variables = Object.keys(model).reduce(
(acc, key) => ({ [`$${key}`]: model[key], ...acc }),
{},
)
// TODO catch error
return safeEval(`return ${functions}`, {
dayjs: deepCloneWithFunction(dayjs),
fromNow: (time: Date | string) => dayjs(time).fromNow(),
...variables,
})
}
})
}
return text
}
}

View File

@@ -5,7 +5,7 @@ const Reg = {
'?': /\?\??(.*?)\??\?/g,
}
function ifConditionGramar(condition: string, model: any) {
function ifConditionGrammar(condition: string, model: any) {
const conditionStr = condition.split('|')
conditionStr.forEach((item: string, index: string | number) => {
conditionStr[index] = item.replace(/"/g, '')
@@ -54,7 +54,7 @@ export function macros(str: any, model: any): any {
if (str.search(/\[\[(.*?)\]\]/g) != -1) {
str = str.replace(/\[\[(.*?)\]\]/g, (match, condition) => {
if (condition.search(Reg['?']) != -1) {
return ifConditionGramar(condition, model)
return ifConditionGrammar(condition, model)
}
if (condition.search(Reg['$']) != -1) {
const variable = condition.replace(Reg['$'], '$1').replace(/\s/g, '')

View File

@@ -1,4 +1,4 @@
import { isObject } from 'lodash'
import { cloneDeep, isObject } from 'lodash'
import { join } from 'path'
export const md5 = (text: string) =>
@@ -74,3 +74,17 @@ export const safePathJoin = (...path: string[]) => {
return join(...newPathArr)
}
export const deepCloneWithFunction = <T extends object>(object: T): T => {
const clonedModule = cloneDeep(object)
if (typeof object === 'function') {
// @ts-expect-error
const newFunc = (object as Function).bind()
Object.setPrototypeOf(newFunc, clonedModule)
return newFunc
}
return clonedModule
}

View File

@@ -0,0 +1,88 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { TextMarcoService } from '~/processors/helper/helper.marco.service'
dayjs.extend(relativeTime)
describe.only('test TextMarcoService', () => {
const service = new TextMarcoService()
describe('test if condition', () => {
test('case 1', async () => {
const res = await service.replaceTextMarco(
'[[ ? $a > 1 | "yes" | "no" ? ]]',
{ a: -1 },
)
expect(res).toBe('no')
})
test('case 2', async () => {
const res = await service.replaceTextMarco(
'[[ ? $a > 1 | "yes" | "no" ? ]]',
{ a: 2 },
)
expect(res).toBe('yes')
})
test('case 3', async () => {
const res = await service.replaceTextMarco(
'[[ ? $a > 1 | "yes" | "no" ? ]]',
{},
)
expect(res).toBe('no')
})
test('case 3', async () => {
const res = await service.replaceTextMarco(
'[[ ? $$$ > 1 | "yes" | "no" ? ]]',
{ $$: 21 },
)
expect(res).toBe('yes')
})
})
describe('test function', () => {
test('case 1', async () => {
const res = await service.replaceTextMarco(
"[[ #dayjs($created).format('YYYY-MM-DD') ]]",
{ created: new Date() },
)
expect(res).toBe(dayjs().format('YYYY-MM-DD'))
})
test('case 2', async () => {
const date = new Date()
const res = await service.replaceTextMarco('[[ #$date.toISOString() ]]', {
date,
})
expect(res).toBe(date.toISOString())
})
test('case 2', async () => {
const updated = new Date('2020-01-01')
const res = await service.replaceTextMarco(
'更新于 [[ #dayjs($updated).fromNow() ]]',
{ updated },
)
expect(res).toBe(`更新于 ${dayjs(updated).fromNow()}`)
})
test('case 3', async () => {
const updated = new Date('2020-01-01')
const res = await service.replaceTextMarco(
'更新于 [[ #fromNow($updated) ]]',
{ updated },
)
expect(res).toBe(`更新于 ${dayjs(updated).fromNow()}`)
})
test('case 4', async () => {
const created = new Date('2020-01-01')
const res = await service.replaceTextMarco(
'创建于 [[ #dayjs($created).format("YYYY-MM-DD") ]]',
{ created },
)
expect(res).toBe(`创建于 2020-01-01`)
})
})
})