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 @@ + + + + + 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 ` + + ${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', ]