diff --git a/src/global/dayjs.global.ts b/src/global/dayjs.global.ts index 7dab8900..59c96c54 100644 --- a/src/global/dayjs.global.ts +++ b/src/global/dayjs.global.ts @@ -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) diff --git a/src/modules/serverless/serverless.service.ts b/src/modules/serverless/serverless.service.ts index df9c02a3..0e29b00e 100644 --- a/src/modules/serverless/serverless.service.ts +++ b/src/modules/serverless/serverless.service.ts @@ -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) => { diff --git a/src/processors/helper/helper.marco.service.ts b/src/processors/helper/helper.marco.service.ts new file mode 100644 index 00000000..d470feea --- /dev/null +++ b/src/processors/helper/helper.marco.service.ts @@ -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(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( + text: string, + model: T, + ): Promise { + 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 + } +} diff --git a/src/utils/macros.util.ts b/src/utils/macros.util.ts index 64c70d68..cb209701 100644 --- a/src/utils/macros.util.ts +++ b/src/utils/macros.util.ts @@ -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, '') diff --git a/src/utils/tool.util.ts b/src/utils/tool.util.ts index ff1140da..b794e74c 100644 --- a/src/utils/tool.util.ts +++ b/src/utils/tool.util.ts @@ -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 = (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 +} diff --git a/test/src/processors/helper/helper.marco.service.spec.ts b/test/src/processors/helper/helper.marco.service.spec.ts new file mode 100644 index 00000000..bf42f6f2 --- /dev/null +++ b/test/src/processors/helper/helper.marco.service.spec.ts @@ -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`) + }) + }) +})