refactor(plugin-md-power): improve timeline syntax parsing (#534)

This commit is contained in:
pengzhanbo 2025-03-23 00:40:34 +08:00 committed by GitHub
parent 7b8fae22b1
commit dd5c984578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 347 additions and 157 deletions

View File

@ -37,11 +37,13 @@ export default defineUserConfig({
```md{1,9} title="timeline.md"
::: timeline 配置
- 标题 配置
- 标题
配置
正文内容
- 标题 配置
- 标题
配置
正文内容
:::
@ -49,8 +51,12 @@ export default defineUserConfig({
对于列表的每一个项:
- __第一行__: 从起始位置定义 __标题__,在标题之后跟着 `key=value` 的格式配置时间点的 __属性__
- __后续行__: 正文内容,==请注意添加正确的缩进=={.important}。
- 从 __首行开始____首个空行__,均为 __标题__ ,在标题后紧跟随的一行,用于 __配置__ 当前项的行为
- __首个空行之后__: 正文内容
:::important 请注意添加正确的缩进
:::
__一个简单的例子__
@ -58,15 +64,18 @@ __输入__
```md
::: timeline
- 节点一 time=2025-03-20 type=success
- 节点一
time=2025-03-20 type=success
正文内容
- 节点二 time=2025-02-21 type=warning
- 节点二
time=2025-02-21 type=warning
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
:::
@ -76,15 +85,18 @@ __输出__
::: timeline
- 节点一 time=2025-03-20 type=success
- 节点一
time=2025-03-20 type=success
正文内容
- 节点二 time=2025-02-21 type=warning
- 节点二
time=2025-02-21 type=warning
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
:::
@ -100,9 +112,17 @@ __时间线__ 支持非常灵活且灵活的配置项,配置主要分为两个
`::: timeline horizontal` 表示 渲染为 水平方向的时间线。
- __列表项配置__ 在列表的每一个项上的配置,配置项列表项的第一行,跟随在标题之后,如:
- __列表项配置__ 列表的每一个项的配置,紧跟随在标题之后的一行,如:
`- 节点一 time=2025-03-20 type=success` 表示 时间点为 `2025-03-20`,节点类型为 `success`
```md
::: timeline
- 标题 <!--标题行-->
也是标题 <!--标题行-->
time=2025-03-20 type=success <!--配置跟随在最后的标题行之后的单独一行,可选-->
<!--空行,有正文时必须-->
正文内容
:::
```
### 容器配置
@ -202,13 +222,24 @@ __输入__
```md /horizontal/
::: timeline horizontal
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
```
@ -217,15 +248,25 @@ __输出__
::: timeline horizontal
- 节点一 time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点一
time=2025-03-20
正文内容
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四
time=2025-01-22 type=important
正文内容
:::
### 右对齐
@ -236,13 +277,24 @@ __输入__
```md /placement="right"/
::: timeline placement="right"
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
```
@ -251,13 +303,24 @@ __输出__
::: timeline placement="right"
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
@ -271,13 +334,24 @@ __输入__
```md /placement="between"/ /placement=right/
::: timeline placement="between"
- 节点一 time=2025-03-20 placement=right
- 节点一
time=2025-03-20 placement=right
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger placement=right
- 节点三
time=2025-01-22 type=danger placement=right
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
```
@ -286,31 +360,53 @@ __输出__
::: timeline placement="between"
- 节点一 time=2025-03-20 placement=right
- 节点一
time=2025-03-20 placement=right
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger placement=right
- 节点三
time=2025-01-22 type=danger placement=right
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
### 节点类型
在列表项首行标题之后,添加 `type=节点类型` 可以为当前节点设置节点类型。
在列表项配置中,添加 `type=节点类型` 可以为当前节点设置节点类型。
__输入__
```md /type=success/ /type=warning/ /type=danger/ /type=important/
::: timeline
- 节点一 time=2025-03-20 type=success
- 节点一
time=2025-03-20 type=success
正文内容
- 节点二 time=2025-04-20 type=warning
- 节点二
time=2025-04-20 type=warning
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
```
@ -319,32 +415,54 @@ __输出__
::: timeline
- 节点一 time=2025-03-20 type=success
- 节点一
time=2025-03-20 type=success
正文内容
- 节点二 time=2025-04-20 type=warning
- 节点二
time=2025-04-20 type=warning
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
### 线条风格
- 在容器配置中添加 `line=线条风格` 可以为所有节点设置默认线条风格。
- 在列表项首行标题之后,添加 `line=线条风格` 可以为节点设置线条风格。
- 在列表项配置中,添加 `line=线条风格` 可以为节点设置线条风格。
__输入__
```md /line="dotted"/ /line=solid/ /line=dashed/
::: timeline line="dotted"
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger line=dashed
- 节点三
time=2025-01-22 type=danger line=dashed
正文内容
- 节点四 time=2025-01-22 type=important line=solid
- 节点四
time=2025-01-22 type=important line=solid
正文内容
:::
```
@ -353,19 +471,30 @@ __输出__
::: timeline line="dotted"
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success
- 节点二
time=2025-04-20 type=success
正文内容
- 节点三 time=2025-01-22 type=danger line=dashed
- 节点三
time=2025-01-22 type=danger line=dashed
正文内容
- 节点四 time=2025-01-22 type=important line=solid
- 节点四
time=2025-01-22 type=important line=solid
正文内容
:::
### 带图标的节点
在列表项首行标题之后,添加 `icon=图标名称` 可以为节点添加图标。
在列表项配置中,添加 `icon=图标名称` 可以为节点添加图标。
图标名称支持 [iconify](https://icon-sets.iconify.design/) 的图标名称。
@ -373,13 +502,24 @@ __输入__
```md /icon=mdi:balloon/ /icon=mdi:bookmark/
::: timeline placement="between"
- 节点一 time=2025-03-20 placement=right icon=mdi:balloon
- 节点一
time=2025-03-20 placement=right icon=mdi:balloon
正文内容
- 节点二 time=2025-04-20 type=success icon=mdi:bookmark
- 节点二
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=danger placement=right icon=mdi:bullhorn-variant-outline
正文内容
- 节点四 time=2025-01-22 type=important card=true icon="mdi:cake-variant-outline"
- 节点四
time=2025-01-22 type=important card=true icon="mdi:cake-variant-outline"
正文内容
:::
```
@ -388,13 +528,24 @@ __输出__
::: timeline placement="between"
- 节点一 time=2025-03-20 placement=right icon=mdi:balloon
- 节点一
time=2025-03-20 placement=right icon=mdi:balloon
正文内容
- 节点二 time=2025-04-20 type=success icon=mdi:bookmark
- 节点二
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=danger placement=right icon=mdi:bullhorn-variant-outline
正文内容
- 节点四 time=2025-01-22 type=important card=true icon="mdi:cake-variant-outline"
- 节点四
time=2025-01-22 type=important card=true icon="mdi:cake-variant-outline"
正文内容
:::
@ -403,25 +554,36 @@ __输出__
卡片节点可以很灵活的进行控制:
- 在 容器配置中添加 `card` 即可使每个列表项都是卡片节点。
- 在列表项首行标题之后,添加 `card=true` 即可为节点设置为卡片节点。
- 在列表项首行标题之后,添加 `card=false` 即可为节点设置为非卡片节点。
- 在列表项配置中,添加 `card=true` 即可为节点设置为卡片节点。
- 在列表项配置中,添加 `card=false` 即可为节点设置为非卡片节点。
卡片节点的样式会受到 `type` 配置的影响。
::: tip 在列表项首行标题之后添加 `card=true` / `card=false` 可以覆盖容器节点的 `card` 配置
::: tip 在列表项配置中添加 `card=true` / `card=false` 可以覆盖容器节点的 `card` 配置
:::
__输入__
```md{1} /card=false/
::: timeline card
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success card=false
- 节点二
time=2025-04-20 type=success card=false
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
```
@ -430,13 +592,24 @@ __输出__
::: timeline card
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=success card=false
- 节点二
time=2025-04-20 type=success card=false
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
- 节点四 time=2025-01-22 type=important
- 节点四
time=2025-01-22 type=important
正文内容
:::
@ -480,22 +653,38 @@ __示例__
```md /type=your-type/
::: timeline
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=your-type card=true
- 节点二
time=2025-04-20 type=your-type card=true
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
:::
```
::: timeline
- 节点一 time=2025-03-20
- 节点一
time=2025-03-20
正文内容
- 节点二 time=2025-04-20 type=your-type card=true
- 节点二
time=2025-04-20 type=your-type card=true
正文内容
- 节点三 time=2025-01-22 type=danger
- 节点三
time=2025-01-22 type=danger
正文内容
:::

View File

@ -1,16 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`timeline > timelinePlugin() > should work 1`] = `
"<VPTimeline :card="undefined"><VPTimelineItem :card="undefined">
<template #title>这是标题</template>
这是内容</VPTimelineItem><VPTimelineItem :card="undefined">
<template #title>这是标题</template>
这是内容</VPTimelineItem></VPTimeline><VPTimeline horizontal card line="dashed"><VPTimelineItem time="q1" :card="undefined">
<template #title>这是标题</template>
这是内容
"<VPTimeline :card="undefined"><VPTimelineItem :card="undefined"><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem><VPTimelineItem :card="undefined"><template #title>这是标题
这也是标题</template><p>这是内容</p>
</VPTimelineItem></VPTimeline><VPTimeline horizontal card line="dashed"><VPTimelineItem time="q1" :card="undefined"><template #title>这是标题</template><p>这是内容</p>
<ul>
<li>1</li>
<li>2</li>
@ -21,24 +15,11 @@ exports[`timeline > timelinePlugin() > should work 1`] = `
</ul>
</li>
</ul>
</VPTimelineItem><VPTimelineItem time="q2" color="red">
<template #title>这是标题</template>
这是内容</VPTimelineItem></VPTimeline><VPTimeline :card="undefined" placement="right"><VPTimelineItem type="warning" icon="xxx" card>
<template #title>这是标题</template>
<template #icon><VPIcon name="xxx"/></template>
这是内容</VPTimelineItem><VPTimelineItem type="danger" line="dotted" :card="undefined">
<template #title>这是标题</template>
这是内容</VPTimelineItem></VPTimeline><VPTimeline :card="undefined" placement="between"><VPTimelineItem card placement="right">
<template #title>这是标题</template>
这是内容</VPTimelineItem><VPTimelineItem card placement="left">
<template #title>这是标题</template>
这是内容</VPTimelineItem><VPTimelineItem :card="undefined">
<template #title>这是标题</template>
<p>这是内容</p>
</VPTimelineItem><VPTimelineItem time="q2" color="red"><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem></VPTimeline><VPTimeline :card="undefined" placement="right"><VPTimelineItem type="warning" icon="xxx" card><template #icon><VPIcon name="xxx"/></template><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem><VPTimelineItem type="danger" line="dotted" :card="undefined"><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem></VPTimeline><VPTimeline :card="undefined" placement="between"><VPTimelineItem card placement="right"><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem><VPTimelineItem card placement="left"><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem><VPTimelineItem :card="undefined"><template #title>这是标题</template><p>这是内容</p>
</VPTimelineItem></VPTimeline>"
`;

View File

@ -4,24 +4,24 @@ import { extractTimelineAttributes, timelinePlugin } from '../src/node/container
describe('timeline > extractTimelineAttributes()', () => {
it('should work', () => {
const meta = extractTimelineAttributes('这是标题 time=Q1')
expect(meta).toEqual({ title: '这是标题', time: 'Q1' })
const meta = extractTimelineAttributes('这time=Q1')
expect(meta).toEqual({ time: 'Q1' })
})
it('should work with multi attrs', () => {
const meta = extractTimelineAttributes('这是标题 time=Q1 icon=ri:clockwise-line card=true placement=left line=dashed')
expect(meta).toEqual({ title: '这是标题', time: 'Q1', icon: 'ri:clockwise-line', card: 'true', placement: 'left', line: 'dashed' })
const meta = extractTimelineAttributes('time=Q1 icon=ri:clockwise-line card=true placement=left line=dashed')
expect(meta).toEqual({ time: 'Q1', icon: 'ri:clockwise-line', card: 'true', placement: 'left', line: 'dashed' })
})
it('should work with title include space', () => {
const meta = extractTimelineAttributes('这是标题 这也是标题 time=Q1 icon=ri:clockwise-line card=true placement=left line=dashed')
const meta = extractTimelineAttributes('time=Q1 icon=ri:clockwise-line card=true placement=left line=dashed')
expect(meta).toEqual({ title: '这是标题 这也是标题', time: 'Q1', icon: 'ri:clockwise-line', card: 'true', placement: 'left', line: 'dashed' })
expect(meta).toEqual({ time: 'Q1', icon: 'ri:clockwise-line', card: 'true', placement: 'left', line: 'dashed' })
})
it('should work with unknown attr', () => {
const meta = extractTimelineAttributes('这是标题 time=Q1 unknown=true card=true')
expect(meta).toEqual({ title: '这是标题 unknown=true card=true', time: 'Q1' })
const meta = extractTimelineAttributes('time=Q1 unknown=true card=true')
expect(meta).toEqual({ time: 'Q1' })
})
})
@ -33,14 +33,19 @@ describe('timeline > timelinePlugin()', () => {
const source = `\
::: timeline
-
-
:::
::: timeline horizontal line="dashed" card
- time=q1
-
time=q1
- 1
- 2
@ -48,23 +53,33 @@ describe('timeline > timelinePlugin()', () => {
- 1.1
- 1.2
- time=q2 color=red card=false
-
time=q2 color=red card=false
:::
::: timeline placement="right"
- icon=xxx card=true type=warning
-
icon=xxx card=true type=warning
- type=danger line=dotted
-
type=danger line=dotted
:::
::: timeline placement="between"
- card=true placement=right
-
card=true placement=right
- card=true placement=left
-
card=true placement=left
-

View File

@ -77,6 +77,7 @@
"@mdit/plugin-sup": "catalog:prod",
"@mdit/plugin-tab": "catalog:prod",
"@mdit/plugin-tasklist": "catalog:prod",
"@pengzhanbo/utils": "catalog:prod",
"@vuepress/helper": "catalog:vuepress",
"@vueuse/core": "catalog:prod",
"chokidar": "catalog:prod",

View File

@ -1,15 +1,21 @@
/**
* ::: 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"
* - title
* time="Q1" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
*
* content
*
* - title
* time="Q2" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
*
* content
* :::
*/
import type Token from 'markdown-it/lib/token.mjs'
import type { Markdown } from 'vuepress/markdown'
import { isEmptyObject } from '@pengzhanbo/utils'
import { resolveAttrs } from '.././utils/resolveAttrs.js'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
import { createContainerPlugin } from './createContainer.js'
export interface TimelineAttrs {
@ -20,7 +26,6 @@ export interface TimelineAttrs {
}
export interface TimelineItemMeta {
title: string
time?: string
type?: string
icon?: string
@ -54,9 +59,9 @@ export function timelinePlugin(md: Markdown) {
after: () => '</VPTimeline>',
})
md.renderer.rules.timeline_item_open = (tokens, idx, _, env) => {
md.renderer.rules.timeline_item_open = (tokens, idx) => {
const token = tokens[idx]
const { title, time, type, icon, color, line, card, placement } = token.meta as TimelineItemMeta
const { time, type, icon, color, line, card, placement } = token.meta as TimelineItemMeta
return `<VPTimelineItem${
time ? ` time="${time}"` : ''
}${
@ -69,12 +74,12 @@ export function timelinePlugin(md: Markdown) {
card === 'true' ? ' card' : card === 'false' ? '' : ' :card="undefined"'
}${
placement ? ` placement="${placement}"` : ''
}>
<template #title>${md.renderInline(title, cleanMarkdownEnv(env))}</template>
${icon ? `<template #icon><VPIcon name="${icon}"/></template>` : ''}`
}>${icon ? `<template #icon><VPIcon name="${icon}"/></template>` : ''}`
}
md.renderer.rules.timeline_item_close = () => '</VPTimelineItem>'
md.renderer.rules.timeline_item_title_open = () => '<template #title>'
md.renderer.rules.timeline_item_title_close = () => '</template>'
}
function parseTimeline(tokens: Token[], index: number) {
@ -101,20 +106,27 @@ function parseTimeline(tokens: Token[], index: number) {
// 仅处理根级列表项层级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
tokens[i + 1].type = 'timeline_item_title_open'
tokens[i + 3].type = 'timeline_item_title_close'
// - title
// attrs
// 列表项 `-` 后面包括紧跟随的后续行均在 type=inline 的 token 中, 并作为 children
const inlineToken = tokens[i + 2]
const softbreakIndex = inlineToken.children!.findIndex(
// 找到最后一个 softbreak最后一行作为 attrs 进行解析
const softbreakIndex = inlineToken.children!.findLastIndex(
token => token.type === 'softbreak',
)
inlineToken.children = softbreakIndex !== -1
? inlineToken.children!.slice(softbreakIndex)
: []
const content = inlineToken.content.replace(/\n[\s\S]*/, '')
token.meta = extractTimelineAttributes(content.trim())
if (softbreakIndex !== -1) {
const lastToken = inlineToken.children![inlineToken.children!.length - 1]
token.meta = extractTimelineAttributes(lastToken.content.trim())
if (!isEmptyObject(token.meta)) {
inlineToken.children = inlineToken.children!.slice(0, softbreakIndex)
}
}
else {
token.meta = {}
}
}
}
else if (token.type === 'list_item_close') {
@ -128,27 +140,22 @@ function parseTimeline(tokens: Token[], index: number) {
export function extractTimelineAttributes(rawText: string): TimelineItemMeta {
const attrKeys = ['time', 'type', 'icon', 'line', 'color', 'card', 'placement'] as const
const attrs: Partial<TimelineItemMeta> = {}
const attrs: TimelineItemMeta = {}
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
@ -167,8 +174,5 @@ export function extractTimelineAttributes(rawText: string): TimelineItemMeta {
buffer = buffer.slice(valueEnd)
}
return {
title: titleSegments.join(' ').trim(),
...attrs,
}
return attrs
}