diff --git a/docs/.vuepress/notes/zh/theme-guide.ts b/docs/.vuepress/notes/zh/theme-guide.ts
index 09a96462..0f35a093 100644
--- a/docs/.vuepress/notes/zh/theme-guide.ts
+++ b/docs/.vuepress/notes/zh/theme-guide.ts
@@ -48,6 +48,7 @@ export const themeGuide = defineNoteConfig({
'collapse',
'npm-to',
'caniuse',
+ 'chat',
'include',
],
},
diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts
index aea313f1..1d98e043 100644
--- a/docs/.vuepress/theme.ts
+++ b/docs/.vuepress/theme.ts
@@ -30,6 +30,7 @@ export const theme: Theme = plumeTheme({
abbr: true,
timeline: true,
collapse: true,
+ chat: true,
imageSize: 'all',
pdf: true,
caniuse: true,
diff --git a/docs/notes/theme/config/markdown.md b/docs/notes/theme/config/markdown.md
index 45babe5e..82446eec 100644
--- a/docs/notes/theme/config/markdown.md
+++ b/docs/notes/theme/config/markdown.md
@@ -167,6 +167,12 @@ export default defineUserConfig({
- **默认值**: `false`
- **详情**: 是否启用折叠面板容器语法
+### chat
+
+- **类型**: `boolean`
+- **默认值**: `false`
+- **详情**: 是否启用对话记录容器
+
### demo
- **类型**: `boolean`
diff --git a/docs/notes/theme/guide/markdown/chat.md b/docs/notes/theme/guide/markdown/chat.md
new file mode 100644
index 00000000..c8a5b25a
--- /dev/null
+++ b/docs/notes/theme/guide/markdown/chat.md
@@ -0,0 +1,123 @@
+---
+title: 对话记录
+icon: cil:chat-bubble
+createTime: 2025/03/24 21:40:18
+permalink: /guide/markdown/chat/
+badge:
+ type: tip
+ text: 1.0.0-rc.138
+---
+
+## 前言
+
+::: chat title="阿 B"
+{:2025-03-24 10:15:00}
+
+{阿 B}
+在文档里放聊天记录截图还是太难看了,有没有更好的方法?\[doge\]
+
+{.}
+有的,兄弟,包有的
+
+{.}
+但是挂聊天记录真的没问题吗?
+
+{阿 B}
+祖安对线,战绩可查 \[doge\]
+
+{:2025-03-24 15:32:00}
+
+{.}
+好消息:文档支持聊天记录了!
+
+{.}
+坏消息:我把你挂出来了 \[doge\]
+
+{阿 B}
+???
+:::
+
+## 概述
+
+在 Markdown 中,使用 `:: chat` 容器包裹带有特定标记的文本内容,可以在文档中显示 ==聊天记录==。
+
+::: warning 这是一个大多数时候都用不上的功能,需要使用时请斟酌是否要这么做,对于涉及隐私的内容请自行过滤。
+:::
+
+## 启用
+
+该功能默认不启用,你需要在 `theme` 配置中启用。
+
+```ts title=".vuepress/config.ts"
+export default defineUserConfig({
+ theme: plumeTheme({
+ markdown: {
+ chat: true, // [!code ++]
+ }
+ })
+})
+```
+
+## 使用
+
+在 `::: chat` 容器中,使用特定的标记,标识消息的发送者和发送时间,然后在文档中显示聊天记录。
+
+```md
+::: chat title="标题"
+{:date}
+
+{username}
+xxx
+
+{.}
+xxx
+:::
+```
+
+- `{:date}` 标记起始时间 (可选)。使用 `{:` + date + `}` 标记,date 可以为常见的日期格式。
+
+ 主题不对 `date` 做任何处理,只是简单的渲染。
+
+- `{username}` 标记后续内容的发送者,使用 `{` + username + `}` 标记,username 可以为任意字符串。
+
+- `{.}` 标记为本人发送
+
+## 示例
+
+__输入:__
+
+``` md
+::: chat title="标题"
+{:2025-03-24 10:15:00}
+
+{用户一}
+用户一的消息
+
+{.}
+本人的消息
+
+{用户二}
+用户二的消息
+
+{.}
+本人的消息
+:::
+```
+
+__输出:__
+
+::: chat title="标题"
+{:2025-03-24 10:15:00}
+
+{用户一}
+用户一的消息
+
+{.}
+本人的消息
+
+{用户二}
+用户二的消息
+
+{.}
+本人的消息
+:::
diff --git a/plugins/plugin-md-power/__test__/__snapshots__/chatPlugin.spec.ts.snap b/plugins/plugin-md-power/__test__/__snapshots__/chatPlugin.spec.ts.snap
new file mode 100644
index 00000000..d79f075c
--- /dev/null
+++ b/plugins/plugin-md-power/__test__/__snapshots__/chatPlugin.spec.ts.snap
@@ -0,0 +1,147 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`chatPlugin > should not work 1`] = `
+"
::: char
+not work
+:::
+"
+`;
+
+exports[`chatPlugin > should work 1`] = `
+"
+
+
+
2025-06-01 12:00:00
+
+
+
+
"
+`;
+
+exports[`chatPlugin > should work multiple date and user 1`] = `
+"
+
+
+
2025-06-01 12:00:00
+
+
+
+
+
2025-06-05 12:00:00
+
+
+
+
+
+
"
+`;
+
+exports[`chatPlugin > should work multiple users 1`] = `
+"
+
+
+
2025-06-01 12:00:00
+
+
+
+
+
+
"
+`;
diff --git a/plugins/plugin-md-power/__test__/chatPlugin.spec.ts b/plugins/plugin-md-power/__test__/chatPlugin.spec.ts
new file mode 100644
index 00000000..48532644
--- /dev/null
+++ b/plugins/plugin-md-power/__test__/chatPlugin.spec.ts
@@ -0,0 +1,82 @@
+import MarkdownIt from 'markdown-it'
+import { describe, expect, it } from 'vitest'
+import { chatPlugin } from '../src/node/container/chat.js'
+
+describe('chatPlugin', () => {
+ const md = new MarkdownIt().use(chatPlugin)
+ it('should work', () => {
+ const code = `\
+::: chat title="用户聊天"
+{:2025-06-01 12:00:00}
+{用户一}
+这是用户一的消息
+{.}
+这是本人的消息
+:::
+`
+ expect(md.render(code)).toMatchSnapshot()
+ })
+
+ it('should work multiple users', () => {
+ const code = `\
+::: chat
+{:2025-06-01 12:00:00}
+{用户一}
+这是用户一的消息
+
+{.}
+这是本人的消息
+
+{用户二}
+这是用户二的消息
+
+{.}
+这是本人的消息
+:::
+`
+ expect(md.render(code)).toMatchSnapshot()
+ })
+
+ it('should work multiple date and user', () => {
+ const code = `\
+::: chat
+
+{:2025-06-01 12:00:00}
+{用户一}
+这是用户一的消息
+
+{.}
+这是本人的消息
+
+{用户二}
+这是用户二的消息
+
+{.}
+这是本人的消息
+
+{:2025-06-05 12:00:00}
+{用户一}
+这是用户一的消息
+
+{.}
+这是本人的消息
+
+{用户二}
+这是用户二的消息
+
+{.}
+这是本人的消息
+:::
+`
+ expect(md.render(code)).toMatchSnapshot()
+ })
+
+ it('should not work', () => {
+ const code = `\
+::: char
+not work
+:::
+`
+ expect(md.render(code)).toMatchSnapshot()
+ })
+})
diff --git a/plugins/plugin-md-power/src/client/styles/chat.css b/plugins/plugin-md-power/src/client/styles/chat.css
new file mode 100644
index 00000000..5300eece
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/styles/chat.css
@@ -0,0 +1,129 @@
+:root {
+ --vp-chat-c-bg: var(--vp-c-bg-soft);
+ --vp-chat-c-bg-header: var(--vp-c-bg-soft);
+ --vp-chat-c-bg-content: var(--vp-c-bg-soft);
+ --vp-chat-c-bg-user: var(--vp-c-bg);
+ --vp-chat-c-bg-self: var(--vp-c-brand-soft);
+ --vp-chat-c-title: var(--vp-c-text-1);
+ --vp-chat-c-text: var(--vp-c-text-1);
+ --vp-chat-c-date: var(--vp-c-text-3);
+ --vp-chat-c-username: var(--vp-c-text-2);
+}
+
+.vp-chat {
+ width: 100%;
+ margin: 16px 0;
+ overflow: hidden;
+ background-color: var(--vp-chat-c-bg);
+ border-radius: 6px;
+ transition: background-color var(--vp-t-color);
+}
+
+@media (min-width: 640px) {
+ .vp-chat {
+ width: 320px;
+ margin: 16px auto;
+ }
+}
+
+@media (min-width: 768px) {
+ .vp-chat {
+ width: 360px;
+ }
+}
+
+@media (min-width: 960px) {
+ .vp-chat {
+ width: 480px;
+ }
+}
+
+.vp-chat .vp-chat-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 44px;
+ background-color: var(--vp-chat-c-bg-header);
+ transition: background-color var(--vp-t-color);
+}
+
+.vp-chat .vp-chat-title {
+ flex: 1 2;
+ font-weight: 600;
+ color: var(--vp-chat-c-title);
+ text-align: center;
+ transition: color var(--vp-t-color);
+}
+
+.vp-chat .vp-chat-content {
+ padding: 0 16px 24px;
+ background-color: var(--vp-chat-c-bg-content);
+ transition: background-color var(--vp-t-color);
+}
+
+.vp-chat .vp-chat-date {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 16px 0;
+ font-size: 12px;
+ color: var(--vp-chat-c-date);
+ transition: color var(--vp-t-color);
+}
+
+.vp-chat .vp-chat-message {
+ display: flex;
+ margin-bottom: 16px;
+}
+
+.vp-chat .vp-chat-message.self {
+ justify-content: flex-end;
+}
+
+.vp-chat .vp-chat-message:last-child {
+ margin-bottom: 0;
+}
+
+.vp-chat .vp-chat-message-body {
+ flex-shrink: 2;
+ padding-right: 32px;
+}
+
+.vp-chat .vp-chat-message.self .vp-chat-message-body {
+ padding-right: 0;
+ padding-left: 32px;
+}
+
+.vp-chat .vp-chat-username {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--vp-chat-c-username);
+}
+
+.vp-chat .vp-chat-message-body .message-content {
+ max-width: 100%;
+ padding: 8px 16px;
+ font-size: 14px;
+ line-height: 22px;
+ color: var(--vp-chat-c-text);
+ background-color: var(--vp-chat-c-bg-user);
+ border-radius: 6px;
+ box-shadow: var(--vp-shadow-1);
+}
+
+.vp-chat .vp-chat-message.self .vp-chat-message-body .message-content {
+ background-color: var(--vp-chat-c-bg-self);
+}
+
+.vp-chat .vp-chat-message-body .message-content :where(p, ul, ol) {
+ margin: 8px 0;
+}
+
+.vp-chat .vp-chat-message-body .message-content :first-child {
+ margin-top: 0;
+}
+
+.vp-chat .vp-chat-message-body .message-content :last-child {
+ margin-bottom: 0;
+}
diff --git a/plugins/plugin-md-power/src/node/container/chat.ts b/plugins/plugin-md-power/src/node/container/chat.ts
new file mode 100644
index 00000000..d66506af
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/container/chat.ts
@@ -0,0 +1,144 @@
+/**
+ * ::: chat
+ * {:time}
+ *
+ * {user-1}
+ * xxxxxxxx
+ *
+ * {.}
+ * xxxxxx
+ * :::
+ */
+import type { PluginSimple } from 'markdown-it'
+import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs'
+import type Token from 'markdown-it/lib/token.mjs'
+import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
+import { resolveAttrs } from '.././utils/resolveAttrs.js'
+import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
+
+interface ChatMeta {
+ title?: string
+}
+
+interface ChatMessage {
+ sender: 'user' | 'self'
+ date: string
+ username: string
+ content: string[]
+}
+
+export const chatPlugin: PluginSimple = (md) => {
+ md.block.ruler.before('fence', 'chat_def', chatDef)
+
+ md.renderer.rules.chat_container = (tokens: Token[], idx: number, _, env) => {
+ const { meta, content } = tokens[idx]
+ const { title } = meta as ChatMeta
+ const messages = parseChatContent(content)
+ return `
+
+
+ ${chatMessagesRender(md, env, messages)}
+
+
`
+ }
+}
+
+function chatDef(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
+ const start = state.bMarks[startLine] + state.tShift[startLine]
+ const max = state.eMarks[startLine]
+ let pos = start
+
+ if (state.src.slice(pos, pos + 3) !== ':::')
+ return false
+
+ pos += 3
+
+ const info = state.src.slice(start + 3, max).trim()
+ if (!info.startsWith('chat'))
+ return false
+
+ /* istanbul ignore if -- @preserve */
+ if (silent)
+ return true
+
+ let line = startLine
+ let content = ''
+ while (++line < endLine) {
+ if (state.src.slice(state.bMarks[line], state.eMarks[line]).trim() === ':::') {
+ break
+ }
+
+ content += `${state.src.slice(state.bMarks[line], state.eMarks[line])}\n`
+ }
+
+ const token = state.push('chat_container', '', 0)
+ token.meta = resolveAttrs(info).attrs
+ token.content = content
+ token.markup = '::: chat'
+ token.map = [startLine, line + 1]
+
+ state.line = line + 1
+
+ return true
+}
+
+function chatMessagesRender(md: Markdown, env: MarkdownEnv, messages: ChatMessage[]): string {
+ let currentDate = ''
+ return messages.map(({ sender, username, date, content }) => {
+ let messageContent = ''
+ if (!currentDate || currentDate !== date) {
+ currentDate = date
+ messageContent += `${currentDate}
\n`
+ }
+ messageContent += `
+
\
+ ${sender === 'user' ? `\n
${username}
` : ''}
+
+ ${md.render(content.join('\n'), cleanMarkdownEnv(env)).trim()}
+
+
+
`
+
+ return messageContent
+ })
+ .join('\n')
+}
+
+// 解析聊天内容的核心逻辑
+function parseChatContent(content: string): ChatMessage[] {
+ const lines = content.split('\n')
+ const messages: ChatMessage[] = []
+
+ let currentDate = ''
+ let message!: ChatMessage
+
+ for (const line of lines) {
+ const lineStr = line.trim()
+ // 解析时间标记
+ if (lineStr.startsWith('{:') && lineStr.endsWith('}')) {
+ currentDate = lineStr.slice(2, -1).trim()
+ continue
+ }
+
+ // 解析用户 / 本人标记
+ if (lineStr.startsWith('{') && lineStr.endsWith('}')) {
+ const username = lineStr.slice(1, -1).trim()
+ message = {
+ sender: username === '.' ? 'self' : 'user',
+ username,
+ date: currentDate,
+ content: [],
+ }
+ messages.push(message)
+ continue
+ }
+
+ // 收集消息内容
+ if (message?.sender) {
+ message.content.push(line)
+ }
+ }
+ return messages
+}
diff --git a/plugins/plugin-md-power/src/node/container/index.ts b/plugins/plugin-md-power/src/node/container/index.ts
index fb9b8af3..4f6fdc0b 100644
--- a/plugins/plugin-md-power/src/node/container/index.ts
+++ b/plugins/plugin-md-power/src/node/container/index.ts
@@ -4,6 +4,7 @@ import type { MarkdownPowerPluginOptions } from '../../shared/index.js'
import { isPlainObject } from '@vuepress/helper'
import { alignPlugin } from './align.js'
import { cardPlugin } from './card.js'
+import { chatPlugin } from './chat.js'
import { codeTabs } from './codeTabs.js'
import { collapsePlugin } from './collapse.js'
import { demoWrapperPlugin } from './demoWrapper.js'
@@ -54,4 +55,7 @@ export async function containerPlugin(
if (options.collapse)
collapsePlugin(md)
+
+ if (options.chat)
+ chatPlugin(md)
}
diff --git a/plugins/plugin-md-power/src/node/prepareConfigFile.ts b/plugins/plugin-md-power/src/node/prepareConfigFile.ts
index fa8c0987..621770e9 100644
--- a/plugins/plugin-md-power/src/node/prepareConfigFile.ts
+++ b/plugins/plugin-md-power/src/node/prepareConfigFile.ts
@@ -116,6 +116,10 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('VPCollapseItem', VPCollapseItem)`)
}
+ if (options.chat) {
+ imports.add(`import '${CLIENT_FOLDER}styles/chat.css'`)
+ }
+
return app.writeTemp(
'md-power/config.js',
`\
diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts
index ed0cda20..79a783d4 100644
--- a/plugins/plugin-md-power/src/shared/plugin.ts
+++ b/plugins/plugin-md-power/src/shared/plugin.ts
@@ -91,6 +91,24 @@ export interface MarkdownPowerPluginOptions {
*/
collapse?: boolean
+ /**
+ * 是否启用 chat 容器 语法
+ *
+ * ```md
+ * ::: chat
+ * {:date}
+ *
+ * {user}
+ * message
+ *
+ * {.}
+ * message
+ * :::
+ * ```
+ * @default false
+ */
+ chat?: boolean
+
// video embed
/**
* 是否启用 bilibili 视频嵌入
diff --git a/theme/src/node/detector/fields.ts b/theme/src/node/detector/fields.ts
index ac33c761..9ddbe947 100644
--- a/theme/src/node/detector/fields.ts
+++ b/theme/src/node/detector/fields.ts
@@ -59,6 +59,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
'replit',
'timeline',
'collapse',
+ 'chat',
'youtube',
]