feat: add support optional icon for file-tree and code-tabs

This commit is contained in:
pengzhanbo 2024-09-25 13:22:47 +08:00
parent 43d3e56230
commit eff4ec3a1a
18 changed files with 365 additions and 66 deletions

View File

@ -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,

View File

@ -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/) 的图标。 主题一方面提供了
[`<Icon />`](../功能/组件.md#图标) 组件来支持在 markdown 中使用图标,
在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。
一方面,主题还提供了另一种可选的方式,以更简单的方式,在 Markdown 中使用图标,并且将 图标资源编译到
本地静态资源中
主题一方面提供了[`<Icon />`](../功能/组件.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

View File

@ -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 <!-- [!code hl] -->
```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
```
:::
## 分组标题图标 <Badge type="tip" text="v1.0.0-rc.103 +" />
主题支持在 代码块分组的组标题上显示图标。 图标根据 标题,即 `@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+ 不同的图标,对最终的构建包体积影响
也不会很大。
:::

View File

@ -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` 包,以便主题能够将图标全部解析为本地资源,
这可以有效的提高页面的访问体验。
:::
## 图标组件
通过 `<Icon />` 组件来使用图标。

View File

@ -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;

View File

@ -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<CodeTabsOptions> = (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 `<template #title${index}="{ value, isActive }">${icon ? `<VPIcon name="${icon}"/>` : ''}<span>${title}</span></template>`
}).join('')

View File

@ -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 `<div class="vp-file-tree">${title ? `<p class="vp-file-tree-title">${title}</p>` : ''}`
}
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,

View File

@ -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 : {})
}
}

View File

@ -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

View File

@ -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<never> = md =>
md.inline.ruler.before('emphasis', 'iconify', createTokenizer())
export const iconsPlugin: PluginWithOptions<IconsOptions> = (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)

View File

@ -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)

View File

@ -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'])
}
},

View File

@ -0,0 +1,3 @@
export interface CodeTabsOptions {
icon?: boolean | { named?: false | string[], extensions?: false | string[] }
}

View File

@ -0,0 +1,5 @@
export type FileTreeIconMode = 'simple' | 'colored'
export interface FileTreeOptions {
icon?: FileTreeIconMode
}

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -13,7 +13,7 @@ defineProps<{
<template>
<div class="vp-link-card">
<span class="body">
<VPLink :href="href" no-icon class="link">
<VPLink :href="href" no-icon class="link no-icon">
<slot name="title">
<VPIcon v-if="icon" :name="icon" />
<span v-if="title" v-html="title" />