diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 66274a92..0be3ff71 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -20,10 +20,8 @@ export const theme: Theme = plumeTheme({ imageSize: 'all', pdf: true, caniuse: true, - plot: true, bilibili: true, youtube: true, - icons: true, codepen: true, replit: true, codeSandbox: true, diff --git a/docs/notes/theme/guide/markdown/进阶.md b/docs/notes/theme/guide/markdown/进阶.md index 6a5b30b8..15e2de21 100644 --- a/docs/notes/theme/guide/markdown/进阶.md +++ b/docs/notes/theme/guide/markdown/进阶.md @@ -178,6 +178,7 @@ tags: - 通过加粗文件名或目录名来突出显示,例如 `**README.md**` - 通过在名称后添加更多文本来为文件或目录添加注释 - 使用 `...` 或 `…` 作为名称来添加占位符文件和目录。 +- 在 `:::file-tree` 后添加 `:simple-icon` 或 添加 `:colored-icon` 可以切换为简单图标或彩色图标,默认为彩色图标。 **输入:** @@ -233,6 +234,60 @@ tags: - … ::: +### 使用简单图标 + +**输入:** + +```md +::: file-tree:simple-icon +- docs + - .vuepress + - config.ts + - page1.md + - README.md +- package.json +::: +``` + +**输出:** + +::: file-tree:simple-icon + +- docs + - .vuepress + - config.ts + - page1.md + - README.md +- package.json +::: + +### 配置 + +你可以在 `plugins.mdPower.fileTree` 选项中配置 文件树的图标默认类型: + +```ts +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + markdownPower: { + fileTree: { + icon: 'simple', // 'simple' | 'colored' + } + }, + } + }) +}) +``` + +::: tip 担心彩色图标会影响构建包体积? +您无需担心,文件树的彩色图标 也是从 `iconify` 解析获取,推荐您在本地安装 `@iconify/json` 项目, +主题会自动将 `@iconify/json` 中的图标数据解析为本地图标资源,即使您在不同的页面 +多次使用,这包括了 导航栏、侧边栏、图标组件等,相同图标仅会存在一份资源! + +每个彩色图标的大小约在 `1kb ~ 2kb` 之间,即使您的文件树非常夸张的使用了 100+ 不同的图标,对最终的构建包体积影响 +也不会很大。 +::: + ## 选项组 在 Markdown 中支持选项卡。 @@ -513,34 +568,13 @@ interface PlotOptions { ## iconify 图标 -在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 主题一方面提供了 -[``](../功能/组件.md#图标) 组件来支持在 markdown 中使用图标, +在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 -一方面,主题还提供了另一种可选的方式,以更简单的方式,在 Markdown 中使用图标,并且将 图标资源编译到 -本地静态资源中。 +主题一方面提供了[``](../功能/组件.md#图标) 组件来支持在 markdown 中使用图标; +另一方面,主题还提供了图标的 markdown 语法,以更简单的方式,在 Markdown 中使用图标。 -### 配置 - -该功能默认不启用,你需要在 `theme` 配置中启用。 - -::: code-tabs -@tab .vuepress/config.ts - -```ts -export default defineUserConfig({ - theme: plumeTheme({ - plugins: { - markdownPower: { - icons: true, - }, - } - }) -}) -``` - -::: - -同时,该功能还需要你额外安装 `@iconify/json` 依赖。 +为了更好的使用该功能,主题推荐你安装 `@iconify/json` 依赖。主题会自动解析 `@iconify/json` 中的图标数据, +将有使用的图标打包为本地资源,以获得更好的访问体验。 ::: code-tabs @tab pnpm diff --git a/docs/notes/theme/guide/代码/代码组.md b/docs/notes/theme/guide/代码/代码组.md index c189e04f..53808417 100644 --- a/docs/notes/theme/guide/代码/代码组.md +++ b/docs/notes/theme/guide/代码/代码组.md @@ -70,3 +70,193 @@ export default config ::: 你还可以通过 `@tab:active` 选择其中一个代码块作为默认显示的代码块。 + +**输入:** + +````md +::: code-tabs +@tab config.js +```js +/** + * @type {import('vuepress').UserConfig} + */ +const config = { + // .. +} + +export default config +``` + +@tab:active config.ts +```ts +import type { UserConfig } from 'vuepress' + +const config: UserConfig = { + // .. +} + +export default config +``` +::: +```` + +**输出:** + +::: code-tabs +@tab config.js + +```js +/** + * @type {import('vuepress').UserConfig} + */ +const config = { + // .. +} + +export default config +``` + +@tab:active config.ts + +```ts +import type { UserConfig } from 'vuepress' + +const config: UserConfig = { + // .. +} + +export default config +``` + +::: + +## 分组标题图标 + +主题支持在 代码块分组的组标题上显示图标。 图标根据 标题,即 `@tab 标题` 进行解析适配不同的图标 + +默认解析规则与 [文件树](../markdown/进阶.md#文件树) 一致。 + +如, `pnpm / yarn / npm` 分组图标: + +**输入:** + +````md +::: code-tabs +@tab pnpm + +```sh +pnpm i +``` + +@tab yarn + +```sh +yarn +``` + +@tab npm + +```sh +npm install +``` + +::: +```` + +**输出:** + +::: code-tabs +@tab pnpm + +```sh +pnpm i +``` + +@tab yarn + +```sh +yarn +``` + +@tab npm + +```sh +npm install +``` + +::: + +主题默认适配了 前端主流的一些技术: + +- 运行环境,如: `NodeJs / Deno / Bun` +- 包管理器,如: `pnpm / yarn / npm` +- 库、框架,如: `vue / react / angular / svelte / solid / Next / Nuxt` 等 + +还包括一些主流的语言,如: `TypeScript / JavaScript / C / C++ / Java / Python / Rust / Kotlin / Swift / Go` 等 + +::: info +如果您发现您所使用的 库、框架、语言等未能正确显示图标,可以提出 [issue](https://github.com/pengzhanbo/vuepress-theme-plume/issues/new) 告诉我,我会尽量添加相关图标。 +::: + +### 配置 + +您可以通过 `plugins.mdPower.codeTabs` 控制分组图标的行为: + +```ts +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + markdownPower: { + codeTabs: { + icon: true, // CodeTabsOptions + } + }, + } + }) +}) +``` + +```ts +export interface CodeTabsOptions { + icon?: boolean | { named?: false | string[], extensions?: false | string[] } +} +``` + +- `true`: 使用默认解析规则显示图标 +- `false`: 不显示图标 +- `{ named?: false | string[], extensions?: false | string[] }`: + - `named`: 表示 库/框架/语言 名称,严格匹配 `@tab 标题` 中的 `标题` 字段,如 `pnpm`、`yarn`、`npm` 等,如果设置为 `false` 则不显示图标,如果为 空数组,则使用默认匹配规则 + - `extensions`: 表示 文件扩展名,匹配 `@tab 标题` 中的 `标题` 字段是否包含扩展名,如 `.ts`、`.js` 等,如果设置为 `false` 则不显示图标,如果为 空数组,则使用默认匹配规则 + + 请注意, `named` 和 `extensions` 数组中的元素必须是 `string` 类型,且严格区分大小写。 + +举一个例子,如果您是一个前端开发,且仅想在 `pnpm/yarn/npm` 分组时显示图标,其它分组时不显示图标, +那么可以进行如下配置: + +```ts +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + markdownPower: { + codeTabs: { + icon: { + named: ['pnpm', 'yarn', 'npm'], // [!code ++:2] + extensions: false, + } + } + }, + } + }) +}) +``` + +你可以灵活地配置图标显示规则。 + +::: tip 担心图标会影响构建包体积? +您无需担心,代码块分组的图标 也是从 `iconify` 解析获取,推荐您在本地安装 `@iconify/json` 项目, +主题会自动将 `@iconify/json` 中的图标数据解析为本地图标资源,即使您在不同的页面 +多次使用,这包括了 导航栏、侧边栏、图标组件等,相同图标仅会存在一份资源! + +每个彩色图标的大小约在 `1kb ~ 2kb` 之间,即使您的文件树非常夸张的使用了 100+ 不同的图标,对最终的构建包体积影响 +也不会很大。 +::: diff --git a/docs/notes/theme/guide/功能/图标.md b/docs/notes/theme/guide/功能/图标.md index f43357cb..54957cdf 100644 --- a/docs/notes/theme/guide/功能/图标.md +++ b/docs/notes/theme/guide/功能/图标.md @@ -8,9 +8,24 @@ permalink: /guide/features/icon/ ## 概述 -主题支持 [iconify](https://icon-sets.iconify.design/) 的所有图标,并提供了不同的方式来使用它们。 +主题支持 [iconify](https://icon-sets.iconify.design/) 的所有图标,并提供了不同的方式来使用它们: -## 组件 +- [导航栏图标](../../config/导航栏配置.md#配置) +- [侧边栏图标](../../guide/知识笔记.md#侧边栏图标) +- [图标组件](#图标组件) +- [图标语法糖](../../guide/markdown/进阶.md#iconify-图标) +- [文件树图标](../../guide/markdown/进阶.md#文件树) +- 代码分组标题图标 + +::: tip 主题对图标的优化 +上述的不同的使用图标的方式,主题在内部都采取了相同的解析策略,即使您在不同的位置使用了相同的图标, +也不会重复加载相同的图标资源。 + +图标默认是通过远程请求加载,主题也非常建议您在本地项目中安装 `@iconify/json` 包,以便主题能够将图标全部解析为本地资源, +这可以有效的提高页面的访问体验。 +::: + +## 图标组件 通过 `` 组件来使用图标。 diff --git a/plugins/plugin-md-power/src/client/components/CodeTabs.vue b/plugins/plugin-md-power/src/client/components/CodeTabs.vue index 5f1df5e3..95afd174 100644 --- a/plugins/plugin-md-power/src/client/components/CodeTabs.vue +++ b/plugins/plugin-md-power/src/client/components/CodeTabs.vue @@ -184,6 +184,10 @@ function onTabNavClick(index: number): void { transition: background var(--vp-t-color); } +.vp-code-tab-nav:focus-visible { + outline: none; +} + .vp-code-tab-nav.active { color: var(--vp-code-tab-active-text-color); background: transparent; diff --git a/plugins/plugin-md-power/src/node/container/codeTabs.ts b/plugins/plugin-md-power/src/node/container/codeTabs.ts index 10164a6f..9e52a45f 100644 --- a/plugins/plugin-md-power/src/node/container/codeTabs.ts +++ b/plugins/plugin-md-power/src/node/container/codeTabs.ts @@ -1,9 +1,32 @@ -import type { PluginSimple } from 'markdown-it' +import type { PluginWithOptions } from 'markdown-it' +import type { CodeTabsOptions } from '../../shared/index.js' import { tab } from '@mdit/plugin-tab' -import { getFileIconName } from '../fileIcons/index.js' +import { isPlainObject } from '@vuepress/helper' +import { definitions, getFileIconName, getFileIconTypeFromExtension } from '../fileIcons/index.js' import { stringifyProp } from '../utils/stringifyProp.js' -export const codeTabs: PluginSimple = (md) => { +export const codeTabs: PluginWithOptions = (md, options: CodeTabsOptions = {}) => { + const getIcon = (filename: string): string | undefined => { + if (options.icon === false) + return undefined + const { named, extensions } = isPlainObject(options.icon) ? options.icon : {} + if (named === false && definitions.named[filename]) + return undefined + if (extensions === false && getFileIconTypeFromExtension(filename)) { + return undefined + } + const hasNamed = named && named.length + const hasExt = extensions && extensions.length + if (hasNamed || hasExt) { + if (hasNamed && named.includes(filename)) + return definitions.named[filename] + if (hasExt && extensions.some(ext => filename.endsWith(ext))) + return getFileIconTypeFromExtension(filename) + return undefined + } + return getFileIconName(filename) + } + tab(md, { name: 'code-tabs', @@ -17,7 +40,7 @@ export const codeTabs: PluginSimple = (md) => { }) const titlesContent = titles.map((title, index) => { - const icon = getFileIconName(title) + const icon = getIcon(title) return `` }).join('') diff --git a/plugins/plugin-md-power/src/node/container/fileTree.ts b/plugins/plugin-md-power/src/node/container/fileTree.ts index df1964ec..abe9d7e6 100644 --- a/plugins/plugin-md-power/src/node/container/fileTree.ts +++ b/plugins/plugin-md-power/src/node/container/fileTree.ts @@ -1,8 +1,9 @@ import type { Markdown } from 'vuepress/markdown' +import type { FileTreeIconMode, FileTreeOptions } from '../../shared/index.js' import Token from 'markdown-it/lib/token.mjs' import container from 'markdown-it-container' import { removeEndingSlash, removeLeadingSlash } from 'vuepress/shared' -import { getFileIcon } from '../fileIcons/index.js' +import { defaultFile, defaultFolder, getFileIcon } from '../fileIcons/index.js' interface FileTreeNode { filename: string @@ -17,10 +18,22 @@ const closeType = `container_${type}_close` const componentName = 'FileTreeItem' const itemOpen = 'file_tree_item_open' const itemClose = 'file_tree_item_close' +const RE_SIMPLE_ICON = /:simple-icon\b/ +const RE_COLORED_ICON = /:colored-icon\b/ + +export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}) { + const getIcon = (filename: string, type: 'folder' | 'file', mode?: FileTreeIconMode): string => { + mode ||= options.icon || 'colored' + if (mode === 'simple') + return type === 'folder' ? defaultFolder : defaultFile + return getFileIcon(filename, type) + } -export function fileTreePlugin(md: Markdown) { const validate = (info: string): boolean => info.trim().startsWith(type) + const render = (tokens: Token[], idx: number): string => { + const mode = getFileIconMode(tokens[idx].info) + if (tokens[idx].nesting === 1) { const hasRes: number[] = [] // level stack for ( @@ -36,7 +49,7 @@ export function fileTreePlugin(md: Markdown) { hasRes.push(token.level) const [info, inline] = result const { filename, type, expanded, empty } = info - const icon = getFileIcon(filename, type) + const icon = getIcon(filename, type, mode) token.type = itemOpen token.tag = componentName @@ -56,9 +69,8 @@ export function fileTreePlugin(md: Markdown) { } } } - const info = tokens[idx].info.trim() - const title = info.slice(type.length).trim() + const title = resolveTitle(tokens[idx].info) return `
${title ? `

${title}

` : ''}` } else { @@ -69,6 +81,20 @@ export function fileTreePlugin(md: Markdown) { md.use(container, type, { validate, render }) } +function getFileIconMode(info: string): FileTreeIconMode | undefined { + if (RE_SIMPLE_ICON.test(info)) + return 'simple' + if (RE_COLORED_ICON.test(info)) + return 'colored' + return undefined +} + +function resolveTitle(info: string): string { + info = info.trim().slice(type.length).trim() + info = info.replace(RE_SIMPLE_ICON, '').replace(RE_COLORED_ICON, '') + return info.trim() +} + export function resolveTreeNodeInfo( tokens: Token[], current: Token, diff --git a/plugins/plugin-md-power/src/node/container/index.ts b/plugins/plugin-md-power/src/node/container/index.ts index 9b4ae23f..f76d98f4 100644 --- a/plugins/plugin-md-power/src/node/container/index.ts +++ b/plugins/plugin-md-power/src/node/container/index.ts @@ -1,6 +1,7 @@ import type { App } from 'vuepress' import type { Markdown } from 'vuepress/markdown' import type { MarkdownPowerPluginOptions } from '../../shared/index.js' +import { isPlainObject } from '@vuepress/helper' import { alignPlugin } from './align.js' import { codeTabs } from './codeTabs.js' import { fileTreePlugin } from './fileTree.js' @@ -17,13 +18,13 @@ export async function containerPlugin( // ::: tabs tabs(md) // ::: code-tabs - codeTabs(md) + codeTabs(md, options.codeTabs) if (options.repl) await langReplPlugin(app, md, options.repl) if (options.fileTree) { // ::: file-tree - fileTreePlugin(md) + fileTreePlugin(md, isPlainObject(options.fileTree) ? options.fileTree : {}) } } diff --git a/plugins/plugin-md-power/src/node/fileIcons/findIcon.ts b/plugins/plugin-md-power/src/node/fileIcons/findIcon.ts index 8cc28aef..d123ed60 100644 --- a/plugins/plugin-md-power/src/node/fileIcons/findIcon.ts +++ b/plugins/plugin-md-power/src/node/fileIcons/findIcon.ts @@ -30,7 +30,7 @@ export function getFileIconName(fileName: string, type: 'file' | 'folder' = 'fil return icon } -function getFileIconTypeFromExtension(fileName: string): string | undefined { +export function getFileIconTypeFromExtension(fileName: string): string | undefined { const firstDotIndex = fileName.indexOf('.') if (firstDotIndex === -1) return diff --git a/plugins/plugin-md-power/src/node/inline/icons.ts b/plugins/plugin-md-power/src/node/inline/icons.ts index b6eddedb..8956f3e3 100644 --- a/plugins/plugin-md-power/src/node/inline/icons.ts +++ b/plugins/plugin-md-power/src/node/inline/icons.ts @@ -6,13 +6,14 @@ */ import type { PluginWithOptions } from 'markdown-it' import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs' +import type { IconsOptions } from '../../shared/index.js' const [openTag, endTag] = [':[', ']:'] -export const iconsPlugin: PluginWithOptions = md => - md.inline.ruler.before('emphasis', 'iconify', createTokenizer()) +export const iconsPlugin: PluginWithOptions = (md, options = {}) => + md.inline.ruler.before('emphasis', 'iconify', createTokenizer(options)) -function createTokenizer(): RuleInline { +function createTokenizer(options: IconsOptions): RuleInline { return (state, silent) => { let found = false const max = state.posMax @@ -56,8 +57,8 @@ function createTokenizer(): RuleInline { state.posMax = state.pos state.pos = start + 2 - const [name, options = ''] = content.split(/\s+/) - const [size, color] = options.split('/') + const [name, opt = ''] = content.split(/\s+/) + const [size = options.size, color = options.color] = opt.split('/') const icon = state.push('vp_iconify_open', 'VPIcon', 1) icon.markup = openTag @@ -65,7 +66,7 @@ function createTokenizer(): RuleInline { if (name) icon.attrSet('name', name) if (size) - icon.attrSet('size', size) + icon.attrSet('size', String(size)) if (color) icon.attrSet('color', color) diff --git a/plugins/plugin-md-power/src/node/inline/index.ts b/plugins/plugin-md-power/src/node/inline/index.ts index 536960b1..92e52d62 100644 --- a/plugins/plugin-md-power/src/node/inline/index.ts +++ b/plugins/plugin-md-power/src/node/inline/index.ts @@ -6,6 +6,7 @@ import { mark } from '@mdit/plugin-mark' import { sub } from '@mdit/plugin-sub' import { sup } from '@mdit/plugin-sup' import { tasklist } from '@mdit/plugin-tasklist' +import { isPlainObject } from '@vuepress/helper' import { iconsPlugin } from './icons.js' import { plotPlugin } from './plot.js' @@ -22,12 +23,12 @@ export function inlineSyntaxPlugin( if (options.icons) { // :[collect:name]: - md.use(iconsPlugin) + md.use(iconsPlugin, isPlainObject(options.icons) ? options.icons : {}) } if ( options.plot === true - || (typeof options.plot === 'object' && options.plot.tag !== false) + || (isPlainObject(options.plot) && options.plot.tag !== false) ) { // !!plot!! md.use(plotPlugin) diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts index 07a5e43a..45671e6a 100644 --- a/plugins/plugin-md-power/src/node/plugin.ts +++ b/plugins/plugin-md-power/src/node/plugin.ts @@ -7,7 +7,9 @@ import { imageSizePlugin } from './enhance/imageSize.js' import { inlineSyntaxPlugin } from './inline/index.js' import { prepareConfigFile } from './prepareConfigFile.js' -export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin { +export function markdownPowerPlugin( + options: MarkdownPowerPluginOptions = {}, +): Plugin { return { name: 'vuepress-plugin-md-power', @@ -19,11 +21,7 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P extendsBundlerOptions(bundlerOptions, app) { if (options.repl) { - addViteOptimizeDepsInclude( - bundlerOptions, - app, - ['shiki/core', 'shiki/wasm'], - ) + addViteOptimizeDepsInclude(bundlerOptions, app, ['shiki/core', 'shiki/wasm']) } }, diff --git a/plugins/plugin-md-power/src/shared/codeTabs.ts b/plugins/plugin-md-power/src/shared/codeTabs.ts new file mode 100644 index 00000000..6e2bea8a --- /dev/null +++ b/plugins/plugin-md-power/src/shared/codeTabs.ts @@ -0,0 +1,3 @@ +export interface CodeTabsOptions { + icon?: boolean | { named?: false | string[], extensions?: false | string[] } +} diff --git a/plugins/plugin-md-power/src/shared/fileTree.ts b/plugins/plugin-md-power/src/shared/fileTree.ts new file mode 100644 index 00000000..e3070e01 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/fileTree.ts @@ -0,0 +1,5 @@ +export type FileTreeIconMode = 'simple' | 'colored' + +export interface FileTreeOptions { + icon?: FileTreeIconMode +} diff --git a/plugins/plugin-md-power/src/shared/icons.ts b/plugins/plugin-md-power/src/shared/icons.ts index 93ef6e56..9c249ea2 100644 --- a/plugins/plugin-md-power/src/shared/icons.ts +++ b/plugins/plugin-md-power/src/shared/icons.ts @@ -1,10 +1,4 @@ export interface IconsOptions { - /** - * The prefix of the icon className - * @default 'vp-mdi' - */ - prefix?: string - /** * The size of the icon * @default '1em' diff --git a/plugins/plugin-md-power/src/shared/index.ts b/plugins/plugin-md-power/src/shared/index.ts index 60ab43fc..beb2af52 100644 --- a/plugins/plugin-md-power/src/shared/index.ts +++ b/plugins/plugin-md-power/src/shared/index.ts @@ -1,6 +1,8 @@ export * from './caniuse.js' export * from './codepen.js' export * from './codeSandbox.js' +export * from './codeTabs.js' +export * from './fileTree.js' export * from './icons.js' export * from './jsfiddle.js' export * from './pdf.js' @@ -8,7 +10,5 @@ export * from './plot.js' export * from './plugin.js' export * from './repl.js' export * from './replit.js' - export * from './size.js' - export * from './video.js' diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts index becbe6c6..3fcf28e9 100644 --- a/plugins/plugin-md-power/src/shared/plugin.ts +++ b/plugins/plugin-md-power/src/shared/plugin.ts @@ -1,10 +1,16 @@ import type { CanIUseOptions } from './caniuse.js' +import type { CodeTabsOptions } from './codeTabs.js' +import type { FileTreeOptions } from './fileTree.js' import type { IconsOptions } from './icons.js' import type { PDFOptions } from './pdf.js' import type { PlotOptions } from './plot.js' import type { ReplOptions } from './repl.js' export interface MarkdownPowerPluginOptions { + /** + * 配置代码块分组 + */ + codeTabs?: CodeTabsOptions /** * 是否启用 PDF 嵌入语法 * @@ -92,7 +98,7 @@ export interface MarkdownPowerPluginOptions { * * @default false */ - fileTree?: boolean + fileTree?: boolean | FileTreeOptions /** * 是否启用 caniuse 嵌入语法 diff --git a/theme/src/client/components/global/VPLinkCard.vue b/theme/src/client/components/global/VPLinkCard.vue index 991ef701..e3398147 100644 --- a/theme/src/client/components/global/VPLinkCard.vue +++ b/theme/src/client/components/global/VPLinkCard.vue @@ -13,7 +13,7 @@ defineProps<{