refactor: text macro

This commit is contained in:
Innei
2022-04-30 23:15:56 +08:00
parent 80bb68e0aa
commit 8f786bc721
11 changed files with 174 additions and 118 deletions

View File

@@ -8,5 +8,6 @@ export interface RSSProps {
link: string
title: string
text: string
id: string
}[]
}

View File

@@ -288,6 +288,7 @@ export class AggregateService {
const postsRss: RSSProps['data'] = posts.map((post) => {
return {
id: post._id,
title: post.title,
text: post.text,
created: post.created!,
@@ -303,6 +304,7 @@ export class AggregateService {
? dayjs(note.secret).isAfter(new Date())
: false
return {
id: note._id,
title: note.title,
text: isSecret ? '这篇文章暂时没有公开呢' : note.text,
created: note.created!,

View File

@@ -257,3 +257,11 @@ export class FriendLinkOptionsDto {
@JSONSchemaToggleField('允许申请友链')
allowApply: boolean
}
@JSONSchema({ title: '文本设定' })
export class TextOptionsDto {
@IsBoolean()
@IsOptional()
@JSONSchemaToggleField('开启文本宏替换')
macros: boolean
}

View File

@@ -12,6 +12,7 @@ import {
MailOptionsDto,
SeoDto,
TerminalOptionsDto,
TextOptionsDto,
UrlDto,
} from './configs.dto'
@@ -49,6 +50,10 @@ export abstract class IConfig {
@Type(() => TerminalOptionsDto)
@ValidateNested()
terminalOptions: TerminalOptionsDto
@Type(() => TextOptionsDto)
@ValidateNested()
textOptions: TextOptionsDto
}
export type IConfigKeys = keyof IConfig

View File

@@ -74,6 +74,9 @@ const generateDefaultConfig: () => IConfig = () => ({
terminalOptions: {
enable: false,
},
textOptions: {
macros: false,
},
})
@Injectable()

View File

@@ -49,8 +49,8 @@ export class FeedController {
<title>${title}</title>
<link>${xss(url)}</link>
</image>
${data
.map((item) => {
${await Promise.all(
data.map(async (item) => {
return `<entry>
<title>${item.title}</title>
<link href='${xss(item.link)}'/>
@@ -61,7 +61,9 @@ export class FeedController {
${`<blockquote>该渲染由 marked 生成, 可能存在部分语句不通或者排版问题, 最佳体验请前往: <a href='${xss(
item.link,
)}'>${xss(item.link)}</a></blockquote>
${this.markdownService.renderMarkdownContent(item.text)}
${await this.markdownService
.renderArticle(item.id)
.then((res) => res.html)}
<p style='text-align: right'>
<a href='${`${xss(item.link)}#comments`}'>看完了?说点什么呢</a>
</p>`}
@@ -69,8 +71,8 @@ export class FeedController {
</content>
</entry>
`
})
.join('')}
}),
).then((res) => res.join(''))}
</feed>`
return xml

View File

@@ -28,7 +28,6 @@ import { HTTPDecorators } from '~/common/decorator/http.decorator'
import { ApiName } from '~/common/decorator/openapi.decorator'
import { IsMaster } from '~/common/decorator/role.decorator'
import { ArticleTypeEnum } from '~/constants/article.constant'
import { TextMacroService } from '~/processors/helper/helper.macro.service'
import { MongoIdDto } from '~/shared/dto/id.dto'
import { getShortDateTime } from '~/utils'
@@ -52,7 +51,6 @@ export class MarkdownController {
private readonly service: MarkdownService,
private readonly configs: ConfigsService,
private readonly macroService: TextMacroService,
) {}
@Post('/import')

View File

@@ -21,6 +21,7 @@ import { IsMaster } from '~/common/decorator/role.decorator'
import { VisitDocument } from '~/common/decorator/update-count.decorator'
import { CannotFindException } from '~/common/exceptions/cant-find.exception'
import { CountingService } from '~/processors/helper/helper.counting.service'
import { TextMacroService } from '~/processors/helper/helper.macro.service'
import { IntIdOrMongoIdDto, MongoIdDto } from '~/shared/dto/id.dto'
import {
addHidePasswordAndHideCondition,
@@ -42,6 +43,8 @@ export class NoteController {
constructor(
private readonly noteService: NoteService,
private readonly countingService: CountingService,
private readonly macrosService: TextMacroService,
) {}
@Get('/latest')
@@ -228,6 +231,11 @@ export class NoteController {
if (!current) {
throw new CannotFindException()
}
current.text = await this.macrosService.replaceTextMacro(
current.text,
current,
)
if (
!this.noteService.checkPasswordToAccess(current, password) &&
!isMaster
@@ -263,17 +271,4 @@ export class NoteController {
delete current.password
return { data: current, next, prev }
}
@ApiOperation({ summary: '根据 nid 修改' })
@Put('/nid/:nid')
@Auth()
async modifyNoteByNid(@Param() params: NidType, @Body() body: NoteModel) {
const id = await this.noteService.getIdByNid(params.nid)
if (!id) {
throw new CannotFindException()
}
return await this.modify(body, {
id,
})
}
}

View File

@@ -9,6 +9,7 @@ import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
import { EventBusEvents } from '~/constants/event-bus.constant'
import { EventManagerService } from '~/processors/helper/helper.event.service'
import { ImageService } from '~/processors/helper/helper.image.service'
import { TextMacroService } from '~/processors/helper/helper.macro.service'
import { InjectModel } from '~/transformers/model.transformer'
import { deleteKeys } from '~/utils'
@@ -21,6 +22,8 @@ export class NoteService {
private readonly noteModel: MongooseModel<NoteModel>,
private readonly imageService: ImageService,
private readonly eventManager: EventManagerService,
private readonly textMacrosService: TextMacroService,
) {
this.needCreateDefult()
}
@@ -45,6 +48,11 @@ export class NoteService {
throw new CannotFindException()
}
latest.text = await this.textMacrosService.replaceTextMacro(
latest.text,
latest,
)
// 是否存在上一条记录 (旧记录)
// 统一: next 为较老的记录 prev 为较新的记录
// FIXME may cause bug
@@ -99,9 +107,15 @@ export class NoteService {
)
: this.eventManager.broadcast(
BusinessEvents.NOTE_CREATE,
doc.toJSON(),
{
scope: EventScope.TO_SYSTEM_VISITOR,
...doc.toJSON(),
text: await this.textMacrosService.replaceTextMacro(
doc.text,
doc,
),
},
{
scope: EventScope.TO_VISITOR,
},
),
])
@@ -129,14 +143,28 @@ export class NoteService {
})
await Promise.all([
this.imageService.recordImageDimensions(this.noteModel, id),
this.model.findById(id).then((doc) => {
this.model.findById(id).then(async (doc) => {
if (!doc) {
return
}
delete doc.password
this.eventManager.broadcast(BusinessEvents.NOTE_UPDATE, doc, {
scope: EventScope.TO_SYSTEM_VISITOR,
scope: EventScope.TO_SYSTEM,
})
this.eventManager.broadcast(
BusinessEvents.NOTE_UPDATE,
{
...doc,
text: await this.textMacrosService.replaceTextMacro(
doc.text,
doc,
),
},
{
scope: EventScope.TO_VISITOR,
},
)
}),
])
})

View File

@@ -17,6 +17,7 @@ import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { EventBusEvents } from '~/constants/event-bus.constant'
import { EventManagerService } from '~/processors/helper/helper.event.service'
import { ImageService } from '~/processors/helper/helper.image.service'
import { TextMacroService } from '~/processors/helper/helper.macro.service'
import { InjectModel } from '~/transformers/model.transformer'
import { CategoryService } from '../category/category.service'
@@ -35,6 +36,7 @@ export class PostService {
private categoryService: CategoryService,
private readonly imageService: ImageService,
private readonly eventManager: EventManagerService,
private readonly textMacroService: TextMacroService,
) {}
get model() {
@@ -70,18 +72,30 @@ export class PostService {
})
process.nextTick(async () => {
this.eventManager.emit(EventBusEvents.CleanAggregateCache, null, {
scope: EventScope.TO_SYSTEM,
})
const doc = res.toJSON()
await Promise.all([
this.eventManager.emit(EventBusEvents.CleanAggregateCache, null, {
scope: EventScope.TO_SYSTEM,
}),
this.eventManager.broadcast(
BusinessEvents.POST_CREATE,
{
...res.toJSON(),
...doc,
category,
},
{
scope: EventScope.TO_SYSTEM_VISITOR,
scope: EventScope.TO_SYSTEM,
},
),
this.eventManager.broadcast(
BusinessEvents.POST_CREATE,
{
...doc,
category,
text: await this.textMacroService.replaceTextMacro(doc.text, doc),
},
{
scope: EventScope.TO_SYSTEM,
},
),
this.imageService.recordImageDimensions(this.postModel, res._id),
@@ -131,20 +145,27 @@ export class PostService {
Object.assign(originDocument, omit(data, PostModel.protectedKeys))
await originDocument.save()
process.nextTick(async () => {
this.eventManager.emit(EventBusEvents.CleanAggregateCache, null, {
scope: EventScope.TO_SYSTEM,
})
const doc = await this.postModel.findById(id).lean({ getters: true })
// 更新图片信息缓存
await Promise.all([
this.eventManager.emit(EventBusEvents.CleanAggregateCache, null, {
scope: EventScope.TO_SYSTEM,
}),
this.imageService.recordImageDimensions(this.postModel, id),
this.eventManager.broadcast(
BusinessEvents.POST_UPDATE,
await this.postModel.findById(id).lean({ getters: true }),
{
scope: EventScope.TO_SYSTEM_VISITOR,
},
),
doc &&
this.eventManager.broadcast(
BusinessEvents.POST_UPDATE,
{
...doc,
text: await this.textMacroService.replaceTextMacro(doc.text, doc),
},
{
scope: EventScope.TO_VISITOR,
},
),
this.eventManager.broadcast(BusinessEvents.POST_UPDATE, doc, {
scope: EventScope.TO_SYSTEM,
}),
])
})

View File

@@ -1,29 +1,25 @@
import dayjs from 'dayjs'
import { FastifyRequest } from 'fastify'
import { marked } from 'marked'
import {
BadRequestException,
Inject,
Injectable,
Logger,
Scope,
} from '@nestjs/common'
import { REQUEST } from '@nestjs/core'
import { BadRequestException, Injectable, Logger } from '@nestjs/common'
import { UserModel } from '~/modules/user/user.model'
import { ConfigsService } from '~/modules/configs/configs.service'
import { deepCloneWithFunction } from '~/utils'
import { safeEval } from '~/utils/safe-eval.util'
const logger = new Logger('TextMacroService')
const RegMap = {
'#': /^#(.*?)$/g,
$: /^\$(.*?)$/g,
'?': /^\?\??(.*?)\??\?$/g,
} as const
class HelperStatic {
public ifConditionGrammar<T extends object>(text: string, model: T) {
@Injectable()
export class TextMacroService {
private readonly logger: Logger
constructor(private readonly configService: ConfigsService) {
this.logger = new Logger(TextMacroService.name)
}
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, '')
@@ -108,80 +104,77 @@ class HelperStatic {
extraContext: Record<string, any> = {},
): Promise<string> {
const { macros } = await this.configService.get('textOptions')
if (!macros) {
return text
}
try {
const matchedReg = /\[\[\s(.*?)\s\]\]/g
if (text.search(matchedReg) != -1) {
text = text.replace(matchedReg, (match, condition) => {
const ast = marked.lexer(text)
// 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)
}
})
const matched = text.search(matchedReg) != -1
if (isInCodeBlock) {
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] ?? extraContext[variable]
}
// 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], ...acc }),
{},
)
try {
const result = safeEval(
`return ${functions}`,
this.generateFunctionContext({ ...variables, ...extraContext }),
{ timeout: 1000 },
)
cacheMap[functions] = result
return result
} catch {
return match
}
}
})
condition = condition?.trim()
if (condition.search(RegMap['?']) != -1) {
return helper.ifConditionGrammar(condition, model)
}
if (condition.search(RegMap['$']) != -1) {
const variable = condition
.replace(RegMap['$'], '$1')
.replace(/\s/g, '')
return model[variable] ?? extraContext[variable]
}
// 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')
const variables = Object.keys(model).reduce(
(acc, key) => ({ [`$${key}`]: model[key], ...acc }),
{},
)
try {
return safeEval(
`return ${functions}`,
this.generateFunctionContext({ ...variables, ...extraContext }),
{ timeout: 1000 },
)
} catch {
return match
}
}
})
}
return text
} catch (err) {
logger.log(err.message)
this.logger.log(err.message)
return text
}
}
}
const helper = new HelperStatic()
@Injectable({ scope: Scope.REQUEST })
export class TextMacroService {
constructor(
@Inject(REQUEST)
private readonly request: FastifyRequest & {
isMaster: boolean
user: UserModel
},
) {}
public replaceTextMacro(text: string, model: object) {
const isMaster = this.request.isMaster
return helper.replaceTextMacro(text, model, {
hideForGuest(text: string) {
return isMaster ? text : ''
},
})
}
}