fix: code group-icons by data-title, close #557

This commit is contained in:
YunYouJun 2025-07-12 17:11:21 +08:00
parent 61eb4610e8
commit f8f0e8df22
19 changed files with 1445 additions and 1684 deletions

View File

@ -704,18 +704,6 @@ export default defineValaxyConfig({
## Add Code Block Title And Icons {lang="en"}
默认**关闭**,你可以通过将 `groupIcons` 设置为 `空对象` 来开启:
```ts [valaxy.config.ts]
import { defineValaxyConfig } from 'valaxy'
export default defineValaxyConfig({
groupIcons: {
customIcon: {}
}
})
```
它基于 [vitepress-plugin-group-icons](https://github.com/yuyinws/vitepress-plugin-group-icons) 实现,内置了一些[常用图标](https://vp.yuy1n.io/features.html#built-in-icons),你可以如下自定义更多图标。
```ts [valaxy.config.ts] {5-14}

View File

@ -44,6 +44,8 @@ export default defineConfig({
Refer to the corresponding theme documentation, configure `themeConfig`.
:::
> [valaxy-theme-yun](https://github.com/YunYouJun/valaxy/blob/main/packages/valaxy-theme-yun/docs/README.md)
```ts [valaxy.config.ts]
import { defineConfig } from 'valaxy'

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "0.23.6",
"private": true,
"packageManager": "pnpm@10.12.4",
"packageManager": "pnpm@10.13.1",
"description": "📄 Vite & Vue powered static blog generator.",
"author": {
"email": "me@yunyoujun.cn",
@ -82,28 +82,28 @@
"valaxy-theme-yun": "workspace:*"
},
"devDependencies": {
"@antfu/eslint-config": "^4.16.1",
"@antfu/eslint-config": "^4.16.2",
"@iconify-json/logos": "catalog:build",
"@iconify-json/vscode-icons": "catalog:build",
"@microsoft/api-extractor": "^7.52.8",
"@playwright/test": "^1.53.1",
"@playwright/test": "^1.54.1",
"@types/debug": "^4.1.12",
"@types/fs-extra": "^11.0.4",
"@types/markdown-it-attrs": "^4.1.3",
"@types/markdown-it-container": "^2.0.10",
"@types/markdown-it-emoji": "^3.0.1",
"@types/node": "^24.0.7",
"@types/node": "^24.0.13",
"@types/prompts": "^2.4.9",
"bumpp": "^10.2.0",
"cross-env": "^7.0.3",
"eslint": "^9.30.0",
"eslint": "^9.31.0",
"https-localhost": "^4.7.1",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"npm-run-all": "^4.1.5",
"prompts": "^2.4.2",
"rimraf": "^6.0.1",
"stylelint": "^16.21.0",
"stylelint": "^16.21.1",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard-scss": "^15.0.1",
"tsup": "^8.5.0",
@ -126,6 +126,9 @@
]
}
},
"resolutions": {
"vite": "catalog:build"
},
"lint-staged": {
"{packages,demo,scripts}/**/*.{js,ts,vue,json,yml}": [
"eslint --fix"

View File

@ -48,12 +48,12 @@
"@advjs/gui": "0.0.7-beta.7",
"@iconify-json/ri": "catalog:build",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@primevue/themes": "^4.3.5",
"@primevue/themes": "catalog:frontend",
"@types/body-parser": "^1.19.6",
"@types/splitpanes": "^2.2.6",
"@types/wicg-file-system-access": "^2023.10.6",
"gray-matter": "^4.0.3",
"primevue": "^4.3.5",
"primevue": "catalog:frontend",
"splitpanes": "^4.0.4",
"typescript": "catalog:build",
"unbuild": "catalog:build",

View File

@ -1,6 +1,8 @@
<script lang="ts" setup>
import { useSiteConfig } from 'valaxy'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const siteConfig = useSiteConfig()
</script>
@ -18,10 +20,10 @@ const siteConfig = useSiteConfig()
class="site-subtitle block"
text="xs"
>
{{ siteConfig.subtitle }}
{{ t(siteConfig.subtitle) }}
</h4>
<div v-if="siteConfig.description" class="site-description">
{{ siteConfig.description }}
{{ t(siteConfig.description) }}
</div>
</div>
</template>

View File

@ -1,9 +1,12 @@
<script setup lang="ts">
import { useFrontmatter, useSiteConfig } from 'valaxy'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useYunAppStore } from '../../stores'
const { t } = useI18n()
const yunApp = useYunAppStore()
const fm = useFrontmatter()
const siteConfig = useSiteConfig()
@ -68,7 +71,7 @@ function goToLink() {
</span>
</div>
<span v-if="showSiteTitle" class="font-light truncate">
{{ siteConfig.title }}
{{ t(siteConfig.title) }}
</span>
</div>
</template>

View File

@ -1,11 +1,13 @@
<script setup lang="ts">
import { useSiteConfig } from 'valaxy'
import { useI18n } from 'vue-i18n'
const siteConfig = useSiteConfig()
const { t } = useI18n()
</script>
<template>
<div v-if="siteConfig.description" class="site-description text-$va-c-text text-sm">
{{ siteConfig.description }}
{{ t(siteConfig.description) }}
</div>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useSiteConfig } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useThemeConfig } from '../../composables'
@ -7,6 +8,8 @@ const router = useRouter()
const siteConfig = useSiteConfig()
const themeConfig = useThemeConfig()
const { t } = useI18n()
// bg-gradient-to-r gradient-text from-#1e3c72 to-dark dark:(from-#66a6ff to-blue-500)
</script>
@ -16,14 +19,14 @@ const themeConfig = useThemeConfig()
class="site-name text-lg leading-loose"
:class="themeConfig.banner.siteNameClass"
>
{{ siteConfig.title }}
{{ t(siteConfig.title) }}
</RouterLink>
<span
v-else
class="site-name text-lg leading-loose"
:class="themeConfig.banner.siteNameClass"
>
{{ siteConfig.title }}
{{ t(siteConfig.title) }}
</span>
</template>

View File

@ -31,7 +31,7 @@
"@vueuse/motion": "^3.0.3",
"animejs": "^4.0.2",
"gsap": "^3.13.0",
"primevue": "^4.3.5"
"primevue": "catalog:frontend"
},
"devDependencies": {
"@types/animejs": "^3.1.13",

View File

@ -180,7 +180,8 @@ export interface ThemeConfig extends DefaultTheme.Config {
/**
* nav items
* @zh
* @zh
*
*/
nav?: NavItem[]
/**

View File

@ -20,6 +20,7 @@ import { setupValaxyDevTools } from './utils/dev'
*/
import '#valaxy/styles'
import 'uno.css'
import 'virtual:group-icons.css'
const valaxyConfig = initValaxyConfig()

View File

@ -1,17 +1,15 @@
// ref vitepress
// src/node/markdown/plugins/containers.ts
import type MarkdownIt from 'markdown-it'
import type { MarkdownItAsync } from 'markdown-it-async'
import type Token from 'markdown-it/lib/token.mjs'
import type {
Options,
} from './preWrapper'
import container from 'markdown-it-container'
import { nanoid } from 'nanoid'
import {
extractTitle,
getAdaptiveThemeMarker,
} from './preWrapper'
type ContainerArgs = [
@ -22,33 +20,35 @@ type ContainerArgs = [
},
]
function createContainer(classes: string, { icon, color, text: defaultTitle, langs }: BlockItem = {}): ContainerArgs {
function createContainer(classes: string, { icon, color, text: defaultTitle, langs }: BlockItem = {}, md: MarkdownItAsync): ContainerArgs {
return [
container,
classes,
{
render(tokens, idx) {
const token = tokens[idx]
const info = token.info.trim().slice(classes.length).trim()
if (token.nesting === 1) {
if (classes === 'details') {
return `<details class="${classes} custom-block">${
`<summary>${info}</summary>`
}\n`
}
let iconTag = ''
token.attrJoin('class', `${classes} custom-block`)
const attrs = md.renderer.renderAttrs(token)
const info = token.info.trim().slice(classes.length).trim()
let iconTag = ''
if (icon)
iconTag = `<i class="icon ${icon}" ${color ? `style="color: ${color}"` : ''}></i>`
let title = `<span lang="en">${info || defaultTitle}</span>`
let titleWithLang = `<span lang="en">${info || defaultTitle}</span>`
if (langs) {
Object.keys(langs).forEach((lang) => {
title += `<span lang="${lang}">${info || langs[lang]}</span>`
titleWithLang += `<span lang="${lang}">${info || langs[lang]}</span>`
})
}
const title = md.renderInline(titleWithLang, {})
const titleClass
= `custom-block-title${info ? '' : ' custom-block-title-default'}`
return `<div class="${classes} custom-block"><p class="custom-block-title">${iconTag}${title}</p>\n`
if (classes === 'details')
return `<details ${attrs}><summary>${title}</summary>\n`
return `<div ${attrs}><p class="${titleClass}">${iconTag}${title}</p>\n`
}
else {
return classes === 'details' ? '</details>\n' : '</div>\n'
@ -111,37 +111,47 @@ const defaultBlocksOptions: ContainerOptions = {
},
}
export function containerPlugin(md: MarkdownIt, options: Options, containerOptions: ContainerOptions = {}) {
const blockKeys = Object.keys(Object.assign(defaultBlocksOptions, containerOptions))
export function containerPlugin(md: MarkdownItAsync, containerOptions: ContainerOptions = {}) {
const blockKeys = new Set(Object.keys(Object.assign(defaultBlocksOptions, containerOptions)))
blockKeys.forEach((optionKey) => {
const option: BlockItem = {
...defaultBlocksOptions[optionKey as keyof Blocks],
...(containerOptions[optionKey as keyof Blocks] || {}),
}
md.use(...createContainer(optionKey, option))
md.use(...createContainer(optionKey, option, md))
})
md.use(...createCodeGroup(options))
// explicitly escape Vue syntax
md.use(container, 'v-pre', {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div v-pre>\n` : `</div>\n`,
})
md.use(container, 'raw', {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n`,
})
const languages = ['zh-CN', 'en']
languages.forEach((lang) => {
md.use(container, lang, {
render: (tokens: Token[], idx: number) => tokens[idx].nesting === 1 ? `<div lang="${lang}">\n` : '</div>\n',
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div lang="${lang}">\n` : '</div>\n',
})
})
md.use(...createCodeGroup(md))
}
function createCodeGroup(options: Options): ContainerArgs {
function createCodeGroup(md: MarkdownItAsync): ContainerArgs {
return [
container,
'code-group',
{
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
const name = nanoid(5)
let tabs = ''
let checked = 'checked="checked"'
let checked = 'checked'
for (
let i = idx + 1;
@ -163,8 +173,8 @@ function createCodeGroup(options: Options): ContainerArgs {
)
if (title) {
const id = nanoid(7)
tabs += `<input type="radio" name="group-${name}" id="tab-${id}" ${checked}><label for="tab-${id}">${title}</label>`
tabs += `<input type="radio" name="group-${idx}" id="tab-${i}" ${checked}/>`
tabs += `<label data-title="${md.utils.escapeHtml(title)}" for="tab-${i}">${title}</label>`
if (checked && !isHtml)
tokens[i].info += ' active'
@ -173,9 +183,7 @@ function createCodeGroup(options: Options): ContainerArgs {
}
}
return `<div class="vp-code-group${getAdaptiveThemeMarker(
options,
)}"><div class="tabs">${tabs}</div><div class="blocks">\n`
return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">\n`
}
return `</div></div>\n`
},

View File

@ -5,7 +5,7 @@ import type { MarkdownEnv } from '../../env'
import type { ThemeOptions } from '../../types'
export interface Options {
hasSingleTheme: boolean
codeCopyButtonTitle: string
theme: ThemeOptions
siteConfig?: SiteConfig
}
@ -21,10 +21,6 @@ export function extractLang(info: string) {
.replace(/^ansi$/, '')
}
export function getAdaptiveThemeMarker(options: Options) {
return options.hasSingleTheme ? '' : ' vp-adaptive-theme'
}
export function extractTitle(info: string, html = false) {
if (html) {
return (
@ -59,15 +55,20 @@ export function preWrapperPlugin(md: MarkdownIt, options: Options) {
// remove title from info
token.info = token.info.replace(/\[.*\]/, '')
// eslint-disable-next-line regexp/no-unused-capturing-group
const active = / active( |$)/.test(token.info) ? ' active' : ''
token.info = token.info.replace(/ active$/, '').replace(/ active /, ' ')
const lang = extractLang(token.info)
const rawCode = fence(...args)
return `
<div ${getCodeHeightLimitStyle(options, env)} class="language-${lang}${getAdaptiveThemeMarker(options)}${
// eslint-disable-next-line regexp/no-unused-capturing-group
/ active( |$)/.test(token.info) ? ' active' : ''
}">
<button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}<button class="collapse"></button>
</div>`
return (
`<div ${getCodeHeightLimitStyle(options, env)} class="language-${lang}${active}">`
+ `<button title="${options.codeCopyButtonTitle}" class="copy"></button>`
+ `<span class="lang">${lang}</span>`
+ `${rawCode}`
+ '<button class="collapse"></button>'
+ '</div>'
)
}
}

View File

@ -48,7 +48,6 @@ export async function setupMarkdownPlugins(
) {
const mdOptions = options?.config.markdown || {}
const theme = mdOptions.theme ?? defaultCodeTheme
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
const siteConfig = options?.config.siteConfig || {}
if (mdOptions.preConfig)
@ -59,8 +58,6 @@ export async function setupMarkdownPlugins(
.use(preWrapperPlugin, { theme, siteConfig })
.use(snippetPlugin, options?.userRoot)
.use(containerPlugin, {
hasSingleTheme,
}, {
...mdOptions.blocks,
...mdOptions?.container,
})
@ -88,9 +85,16 @@ export async function setupMarkdownPlugins(
md.use(emojiPlugin)
.use(footnotePlugin)
.use(footnoteTooltipPlugin)
// if (!isExcerpt) {
md.use(anchorPlugin, {
slugify,
getTokensText: (tokens) => {
return tokens
.filter(t => !['html_inline', 'emoji'].includes(t.type))
.map(t => t.content)
.join('')
},
permalink: anchorPlugin.permalink.linkInsideHeader({
symbol: '&ZeroWidthSpace;',
renderAttrs: (slug, state) => {
@ -113,6 +117,7 @@ export async function setupMarkdownPlugins(
md
.use(headersPlugin, {
level: [2, 3, 4, 5, 6],
slugify,
...(typeof mdOptions.headers === 'boolean' ? undefined : mdOptions.headers),
} as HeadersPluginOptions)
@ -121,6 +126,7 @@ export async function setupMarkdownPlugins(
} as SfcPluginOptions)
.use(titlePlugin)
.use(tocPlugin, {
slugify,
...mdOptions.toc,
} as TocPluginOptions)
@ -151,12 +157,10 @@ export async function setupMarkdownPlugins(
md.use(TaskLists)
if (options?.config.groupIcons) {
const { groupIconMdPlugin } = await import('vitepress-plugin-group-icons')
md.use(groupIconMdPlugin, {
titleBar: { includeSnippet: true },
})
}
const { groupIconMdPlugin } = await import('vitepress-plugin-group-icons')
md.use(groupIconMdPlugin, {
titleBar: { includeSnippet: true },
})
if (mdOptions.config)
mdOptions.config(md)

View File

@ -129,40 +129,18 @@ export async function ViteValaxyPlugins(
createFixPlugins(options),
]
if (valaxyConfig.groupIcons) {
const { groupIconVitePlugin } = await import('vitepress-plugin-group-icons')
plugins.push(
groupIconVitePlugin({
customIcon: {
nodejs: 'vscode-icons:file-type-node',
playwright: 'vscode-icons:file-type-playwright',
typedoc: 'vscode-icons:file-type-typedoc',
eslint: 'vscode-icons:file-type-eslint',
},
...valaxyConfig.groupIcons,
}),
)
}
else {
// virtual module placeholder
const virtualCssId = 'virtual:group-icons.css'
const resolvedVirtualCssId = `\0${virtualCssId}`
plugins.push({
name: 'valaxy:virtual:group-icons.css',
resolveId(id) {
if (id === virtualCssId) {
return resolvedVirtualCssId
}
return void 0
const { groupIconVitePlugin } = await import('vitepress-plugin-group-icons')
plugins.push(
groupIconVitePlugin({
customIcon: {
nodejs: 'vscode-icons:file-type-node',
playwright: 'vscode-icons:file-type-playwright',
typedoc: 'vscode-icons:file-type-typedoc',
eslint: 'vscode-icons:file-type-eslint',
},
async load(id) {
if (id === resolvedVirtualCssId) {
return ''
}
return void 0
},
})
}
...valaxyConfig.groupIcons,
}),
)
if (valaxyConfig.visualizer) {
try {

View File

@ -161,6 +161,9 @@ export interface ValaxyExtendConfig {
* @see https://github.com/btd/rollup-plugin-visualizer
*/
visualizer?: PluginVisualizerOptions
/**
* @see https://github.com/yuyinws/vitepress-plugin-group-icons
*/
groupIcons?: Partial<GroupIconsOptions>
/**
* unocss presets

View File

@ -74,9 +74,9 @@
"@valaxyjs/utils": "workspace:*",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/devtools-api": "7.7.2",
"@vueuse/core": "^13.4.0",
"@vueuse/integrations": "^13.4.0",
"beasties": "^0.3.4",
"@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0",
"beasties": "^0.3.5",
"birpc": "^2.4.0",
"consola": "catalog:build",
"cross-spawn": "^7.0.6",
@ -109,7 +109,7 @@
"markdown-it-table-of-contents": "^0.9.0",
"markdown-it-task-lists": "^2.1.1",
"medium-zoom": "^1.1.0",
"mermaid": "^11.7.0",
"mermaid": "^11.8.1",
"mlly": "^1.7.4",
"nprogress": "^0.2.0",
"open": "10.1.0",
@ -124,7 +124,7 @@
"star-markdown-css": "^0.5.3",
"table": "^6.9.0",
"unhead": "catalog:build",
"unocss": "^66.3.2",
"unocss": "^66.3.3",
"unplugin-vue-components": "28.0.0",
"unplugin-vue-markdown": "^29.1.0",
"unplugin-vue-router": "catalog:build",
@ -136,7 +136,7 @@
"vite-ssg": "^28.0.0",
"vite-ssg-sitemap": "^0.9.0",
"vitepress-plugin-group-icons": "^1.6.1",
"vue": "^3.5.17",
"vue": "catalog:frontend",
"vue-i18n": "catalog:build",
"vue-router": "^4.5.1",
"yargs": "^18.0.0",

File diff suppressed because it is too large Load Diff

View File

@ -13,19 +13,23 @@ catalogs:
'@iconify-json/carbon': ^1.2.10
'@iconify-json/logos': ^1.2.4
'@iconify-json/ri': ^1.2.5
'@iconify-json/simple-icons': ^1.2.40
'@iconify-json/simple-icons': ^1.2.42
'@iconify-json/vscode-icons': ^1.2.23
'@unhead/addons': ^2.0.11
'@unhead/schema-org': ^2.0.11
'@unhead/vue': ^2.0.11
'@unhead/addons': ^2.0.12
'@unhead/schema-org': ^2.0.12
'@unhead/vue': ^2.0.12
consola: ^3.4.2
typescript: ^5.8.3
unbuild: ^3.5.0
unhead: ^2.0.11
unhead: ^2.0.12
unplugin-vue-router: ^0.14.0
vite: ^7.0.0
vue-i18n: ^11.1.7
zx: ^8.6.0
vite: ^7.0.4
vue-i18n: ^11.1.9
zx: ^8.7.0
frontend:
'@primevue/themes': ^4.3.6
primevue: ^4.3.6
vue: ^3.5.17
onlyBuiltDependencies:
- vue-demi