Skip to content

Commit f5442f3

Browse files
authored
feat: support version badges in markdown tables and sidebar (#269)
1 parent b775495 commit f5442f3

10 files changed

Lines changed: 711 additions & 1 deletion

File tree

docs/.vitepress/config.mts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
33
import { fileURLToPath } from 'url'
44
import { defineConfig } from 'vitepress'
55
import { vitepressDemoPlugin } from 'vitepress-demo-plugin'
6+
import { SidebarBadgePlugin, MarkdownBadgePlugin } from './plugins/badge'
67

78
const { version } = pkg
89

@@ -77,7 +78,7 @@ export default defineConfig({
7778
],
7879
],
7980
vite: {
80-
plugins: [vueJsx()],
81+
plugins: [vueJsx(), SidebarBadgePlugin()],
8182
server: {
8283
open: true,
8384
proxy: process.env.VP_MODE === 'development' ? { '/playground': 'http://localhost:5184' } : undefined,
@@ -99,6 +100,8 @@ export default defineConfig({
99100
return code.replace(/import\.meta\.env\.BASE_URL/g, `'${process.env.VITEPRESS_BASE || '/'}'`)
100101
},
101102
})
103+
// 添加 Markdown 内容中的版本标记支持
104+
md.use(MarkdownBadgePlugin)
102105
},
103106
},
104107
themeConfig: {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* eslint-disable */
2+
/**
3+
* Markdown Badge 插件
4+
*
5+
* 功能:在 markdown 内容中支持 @new、@deprecated、@1.2.0 等版本标记语法
6+
*
7+
* @example
8+
* ```markdown
9+
* | 属性名 | 说明 | 类型 |
10+
* | ------ | ---- | ---- |
11+
* | count @new | 数量 | number |
12+
* | oldProp @deprecated | 旧属性 | string |
13+
* | version @1.2.0 | 版本 | string |
14+
* ```
15+
*
16+
* @example
17+
* ```ts
18+
* // config.mts
19+
* import { MarkdownBadgePlugin } from './plugins/badge'
20+
*
21+
* export default defineConfig({
22+
* markdown: {
23+
* config: (md) => {
24+
* md.use(MarkdownBadgePlugin)
25+
* }
26+
* }
27+
* })
28+
* ```
29+
*/
30+
31+
import { MARKDOWN_BADGE_REGEX } from './constants'
32+
import { createBadgeHTML } from './utils'
33+
34+
export function MarkdownBadgePlugin(md: any): void {
35+
const defaultRender = md.renderer.rules.text
36+
37+
md.renderer.rules.text = (tokens: any, idx: any, options: any, env: any, self: any) => {
38+
const token = tokens[idx]
39+
let content = token.content
40+
41+
// 检查是否包含版本标记
42+
if (MARKDOWN_BADGE_REGEX.test(content)) {
43+
MARKDOWN_BADGE_REGEX.lastIndex = 0
44+
content = content.replace(MARKDOWN_BADGE_REGEX, (_: any, badge: any) => {
45+
return createBadgeHTML(badge)
46+
})
47+
48+
return content
49+
}
50+
51+
return defaultRender ? defaultRender(tokens, idx, options, env, self) : content
52+
}
53+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/* eslint-disable */
2+
/**
3+
* Sidebar Badge 插件
4+
*
5+
* 功能:从 markdown 文件的 frontmatter 中读取 badge,自动添加到 sidebar
6+
*
7+
* @example
8+
* ```ts
9+
* // config.mts
10+
* import { SidebarBadgePlugin } from './plugins/badge'
11+
*
12+
* export default defineConfig({
13+
* vite: {
14+
* plugins: [SidebarBadgePlugin({ debug: true })]
15+
* }
16+
* })
17+
* ```
18+
*/
19+
20+
import fs from 'fs'
21+
import path from 'path'
22+
import { withBadge } from './utils'
23+
24+
export interface SidebarBadgeOptions {
25+
/**
26+
* 源文件目录,默认为 'src'
27+
*/
28+
srcDir?: string
29+
/**
30+
* 是否启用调试日志
31+
*/
32+
debug?: boolean
33+
}
34+
35+
export function SidebarBadgePlugin(options?: SidebarBadgeOptions): any {
36+
let vpConfig: any = null
37+
const { srcDir = 'src', debug = false } = options || {}
38+
let badgeCount = 0
39+
40+
return {
41+
name: 'vitepress-sidebar-badge',
42+
43+
async configResolved(config: any) {
44+
if (vpConfig) return
45+
46+
vpConfig = config.vitepress
47+
if (!vpConfig) {
48+
console.warn('[SidebarBadge] VitePress config not found')
49+
return
50+
}
51+
52+
try {
53+
const { default: matter } = await import('gray-matter')
54+
55+
const sidebar = vpConfig.site.themeConfig.sidebar
56+
if (!sidebar) {
57+
console.warn('[SidebarBadge] No sidebar config found')
58+
return
59+
}
60+
61+
badgeCount = processSidebar(sidebar, vpConfig.root || process.cwd(), srcDir, matter, debug)
62+
63+
if (badgeCount > 0) {
64+
console.log(`✅ SidebarBadge: ${badgeCount} badge(s) applied`)
65+
} else if (debug) {
66+
console.log('ℹ️ SidebarBadge: No badges found')
67+
}
68+
} catch (error) {
69+
console.error('[SidebarBadge] Error:', error)
70+
}
71+
},
72+
}
73+
}
74+
75+
/**
76+
* 处理 sidebar 配置
77+
*/
78+
function processSidebar(sidebar: any, rootDir: string, srcDir: string, matter: any, debug: boolean): number {
79+
let count = 0
80+
const processedItems = new Set<any>()
81+
82+
if (Array.isArray(sidebar)) {
83+
count = processSidebarArray(sidebar, rootDir, srcDir, matter, debug, processedItems)
84+
} else if (typeof sidebar === 'object') {
85+
Object.keys(sidebar).forEach((key) => {
86+
const sidebarConfig = sidebar[key]
87+
if (Array.isArray(sidebarConfig)) {
88+
const dirName = key.replace(/^\//, '').replace(/\/$/, '')
89+
count += processSidebarArray(sidebarConfig, rootDir, srcDir, matter, debug, processedItems, dirName)
90+
}
91+
})
92+
}
93+
94+
return count
95+
}
96+
97+
/**
98+
* 处理 sidebar 数组
99+
*/
100+
function processSidebarArray(
101+
sidebarArray: any[],
102+
rootDir: string,
103+
srcDir: string,
104+
matter: any,
105+
debug: boolean,
106+
processedItems: Set<any>,
107+
dirName?: string,
108+
): number {
109+
let count = 0
110+
111+
sidebarArray.forEach((group) => {
112+
if (group.items && Array.isArray(group.items)) {
113+
const baseDir = group.base ? group.base.replace(/^\//, '').replace(/\/$/, '') : dirName
114+
115+
group.items.forEach((item: any) => {
116+
if (item.link) {
117+
const badge = readBadgeFromFrontmatter(item.link, rootDir, srcDir, baseDir || '', matter, debug)
118+
if (badge && !processedItems.has(item)) {
119+
item.text = withBadge(item.text, badge)
120+
processedItems.add(item)
121+
count++
122+
if (debug) {
123+
console.log(`[SidebarBadge] ✓ ${item.link}: ${badge}`)
124+
}
125+
}
126+
}
127+
})
128+
}
129+
})
130+
131+
return count
132+
}
133+
134+
/**
135+
* 从 markdown frontmatter 读取 badge
136+
*/
137+
function readBadgeFromFrontmatter(
138+
link: string,
139+
rootDir: string,
140+
srcDir: string,
141+
baseDir: string,
142+
matter: any,
143+
debug: boolean,
144+
): string | undefined {
145+
try {
146+
const filePath = path.join(rootDir, srcDir, baseDir, `${link}.md`)
147+
148+
if (!fs.existsSync(filePath)) {
149+
return undefined
150+
}
151+
152+
const content = fs.readFileSync(filePath, 'utf-8')
153+
const { data } = matter(content)
154+
155+
return data.badge as string | undefined
156+
} catch (error) {
157+
if (debug) {
158+
console.warn(`[SidebarBadge] ✗ ${link}: ${error}`)
159+
}
160+
return undefined
161+
}
162+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Badge 相关常量配置
3+
*/
4+
5+
/**
6+
* Badge 类型定义
7+
*/
8+
export type BadgeType = 'new' | 'deprecated' | 'beta' | 'alpha'
9+
10+
/**
11+
* Badge 值类型(包含固定类型和版本号字符串)
12+
*/
13+
export type BadgeValue = BadgeType | string
14+
15+
/**
16+
* Badge 类型到中文文本的映射
17+
*/
18+
export const BADGE_TEXT_MAP: Record<BadgeType, string> = {
19+
new: '新增',
20+
deprecated: '已废弃',
21+
beta: 'Beta',
22+
alpha: 'Alpha',
23+
}
24+
25+
/**
26+
* Badge CSS 类名映射
27+
*/
28+
export const BADGE_CLASS_MAP: Record<BadgeType, string> = {
29+
new: 'version-badge version-badge--new',
30+
deprecated: 'version-badge version-badge--deprecated',
31+
beta: 'version-badge version-badge--beta',
32+
alpha: 'version-badge version-badge--alpha',
33+
}
34+
35+
/**
36+
* 版本号正则表达式(支持 v0.4.0 或 0.4.0 格式)
37+
* 格式:\d+ - 至少一个数字开头
38+
* (?:\.\d+)* - 零或多个 .数字 组合
39+
* (?:[-+][a-zA-Z0-9.]+)? - 可选的预发布版本或构建元数据
40+
* 有效:1, 1.2, 1.2.3, v1.2.3-beta.1
41+
* 无效:..., 1.., .1., 1.
42+
*/
43+
export const VERSION_NUMBER_REGEX = /^v?\d+(?:\.\d+)*(?:[-+][a-zA-Z0-9.]+)?$/
44+
45+
/**
46+
* Markdown 中的版本标记正则表达式(用于 @new、@1.2.0 等)
47+
*/
48+
export const MARKDOWN_BADGE_REGEX = /@(new|deprecated|beta|alpha|\d+(?:\.\d+)*(?:[-+][a-zA-Z0-9.]+)?)/g
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Badge 插件统一导出
3+
*/
4+
5+
export { SidebarBadgePlugin } from './SidebarPlugin'
6+
export { MarkdownBadgePlugin } from './MarkdownPlugin'
7+
export * from './constants'
8+
export * from './utils'
9+
10+
export type { SidebarBadgeOptions } from './SidebarPlugin'
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Badge 工具函数
3+
*/
4+
5+
import type { BadgeType, BadgeValue } from './constants'
6+
import { BADGE_TEXT_MAP, BADGE_CLASS_MAP, VERSION_NUMBER_REGEX } from './constants'
7+
8+
/**
9+
* 转义 HTML 特殊字符以防止 XSS
10+
*/
11+
function escapeHtml(text: string): string {
12+
const htmlEscapeMap: Record<string, string> = {
13+
'&': '&amp;',
14+
'<': '&lt;',
15+
'>': '&gt;',
16+
'"': '&quot;',
17+
"'": '&#39;',
18+
}
19+
return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char])
20+
}
21+
22+
/**
23+
* 判断是否为版本号
24+
*/
25+
export function isVersionNumber(badge: string): boolean {
26+
return VERSION_NUMBER_REGEX.test(badge)
27+
}
28+
29+
/**
30+
* 获取 Badge 类型
31+
*/
32+
export function getBadgeType(badge: BadgeValue): BadgeType {
33+
return isVersionNumber(badge) ? 'new' : (badge as BadgeType)
34+
}
35+
36+
/**
37+
* 获取 Badge 显示文本
38+
*/
39+
export function getBadgeText(badge: BadgeValue): string {
40+
if (isVersionNumber(badge)) {
41+
return badge
42+
}
43+
return BADGE_TEXT_MAP[badge as BadgeType] || badge
44+
}
45+
46+
/**
47+
* 获取 Badge CSS 类名
48+
*/
49+
export function getBadgeClass(badge: BadgeValue): string {
50+
const badgeType = getBadgeType(badge)
51+
return BADGE_CLASS_MAP[badgeType] || BADGE_CLASS_MAP.new
52+
}
53+
54+
/**
55+
* 创建 Badge HTML
56+
*/
57+
export function createBadgeHTML(badge: BadgeValue): string {
58+
const badgeText = escapeHtml(getBadgeText(badge))
59+
const badgeClass = getBadgeClass(badge)
60+
return `<span class="${badgeClass}">${badgeText}</span>`
61+
}
62+
63+
/**
64+
* 为文本添加 Badge
65+
*/
66+
export function withBadge(text: string, badge: BadgeValue): string {
67+
const badgeHTML = createBadgeHTML(badge)
68+
return `${text} ${badgeHTML}`
69+
}

0 commit comments

Comments
 (0)