diff --git a/docs/.vuepress/notes/zh/theme-guide.ts b/docs/.vuepress/notes/zh/theme-guide.ts
index 1920594b..64786f5d 100644
--- a/docs/.vuepress/notes/zh/theme-guide.ts
+++ b/docs/.vuepress/notes/zh/theme-guide.ts
@@ -43,6 +43,7 @@ export const themeGuide = defineNoteConfig({
'steps',
'file-tree',
'tabs',
+ 'timeline',
'demo-wrapper',
'npm-to',
'caniuse',
diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts
index da4dbd98..2095369b 100644
--- a/docs/.vuepress/theme.ts
+++ b/docs/.vuepress/theme.ts
@@ -28,6 +28,7 @@ export const theme: Theme = plumeTheme({
annotation: true,
abbr: true,
+ timeline: true,
imageSize: 'all',
pdf: true,
caniuse: true,
diff --git a/docs/notes/theme/config/markdown.md b/docs/notes/theme/config/markdown.md
index d90dc2ac..ce9b002a 100644
--- a/docs/notes/theme/config/markdown.md
+++ b/docs/notes/theme/config/markdown.md
@@ -155,6 +155,12 @@ export default defineUserConfig({
- **默认值**: `true`
- **详情**: 是否启用文件树容器语法
+### timeline
+
+- **类型**: `boolean`
+- **默认值**: `false`
+- **详情**: 是否启用时间线容器语法
+
### demo
- **类型**: `boolean`
diff --git a/docs/notes/theme/guide/markdown/timeline.md b/docs/notes/theme/guide/markdown/timeline.md
new file mode 100644
index 00000000..3b6b32bd
--- /dev/null
+++ b/docs/notes/theme/guide/markdown/timeline.md
@@ -0,0 +1,508 @@
+---
+title: 时间线
+icon: mdi:timeline-text-outline
+createTime: 2025/03/20 18:05:29
+permalink: /guide/markdown/timeline/
+badge:
+ text: 1.0.0-rc.137 +
+ type: tip
+---
+
+## 概述
+
+在 markdown 中,使用 `::: timeline` 容器,包含 markdown 无序列表语法,即可实现 ==时间线== 的 渲染效果。
+
+- 支持 ==水平方向== 和 ==垂直方向==
+- 垂直方向支持 __左对齐__,__右对齐__ 和 __两端对齐__
+- 支持 __图标__ 和 __线条样式__
+- 支持 通过预设 __类型__ 设置 __颜色__,支持自定义颜色
+
+## 启用
+
+该功能默认不启用,你需要在 `theme` 配置中启用。
+
+```ts title=".vuepress/config.ts"
+export default defineUserConfig({
+ theme: plumeTheme({
+ markdown: {
+ timeline: true, // [!code ++]
+ }
+ })
+})
+```
+
+## 使用
+
+在 `::: timeline` 容器中,使用 markdown 无序列表语法,列表的每一个项即 时间线上的每一个点。
+
+```md{1,9} title="timeline.md"
+::: timeline 配置
+- 标题 配置
+
+ 正文内容
+
+- 标题 配置
+
+ 正文内容
+:::
+```
+
+对于列表的每一个项:
+
+- __第一行__: 从起始位置定义 __标题__,在标题之后跟着 `key=value` 的格式配置时间点的 __属性__。
+- __后续行__: 正文内容,==请注意添加正确的缩进=={.important}。
+
+__一个简单的例子:__
+
+__输入:__
+
+```md
+::: timeline
+- 节点一 time=2025-03-20 type=success
+
+ 正文内容
+
+- 节点二 time=2025-02-21 type=warning
+
+ 正文内容
+
+- 节点三 time=2025-01-22 type=danger
+
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline
+
+- 节点一 time=2025-03-20 type=success
+
+ 正文内容
+
+- 节点二 time=2025-02-21 type=warning
+
+ 正文内容
+
+- 节点三 time=2025-01-22 type=danger
+
+ 正文内容
+:::
+
+::: important 时间线默认为垂直方向
+:::
+
+## 配置
+
+__时间线__ 支持非常灵活且灵活的配置项,配置主要分为两个部分:
+
+- __容器配置__: 在 `::: timeline` 容器上的配置,配置项跟随在 `::: timeline` 之后,如:
+
+ `::: timeline horizontal` 表示 渲染为 水平方向的时间线。
+
+- __列表项配置__: 在列表的每一个项上的配置,配置项列表项的第一行,跟随在标题之后,如:
+
+ `- 节点一 time=2025-03-20 type=success` 表示 时间点为 `2025-03-20`,节点类型为 `success`。
+
+### 容器配置
+
+#### horizontal
+
+- __类型:__ `boolean`
+- __默认值:__ `false`
+
+渲染为 水平方向的时间线。
+
+#### card
+
+- __类型:__ `boolean`
+- __默认值:__ `false`
+
+每个时间节点默认渲染为卡片样式(可在列表项配置中覆盖)。
+
+#### placement
+
+- __类型:__ `'left' | 'right' | 'between'`
+- __默认值:__ `'left'`
+
+时间节点的对齐方式。==仅在垂直方向时生效=={.warning}
+
+- `left` : 时间轴左侧对齐
+- `right` : 时间轴右侧对齐
+- `between` : 时间轴两端对齐 (通过列表项配置中的 `placement` 定义位置,默认为 `left`)
+
+#### line
+
+- __类型:__ `'solid' | 'dashed' | 'dotted'`
+- __默认值:__ `'solid'`
+
+线条样式(可在列表项配置中覆盖)
+
+### 列表项配置
+
+#### time
+
+- __类型:__ `string`
+- __默认值:__ `''`
+
+时间点,可以是任何字符串,比如 `2025-03-20`, `Q1` 等。
+
+#### type
+
+- __类型:__ `'info' | 'tip' | 'success' | 'warning' | 'danger' | 'caution' | 'important'`
+- __默认值:__ `'info'`
+
+时间节点的类型。
+
+#### card
+
+- __类型:__ `boolean`
+- __默认值:__ `false` 从 容器配置 `card` 中继承
+
+当前 时间节点渲染为卡片样式。
+
+#### line
+
+- __类型:__ `'solid' | 'dashed' | 'dotted'`
+- __默认值:__ `'solid'` 从 容器配置 `line` 中继承
+
+线条样式
+
+#### icon
+
+- __类型:__ `string`
+- __默认值:__ `''`
+
+时间节点的图标,支持所有的 [iconify](https://icon-sets.iconify.design/) 图标。
+
+#### placement
+
+- __类型:__ `'left' | 'right'`
+- __默认值:__ `'left'`
+
+当 容器配置为 `between` 时,定义当前时间节点的位置。
+
+- `left` : 在时间轴左侧
+- `right` : 在时间轴右侧
+
+#### color
+
+- __类型:__ `string`
+- __默认值:__ `''`
+
+时间节点线条颜色,可以是任何有效的颜色值。
+
+## 示例
+
+### 水平方向
+
+在 `:::timeline` 后跟随声明 `horizontal` , 即可将时间线渲染为 水平方向。
+
+__输入:__
+
+```md /horizontal/
+::: timeline horizontal
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline horizontal
+
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+
+:::
+
+### 右对齐
+
+在 `:::timeline` 后跟随声明 `placement="right"` , 即可将时间线渲染为 右对齐。
+
+__输入:__
+
+```md /placement="right"/
+::: timeline placement="right"
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline placement="right"
+
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+
+### 两端对齐
+
+在 `:::timeline` 后跟随声明 `placement="between"` , 即可将时间线渲染为 两端对齐。
+
+列表项默认位于时间线的左侧,可以通过 `placement="right"` 为列表项设置右侧位置。
+
+__输入:__
+
+```md /placement="between"/ /placement=right/
+::: timeline placement="between"
+- 节点一 time=2025-03-20 placement=right
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger placement=right
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline placement="between"
+
+- 节点一 time=2025-03-20 placement=right
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger placement=right
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+
+### 节点类型
+
+在列表项首行标题之后,添加 `type=节点类型` 可以为当前节点设置节点类型。
+
+__输入:__
+
+```md /type=success/ /type=warning/ /type=danger/ /type=important/
+::: timeline
+- 节点一 time=2025-03-20 type=success
+ 正文内容
+- 节点二 time=2025-04-20 type=warning
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline
+
+- 节点一 time=2025-03-20 type=success
+ 正文内容
+- 节点二 time=2025-04-20 type=warning
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+
+### 线条风格
+
+- 在容器配置中添加 `line=线条风格` 可以为所有节点设置默认线条风格。
+- 在列表项首行标题之后,添加 `line=线条风格` 可以为节点设置线条风格。
+
+__输入:__
+
+```md /line="dotted"/ /line=solid/ /line=dashed/
+::: timeline line="dotted"
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger line=dashed
+ 正文内容
+- 节点四 time=2025-01-22 type=important line=solid
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline line="dotted"
+
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success
+ 正文内容
+- 节点三 time=2025-01-22 type=danger line=dashed
+ 正文内容
+- 节点四 time=2025-01-22 type=important line=solid
+ 正文内容
+:::
+
+### 带图标的节点
+
+在列表项首行标题之后,添加 `icon=图标名称` 可以为节点添加图标。
+
+图标名称支持 [iconify](https://icon-sets.iconify.design/) 的图标名称。
+
+__输入:__
+
+```md /icon=mdi:balloon/ /icon=mdi:bookmark/
+::: timeline placement="between"
+- 节点一 time=2025-03-20 placement=right icon=mdi:balloon
+ 正文内容
+- 节点二 time=2025-04-20 type=success icon=mdi:bookmark
+ 正文内容
+- 节点三 time=2025-01-22 type=danger placement=right icon=mdi:bullhorn-variant-outline
+ 正文内容
+- 节点四 time=2025-01-22 type=important card=true icon="mdi:cake-variant-outline"
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline placement="between"
+
+- 节点一 time=2025-03-20 placement=right icon=mdi:balloon
+ 正文内容
+- 节点二 time=2025-04-20 type=success icon=mdi:bookmark
+ 正文内容
+- 节点三 time=2025-01-22 type=danger placement=right icon=mdi:bullhorn-variant-outline
+ 正文内容
+- 节点四 time=2025-01-22 type=important card=true icon="mdi:cake-variant-outline"
+ 正文内容
+:::
+
+### 卡片节点
+
+卡片节点可以很灵活的进行控制:
+
+- 在 容器配置中添加 `card` 即可使每个列表项都是卡片节点。
+- 在列表项首行标题之后,添加 `card=true` 即可为节点设置为卡片节点。
+- 在列表项首行标题之后,添加 `card=false` 即可为节点设置为非卡片节点。
+
+卡片节点的样式会受到 `type` 配置的影响。
+
+::: tip 在列表项首行标题之后添加 `card=true` / `card=false` 可以覆盖容器节点的 `card` 配置
+:::
+
+__输入:__
+
+```md{1} /card=false/
+::: timeline card
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success card=false
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+```
+
+__输出:__
+
+::: timeline card
+
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=success card=false
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+- 节点四 time=2025-01-22 type=important
+ 正文内容
+:::
+
+## 自定义节点类型
+
+时间轴的节点类型是通过 CSS Variables 控制的,主题提供了以下的 CSS 变量:
+
+```css
+:root {
+ --vp-timeline-c-line: var(--vp-c-border); /* 线条颜色 */
+ --vp-timeline-c-point: var(--vp-c-border); /* 点颜色 */
+ --vp-timeline-c-title: var(--vp-c-text-1); /* 标题文本颜色 */
+ --vp-timeline-c-text: var(--vp-c-text-1); /* 正文文本颜色 */
+ --vp-timeline-c-time: var(--vp-c-text-3); /* 时间文本颜色 */
+ --vp-timeline-c-icon: var(--vp-c-bg); /* 图标颜色 */
+ --vp-timeline-bg-card: var(--vp-c-bg-soft); /* 卡片节点的背景颜色 */
+}
+```
+
+比如主题内置的节点类型 `tip`:
+
+```css /.tip/
+.vp-timeline-item.tip {
+ --vp-timeline-c-line: var(--vp-c-tip-1);
+ --vp-timeline-c-point: var(--vp-c-tip-1);
+ --vp-timeline-bg-card: var(--vp-c-tip-soft);
+}
+```
+
+可以在 [自定义样式](../custom/style.md) 中,覆盖内置的类型,或者添加新的类型。
+
+__示例:__
+
+```css title=".vuepress/styles/index.css"
+.vp-timeline-item.your-type {
+ --vp-timeline-c-line: #3cf;
+ --vp-timeline-c-point: #3cf;
+ --vp-timeline-bg-card: rgba(60, 252, 255, 0.314);
+}
+```
+
+```md /type=your-type/
+::: timeline
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=your-type card=true
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+:::
+```
+
+::: timeline
+
+- 节点一 time=2025-03-20
+ 正文内容
+- 节点二 time=2025-04-20 type=your-type card=true
+ 正文内容
+- 节点三 time=2025-01-22 type=danger
+ 正文内容
+:::
+
+
diff --git a/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap b/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
new file mode 100644
index 00000000..a5086e79
--- /dev/null
+++ b/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
@@ -0,0 +1,40 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`timeline > timelinePlugin() > should work 1`] = `
+"
+ 这是标题
+
+这是内容
+ 这是标题
+
+这是内容
+ 这是标题
+
+这是内容
+
+
+ 这是标题
+
+这是内容
+ 这是标题
+
+这是内容
+ 这是标题
+
+这是内容
+ 这是标题
+
+这是内容
+ 这是标题
+
+这是内容"
+`;
diff --git a/plugins/plugin-md-power/__test__/timelinePlugin.spec.ts b/plugins/plugin-md-power/__test__/timelinePlugin.spec.ts
new file mode 100644
index 00000000..9907eac8
--- /dev/null
+++ b/plugins/plugin-md-power/__test__/timelinePlugin.spec.ts
@@ -0,0 +1,77 @@
+import MarkdownIt from 'markdown-it'
+import { describe, expect, it } from 'vitest'
+import { extractTimelineAttributes, timelinePlugin } from '../src/node/container/timeline.js'
+
+describe('timeline > extractTimelineAttributes()', () => {
+ it('should work', () => {
+ const { title, attrs } = extractTimelineAttributes('这是标题 time=Q1')
+ expect(title).toBe('这是标题')
+ expect(attrs).toEqual({ time: 'Q1' })
+ })
+
+ it('should work with multi attrs', () => {
+ const { title, attrs } = extractTimelineAttributes('这是标题 time=Q1 icon=ri:clockwise-line card=true placement=left line=dashed')
+ expect(title).toBe('这是标题')
+ expect(attrs).toEqual({ time: 'Q1', icon: 'ri:clockwise-line', card: 'true', placement: 'left', line: 'dashed' })
+ })
+
+ it('should work with title include space', () => {
+ const { title, attrs } = extractTimelineAttributes('这是标题 这也是标题 time=Q1 icon=ri:clockwise-line card=true placement=left line=dashed')
+
+ expect(title).toBe('这是标题 这也是标题')
+ expect(attrs).toEqual({ time: 'Q1', icon: 'ri:clockwise-line', card: 'true', placement: 'left', line: 'dashed' })
+ })
+
+ it('should work with unknown attr', () => {
+ const { title, attrs } = extractTimelineAttributes('这是标题 time=Q1 unknown=true card=true')
+ expect(title).toBe('这是标题 unknown=true card=true')
+ expect(attrs).toEqual({ time: 'Q1' })
+ })
+})
+
+describe('timeline > timelinePlugin()', () => {
+ const md = new MarkdownIt()
+ timelinePlugin(md)
+
+ it('should work', () => {
+ const source = `\
+::: timeline
+- 这是标题
+ 这是内容
+
+- 这是标题
+ 这是内容
+:::
+
+::: timeline horizontal line="dashed" card
+- 这是标题 time=q1
+ 这是内容
+ - 1
+ - 2
+ - 3
+ - 1.1
+ - 1.2
+
+- 这是标题 time=q2 color=red card=false
+ 这是内容
+:::
+
+::: timeline placement="right"
+- 这是标题 icon=xxx card=true type=warning
+ 这是内容
+
+- 这是标题 type=danger line=dotted
+ 这是内容
+:::
+
+::: timeline placement="between"
+- 这是标题 card=true placement=right
+ 这是内容
+
+- 这是标题 card=true placement=left
+ 这是内容
+:::
+`
+ expect(md.render(source)).toMatchSnapshot()
+ })
+})
diff --git a/plugins/plugin-md-power/src/client/components/VPTimeline.vue b/plugins/plugin-md-power/src/client/components/VPTimeline.vue
new file mode 100644
index 00000000..9e1e73a5
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/components/VPTimeline.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
diff --git a/plugins/plugin-md-power/src/client/components/VPTimelineItem.vue b/plugins/plugin-md-power/src/client/components/VPTimelineItem.vue
new file mode 100644
index 00000000..f3cc071a
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/components/VPTimelineItem.vue
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ timeline.time }}
+
+
+
+
+
+
diff --git a/plugins/plugin-md-power/src/client/options.ts b/plugins/plugin-md-power/src/client/options.ts
index 7536d90c..0d6f8bda 100644
--- a/plugins/plugin-md-power/src/client/options.ts
+++ b/plugins/plugin-md-power/src/client/options.ts
@@ -26,3 +26,7 @@ if (installed.hlsjs) {
if (installed.mpegtsjs) {
ART_PLAYER_SUPPORTED_VIDEO_TYPES.push('ts', 'flv')
}
+
+export const INJECT_TIMELINE_KEY = Symbol(
+ __VUEPRESS_DEV__ ? 'timeline' : '',
+)
diff --git a/plugins/plugin-md-power/src/node/container/index.ts b/plugins/plugin-md-power/src/node/container/index.ts
index 0c930bd7..999a8b1b 100644
--- a/plugins/plugin-md-power/src/node/container/index.ts
+++ b/plugins/plugin-md-power/src/node/container/index.ts
@@ -11,6 +11,7 @@ import { langReplPlugin } from './langRepl.js'
import { npmToPlugins } from './npmTo.js'
import { stepsPlugin } from './steps.js'
import { tabs } from './tabs.js'
+import { timelinePlugin } from './timeline.js'
export async function containerPlugin(
app: App,
@@ -46,4 +47,7 @@ export async function containerPlugin(
// ::: file-tree
fileTreePlugin(md, isPlainObject(options.fileTree) ? options.fileTree : {})
}
+
+ if (options.timeline)
+ timelinePlugin(md)
}
diff --git a/plugins/plugin-md-power/src/node/container/timeline.ts b/plugins/plugin-md-power/src/node/container/timeline.ts
new file mode 100644
index 00000000..d70b4062
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/container/timeline.ts
@@ -0,0 +1,173 @@
+/**
+ * ::: timeline
+ *
+ * - title time="Q1" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
+ * xxx
+ * - title time="Q2" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
+ * :::
+ */
+import type { Markdown } from 'vuepress/markdown'
+import { resolveAttrs } from '.././utils/resolveAttrs.js'
+import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
+import { createContainerPlugin } from './createContainer.js'
+
+export interface TimelineAttrs {
+ horizontal?: boolean
+ card?: boolean
+ placement?: string
+ line?: string
+}
+
+export interface TimelineItemAttrs {
+ time?: string
+ type?: string
+ icon?: string
+ color?: string
+ line?: string
+ card?: string
+ placement?: string
+}
+
+export interface TimelineItemMeta extends TimelineItemAttrs {
+ title: string
+}
+
+const RE_KEY = /(\w+)=\s*/
+const RE_SEARCH_KEY = /\s+\w+=\s*|$/
+const RE_CLEAN_VALUE = /(?["'])(.*?)(\k)/
+
+export function timelinePlugin(md: Markdown) {
+ createContainerPlugin(md, 'timeline', {
+ before(info, tokens, index) {
+ const listStack: number[] = [] // 记录列表嵌套深度
+
+ for (let i = index + 1; i < tokens.length; i++) {
+ const token = tokens[i]
+ if (token.type === 'container_timeline_close') {
+ break
+ }
+ // 列表层级追踪
+ if (token.type === 'bullet_list_open') {
+ listStack.push(0) // 每个新列表初始层级为0
+ if (listStack.length === 1)
+ token.hidden = true
+ }
+ else if (token.type === 'bullet_list_close') {
+ listStack.pop()
+ if (listStack.length === 0)
+ token.hidden = true
+ }
+ else if (token.type === 'list_item_open') {
+ const currentLevel = listStack.length
+ // 仅处理根级列表项(层级1)
+ if (currentLevel === 1) {
+ token.type = 'timeline_item_open'
+ const titleOpenToken = tokens[i + 1]
+ const titleCloseToken = tokens[i + 3]
+ titleOpenToken.hidden = true
+ titleCloseToken.hidden = true
+ const inlineToken = tokens[i + 2]
+ const firstChildToken = inlineToken.children?.shift()
+ const { title, attrs } = extractTimelineAttributes(firstChildToken!.content.trim())
+
+ token.meta = {
+ title,
+ ...attrs,
+ } as TimelineItemMeta
+ }
+ }
+ else if (token.type === 'list_item_close') {
+ const currentLevel = listStack.length
+ if (currentLevel === 1) {
+ token.type = 'timeline_item_close'
+ }
+ }
+ }
+ const { attrs } = resolveAttrs(info)
+ const { horizontal, card, placement, line } = attrs
+ return ``
+ },
+ after: () => '',
+ })
+
+ md.renderer.rules.timeline_item_open = (tokens, idx, _, env) => {
+ const token = tokens[idx]
+ const { title, time, type, icon, color, line, card, placement } = token.meta as TimelineItemMeta
+ return `
+ ${md.renderInline(title, cleanMarkdownEnv(env))}
+ ${icon ? `` : ''}`
+ }
+
+ md.renderer.rules.timeline_item_close = () => ''
+}
+
+// 核心属性扫描器
+export function extractTimelineAttributes(rawText: string): {
+ title: string
+ attrs: TimelineItemAttrs
+} {
+ const attrKeys = ['time', 'type', 'icon', 'line', 'color', 'card', 'placement'] as const
+ const attrs: Partial = {}
+ let buffer = rawText.trim()
+ const titleSegments: string[] = []
+
+ while (buffer.length) {
+ // 匹配属性键 (支持大小写)
+ const keyMatch = buffer.match(RE_KEY)
+ if (!keyMatch) {
+ titleSegments.push(buffer)
+ break
+ }
+
+ // 提取可能的关键字
+ const matchedKey = keyMatch[1].toLowerCase()
+ if (!attrKeys.includes(matchedKey as any)) {
+ titleSegments.push(buffer)
+ break
+ }
+ const keyStart = keyMatch.index!
+ // 记录非属性内容为标题
+ titleSegments.push(buffer.slice(0, keyStart).trim())
+
+ // 跳过已匹配的 key:
+ const keyEnd = keyStart + keyMatch[0].length
+ buffer = buffer.slice(keyEnd)
+
+ // 提取属性值 (到下一个属性或行尾)
+ let valueEnd = buffer.search(RE_SEARCH_KEY)
+ /* istanbul ignore if -- @preserve */
+ if (valueEnd === -1)
+ valueEnd = buffer.length
+ const value = buffer.slice(0, valueEnd).trim()
+ // 存储属性
+ attrs[matchedKey as keyof TimelineItemAttrs] = value.replace(RE_CLEAN_VALUE, '$2')
+
+ // 跳过已处理的值
+ buffer = buffer.slice(valueEnd)
+ }
+
+ return {
+ title: titleSegments.join(' ').trim(),
+ attrs,
+ }
+}
diff --git a/plugins/plugin-md-power/src/node/prepareConfigFile.ts b/plugins/plugin-md-power/src/node/prepareConfigFile.ts
index 5feac215..38b6a08d 100644
--- a/plugins/plugin-md-power/src/node/prepareConfigFile.ts
+++ b/plugins/plugin-md-power/src/node/prepareConfigFile.ts
@@ -102,6 +102,13 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('Abbreviation', Abbreviation)`)
}
+ if (options.timeline) {
+ imports.add(`import VPTimeline from '${CLIENT_FOLDER}components/VPTimeline.vue'`)
+ imports.add(`import VPTimelineItem from '${CLIENT_FOLDER}components/VPTimelineItem.vue'`)
+ enhances.add(`app.component('VPTimeline', VPTimeline)`)
+ enhances.add(`app.component('VPTimelineItem', VPTimelineItem)`)
+ }
+
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 5d349c36..4e50073c 100644
--- a/plugins/plugin-md-power/src/shared/plugin.ts
+++ b/plugins/plugin-md-power/src/shared/plugin.ts
@@ -56,6 +56,20 @@ export interface MarkdownPowerPluginOptions {
*/
plot?: boolean | PlotOptions
+ /**
+ * 是否启用 timeline 语法
+ *
+ * ```md
+ * ::: timeline
+ * - title time="Q1" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
+ * xxx
+ * :::
+ * ```
+ *
+ * @default false
+ */
+ timeline?: boolean
+
// video embed
/**
* 是否启用 bilibili 视频嵌入
diff --git a/theme/src/node/detector/fields.ts b/theme/src/node/detector/fields.ts
index 3b09dcf4..c246477f 100644
--- a/theme/src/node/detector/fields.ts
+++ b/theme/src/node/detector/fields.ts
@@ -57,6 +57,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
'plot',
'repl',
'replit',
+ 'timeline',
'youtube',
]