diff --git a/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap b/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
index a5086e79..b346d401 100644
--- a/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
+++ b/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
@@ -36,5 +36,9 @@ 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
index 9907eac8..f08a0cc7 100644
--- a/plugins/plugin-md-power/__test__/timelinePlugin.spec.ts
+++ b/plugins/plugin-md-power/__test__/timelinePlugin.spec.ts
@@ -4,28 +4,24 @@ import { extractTimelineAttributes, timelinePlugin } from '../src/node/container
describe('timeline > extractTimelineAttributes()', () => {
it('should work', () => {
- const { title, attrs } = extractTimelineAttributes('这是标题 time=Q1')
- expect(title).toBe('这是标题')
- expect(attrs).toEqual({ time: 'Q1' })
+ const meta = extractTimelineAttributes('这是标题 time=Q1')
+ expect(meta).toEqual({ title: '这是标题', 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' })
+ 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' })
})
it('should work with title include space', () => {
- const { title, attrs } = 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(title).toBe('这是标题 这也是标题')
- expect(attrs).toEqual({ 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' })
})
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' })
+ const meta = extractTimelineAttributes('这是标题 time=Q1 unknown=true card=true')
+ expect(meta).toEqual({ title: '这是标题 unknown=true card=true', time: 'Q1' })
})
})
@@ -70,6 +66,10 @@ describe('timeline > timelinePlugin()', () => {
- 这是标题 card=true placement=left
这是内容
+
+- 这是标题
+
+ 这是内容
:::
`
expect(md.render(source)).toMatchSnapshot()
diff --git a/plugins/plugin-md-power/src/node/container/timeline.ts b/plugins/plugin-md-power/src/node/container/timeline.ts
index d70b4062..ebd4841a 100644
--- a/plugins/plugin-md-power/src/node/container/timeline.ts
+++ b/plugins/plugin-md-power/src/node/container/timeline.ts
@@ -6,6 +6,7 @@
* - title time="Q2" icon="ri:clockwise-line" line="dashed" type="warning" color="red"
* :::
*/
+import type Token from 'markdown-it/lib/token.mjs'
import type { Markdown } from 'vuepress/markdown'
import { resolveAttrs } from '.././utils/resolveAttrs.js'
import { cleanMarkdownEnv } from '../utils/cleanMarkdownEnv.js'
@@ -18,7 +19,8 @@ export interface TimelineAttrs {
line?: string
}
-export interface TimelineItemAttrs {
+export interface TimelineItemMeta {
+ title: string
time?: string
type?: string
icon?: string
@@ -28,10 +30,6 @@ export interface TimelineItemAttrs {
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)/
@@ -39,50 +37,8 @@ const RE_CLEAN_VALUE = /(?["'])(.*?)(\k)/
export function timelinePlugin(md: Markdown) {
createContainerPlugin(md, 'timeline', {
before(info, tokens, index) {
- const listStack: number[] = [] // 记录列表嵌套深度
+ parseTimeline(tokens, index)
- 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 ` ''
}
-// 核心属性扫描器
-export function extractTimelineAttributes(rawText: string): {
- title: string
- attrs: TimelineItemAttrs
-} {
+function parseTimeline(tokens: Token[], index: number) {
+ 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 softbreakIndex = inlineToken.children!.findIndex(
+ 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())
+ }
+ }
+ else if (token.type === 'list_item_close') {
+ const currentLevel = listStack.length
+ if (currentLevel === 1) {
+ token.type = 'timeline_item_close'
+ }
+ }
+ }
+}
+
+export function extractTimelineAttributes(rawText: string): TimelineItemMeta {
const attrKeys = ['time', 'type', 'icon', 'line', 'color', 'card', 'placement'] as const
- const attrs: Partial = {}
+ const attrs: Partial = {}
let buffer = rawText.trim()
const titleSegments: string[] = []
@@ -160,7 +161,7 @@ export function extractTimelineAttributes(rawText: string): {
valueEnd = buffer.length
const value = buffer.slice(0, valueEnd).trim()
// 存储属性
- attrs[matchedKey as keyof TimelineItemAttrs] = value.replace(RE_CLEAN_VALUE, '$2')
+ attrs[matchedKey as keyof TimelineItemMeta] = value.replace(RE_CLEAN_VALUE, '$2')
// 跳过已处理的值
buffer = buffer.slice(valueEnd)
@@ -168,6 +169,6 @@ export function extractTimelineAttributes(rawText: string): {
return {
title: titleSegments.join(' ').trim(),
- attrs,
+ ...attrs,
}
}