diff --git a/src/modules/aggregate/aggregate.interface.ts b/src/modules/aggregate/aggregate.interface.ts
index 04b08fc1..01594824 100644
--- a/src/modules/aggregate/aggregate.interface.ts
+++ b/src/modules/aggregate/aggregate.interface.ts
@@ -8,5 +8,6 @@ export interface RSSProps {
link: string
title: string
text: string
+ id: string
}[]
}
diff --git a/src/modules/aggregate/aggregate.service.ts b/src/modules/aggregate/aggregate.service.ts
index 6e6ec18a..c8b93260 100644
--- a/src/modules/aggregate/aggregate.service.ts
+++ b/src/modules/aggregate/aggregate.service.ts
@@ -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!,
diff --git a/src/modules/configs/configs.dto.ts b/src/modules/configs/configs.dto.ts
index dbf063e8..b0450eb0 100644
--- a/src/modules/configs/configs.dto.ts
+++ b/src/modules/configs/configs.dto.ts
@@ -257,3 +257,11 @@ export class FriendLinkOptionsDto {
@JSONSchemaToggleField('允许申请友链')
allowApply: boolean
}
+
+@JSONSchema({ title: '文本设定' })
+export class TextOptionsDto {
+ @IsBoolean()
+ @IsOptional()
+ @JSONSchemaToggleField('开启文本宏替换')
+ macros: boolean
+}
diff --git a/src/modules/configs/configs.interface.ts b/src/modules/configs/configs.interface.ts
index 02c47548..7af30392 100644
--- a/src/modules/configs/configs.interface.ts
+++ b/src/modules/configs/configs.interface.ts
@@ -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
diff --git a/src/modules/configs/configs.service.ts b/src/modules/configs/configs.service.ts
index 7b67900c..f47e5203 100644
--- a/src/modules/configs/configs.service.ts
+++ b/src/modules/configs/configs.service.ts
@@ -74,6 +74,9 @@ const generateDefaultConfig: () => IConfig = () => ({
terminalOptions: {
enable: false,
},
+ textOptions: {
+ macros: false,
+ },
})
@Injectable()
diff --git a/src/modules/feed/feed.controller.ts b/src/modules/feed/feed.controller.ts
index 73926ab6..71c42543 100644
--- a/src/modules/feed/feed.controller.ts
+++ b/src/modules/feed/feed.controller.ts
@@ -49,8 +49,8 @@ export class FeedController {
${title}
${xss(url)}
- ${data
- .map((item) => {
+ ${await Promise.all(
+ data.map(async (item) => {
return `
${item.title}
@@ -61,7 +61,9 @@ export class FeedController {
${`该渲染由 marked 生成, 可能存在部分语句不通或者排版问题, 最佳体验请前往: ${xss(item.link)}
- ${this.markdownService.renderMarkdownContent(item.text)}
+ ${await this.markdownService
+ .renderArticle(item.id)
+ .then((res) => res.html)}
看完了?说点什么呢
`}
@@ -69,8 +71,8 @@ export class FeedController {
`
- })
- .join('')}
+ }),
+ ).then((res) => res.join(''))}
`
return xml
diff --git a/src/modules/markdown/markdown.controller.ts b/src/modules/markdown/markdown.controller.ts
index ff89f518..7d116f92 100644
--- a/src/modules/markdown/markdown.controller.ts
+++ b/src/modules/markdown/markdown.controller.ts
@@ -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')
diff --git a/src/modules/note/note.controller.ts b/src/modules/note/note.controller.ts
index 2b5451f1..f777df19 100644
--- a/src/modules/note/note.controller.ts
+++ b/src/modules/note/note.controller.ts
@@ -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,
- })
- }
}
diff --git a/src/modules/note/note.service.ts b/src/modules/note/note.service.ts
index 217c692d..7a486f7a 100644
--- a/src/modules/note/note.service.ts
+++ b/src/modules/note/note.service.ts
@@ -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,
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,
+ },
+ )
}),
])
})
diff --git a/src/modules/post/post.service.ts b/src/modules/post/post.service.ts
index 773ef329..d4a49489 100644
--- a/src/modules/post/post.service.ts
+++ b/src/modules/post/post.service.ts
@@ -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,
+ }),
])
})
diff --git a/src/processors/helper/helper.macro.service.ts b/src/processors/helper/helper.macro.service.ts
index c89621a9..185d658c 100644
--- a/src/processors/helper/helper.macro.service.ts
+++ b/src/processors/helper/helper.macro.service.ts
@@ -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(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(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 = {},
): Promise {
+ 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
+
+ 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 : ''
- },
- })
- }
-}