feat: function eval marco
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
103
src/processors/helper/helper.marco.service.ts
Normal file
103
src/processors/helper/helper.marco.service.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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, '')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
88
test/src/processors/helper/helper.marco.service.spec.ts
Normal file
88
test/src/processors/helper/helper.marco.service.spec.ts
Normal 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`)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user