Merge pull request #128 from pengzhanbo/RC-82

RC 82
This commit is contained in:
pengzhanbo 2024-07-23 12:03:22 +08:00 committed by GitHub
commit e690d48d0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 648 additions and 601 deletions

10
.vscode/launch.json vendored
View File

@ -5,13 +5,19 @@
"name": "dev",
"request": "launch",
"type": "node-terminal",
"command": "pnpm run dev"
"command": "pnpm dev"
},
{
"name": "build",
"request": "launch",
"type": "node-terminal",
"command": "pnpm build"
},
{
"name": "docs:dev",
"type": "node-terminal",
"request": "launch",
"command": "pnpm run docs:dev"
"command": "pnpm docs:dev"
},
{
"name": "docs:build",

View File

@ -56,7 +56,7 @@ export const zhNotes = definePlumeNotesConfig({
icon: 'lucide:box',
collapsed: false,
dir: '功能',
items: ['代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap'],
items: ['图标', '代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap'],
},
{
text: '自定义',

View File

@ -6,37 +6,7 @@ permalink: /config/plugins/baidu-tongji/
---
::: caution
主题计划在 未来的版本中 从内置插件中移除此插件。
此插件已从主题内置插件中移除。
如需使用相关功能,请使用 [@vuepress/plugin-baidu-analytics](https://ecosystem.vuejs.press/zh/plugins/analytics/baidu-analytics.html) 代替。
:::
## 概述
为站点添加 百度统计。该插件默认不启用。
关联插件: [@vuepress-plume/plugin-baidu-tongji](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/plugins/plugin-baidu-tongji)
## 配置
### key
- 类型:`string`
- 默认值:`''`
配置百度统计的key
### 启用
```ts{7-9}
import { plumeTheme } from 'vuepress-theme-plume'
import { defineUserConfig } from 'vuepress'
export default defineUserConfig({
theme: plumeTheme({
plugins: {
baiduTongji: {
key: '你的百度统计key'
}
}
}),
})
```

View File

@ -161,6 +161,26 @@ interface BlogOptions {
}
```
### cache
- 类型: `false | 'memory' | 'filesystem'`
- 默认值: `filesystem`
- 详情:
是否启用 编译缓存,或配置缓存方式
此配置项用于解决 VuePress 启动速度慢的问题,在首次启动服务时,对编译结果进行缓存,二次启动时
直接读取缓存,跳过编译,从而加快启动速度。
- `false`:禁用 缓存
- `'memory'`:使用内存缓存,此方式可获得更快的启动速度,但随着项目文件数量增加,内存占用会增加,
适合文章数量较少的项目使用
- `'filesystem'`:使用文件系统缓存,此方式可获得相对快且稳定的启动速度,更适合内容多的项目使用
::: warning
该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。
:::
### locales
- 类型: `Record<string, PlumeThemeLocaleConfig>`

View File

@ -446,11 +446,10 @@ interface PlotOptions {
## iconify 图标
在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 主题虽然提供了
[`<Iconify />`](/guide/features/component/#iconify) 组件来支持在 markdown 中使用图标,
但是它需要从远程加载图标,可能速度比较慢。
在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 主题一方面提供了
[`<Icon />`](../功能/组件.md#图标) 组件来支持在 markdown 中使用图标,
为此,主题提供了另一种可选的方式,以更简单的方式,在 Markdown 中使用图标,并且将 图标资源编译到
一方面,主题还提供了另一种可选的方式,以更简单的方式,在 Markdown 中使用图标,并且将 图标资源编译到
本地静态资源中。
### 配置
@ -537,6 +536,8 @@ github: :[tdesign:logo-github-filled]:
修改颜色::[tdesign:logo-github-filled /#f00]:
修改大小::[tdesign:logo-github-filled 36px]:
修改大小颜色::[tdesign:logo-github-filled 36px/#f00]:
彩色图标 :[skill-icons:vscode-dark 36px]:
```
输出:
@ -546,6 +547,8 @@ github: :[tdesign:logo-github-filled]:
修改大小::[tdesign:logo-github-filled 36px]:
修改大小颜色::[tdesign:logo-github-filled 36px/#f00]:
彩色图标 :[skill-icons:vscode-dark 36px]:
## can I use
此功能默认不启用,你可以在配置文件中启用它。

View File

@ -0,0 +1,85 @@
---
title: 图标
icon: raphael:smile2
author: pengzhanbo
createTime: 2024/07/22 10:45:47
permalink: /guide/features/icon/
---
## 概述
主题支持 [iconify](https://icon-sets.iconify.design/) 的所有图标,并提供了不同的方式来使用它们。
## 组件
通过 `<Icon />` 组件来使用图标。
你可以在 markdown 文件中使用该 组件。
### 属性
`<Icon />` 组件接受一个 `name` 属性,用于指定图标的名称。还支持传入 `color``size` 属性来设置图标的颜色和大小。
但对于 彩色图标,`color` 属性设置无效。
| 属性 | 类型 | 描述 |
| ----- | ------------------ | -------------------------------------------------------------------------- |
| name | `string` | 图标名称,在 [iconify](https://icon-sets.iconify.design/) 可获取对应的名称 |
| color | `string` | 图标颜色,仅纯色图标支持设置颜色 |
| size | `number \| string` | 设置图标大小,默认单位为 `px` ,可自定义单位 |
**示例:**
````md
- 纯色图标:<Icon name="octicon:smiley-16" />
- 定义纯色图标的颜色和大小:<Icon name="octicon:smiley-16" color="red" size="2em" />
- 彩色图标:<Icon name="noto:smiling-face-with-open-hands" />
- 定义彩色图标的大小:<Icon name="noto:smiling-face-with-open-hands" size="2em" />
````
- 纯色图标:<Icon name="octicon:smiley-16" />
- 定义纯色图标的颜色和大小:<Icon name="octicon:smiley-16" color="red" size="2em" />
- 彩色图标:<Icon name="noto:smiling-face-with-open-hands" />
- 定义彩色图标的大小:<Icon name="noto:smiling-face-with-open-hands" size="2em" />
### 加载图标
`<Icon />` 组件默认通过 远程请求 `CDN` 获取图标资源,但这可能受到网络环境的影响,出现加载失败
或者延迟显示的情况。
为了解决这一问题,主题建议 在你的站点项目中安装 `@iconify/json` 包。
主题会检查当前项目是否安装了 `@iconify/json`,如果安装了该包,则主题自动解析所使用到的图标,
并处理为本地图标资源,在构建时打包到 `dist` 目录中。
由于 `@iconify/json` 包比较大,需要手动进行安装:
::: code-tabs
@tab pnpm
```sh
pnpm add @iconify/json
```
@tab yarn
```sh
yarn add @iconify/json
```
@tab npm
```sh
npm install @iconify/json
```
:::
## markdown 语法糖
相关内容请查看 [iconify-图标 语法糖](../markdown/进阶.md#iconify-图标)
---
::: tip 说明
[navbar](../../config/主题配置.md#navbar) 配置和 [notes](../../config/主题配置.md#notes) 配置
中的 `icon` 选项,也支持传入 iconify 图标名,并且当安装了 `@iconify/json`,也会自动解析为本地图标资源。
:::

View File

@ -39,6 +39,7 @@ export default defineConfig(() => {
...DEFAULT_OPTIONS,
entry: ['./src/client/config.ts'],
outDir: './lib/client',
dts: false,
external: [...clientExternal, './components/Content.js'],
},
// client/index.js

View File

@ -25,6 +25,7 @@ export default defineConfig(() => {
...DEFAULT_OPTIONS,
entry: ['./src/client/config.ts'],
outDir: './lib/client',
dts: false,
external: clientExternal,
},
]

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,47 +0,0 @@
# `@vuepress-plume/plugin-iconify`
添加 `iconify` 图标库支持。并注入全局组件 `<Iconify>`
## Install
```sh
npm install @vuepress-plume/plugin-iconify
# or
pnpm add @vuepress-plume/plugin-iconify
# or
yarn add @vuepress-plume/plugin-iconify
```
## Usage
``` js
// .vuepress/config.[jt]s
import { iconifyPlugin } from '@vuepress-plume/plugin-iconify'
export default {
// ...
plugins: [
iconifyPlugin()
]
// ...
}
```
## Options
```ts
interface IconifyOptions {
/**
* 组件名, 默认 `Iconify`
*/
componentName?: string
color?: string
size?: string | number
}
```
## Component
```vue
<Iconify name="material-symbols:home" color="currentColor" size="1em" />
```

View File

@ -1,51 +0,0 @@
{
"name": "@vuepress-plume/plugin-iconify",
"type": "module",
"version": "1.0.0-rc.81",
"description": "The Plugin for VuePress 2 - iconify",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git",
"directory": "plugins/plugin-iconify"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "./lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run copy && pnpm run tsup",
"clean": "rimraf --glob ./lib",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"tsup": "tsup --config tsup.config.ts"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"vue": "^3.4.33"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"iconify",
"vuepress-plugin-iconify"
]
}

View File

@ -1,78 +0,0 @@
<script lang="ts" setup>
import { Icon as OfflineIcon } from '@iconify/vue/offline'
import { ClientOnly } from 'vuepress/client'
import type { IconifyRenderMode } from '@iconify/vue'
import type { StyleValue } from 'vue'
import { computed, toRefs } from 'vue'
import { useIconify } from '../composables/index.js'
const props = withDefaults(
defineProps<{
name?: string
size?: string | number
color?: string
mode?: IconifyRenderMode
style?: StyleValue
flip?: string
vFlip?: boolean
hFlip?: boolean
inline?: boolean
rotate?: number
}>(),
{
name: '',
size: '',
color: '',
},
)
const { name } = toRefs(props)
const { icon, loaded } = useIconify(name)
const size = computed(() => {
const size = props.size || __VP_ICONIFY_SIZE__
if (String(Number(size)) === size)
return `${size}px`
return size
})
const color = computed(() => props.color || __VP_ICONIFY_COLOR__)
const bind = computed<any>(() => ({
icon: icon.value,
mode: props.mode,
inline: props.inline,
rotate: props.rotate,
flip: props.flip,
vFlip: props.vFlip,
hFlip: props.hFlip,
color: props.color,
width: size.value,
height: size.value,
style: props.style,
}))
</script>
<script lang="ts">
declare const __VP_ICONIFY_SIZE__: string
declare const __VP_ICONIFY_COLOR__: string
</script>
<template>
<ClientOnly>
<span v-if="!loaded" class="vp-iconify" :style="{ color, width: size, height: size }" />
<OfflineIcon
v-else-if="icon"
class="vp-iconify"
v-bind="bind"
/>
</ClientOnly>
</template>
<style scoped>
.vp-iconify {
display: inline-block;
vertical-align: middle;
}
</style>

View File

@ -1,32 +0,0 @@
import type { IconifyIcon } from '@iconify/vue'
import { loadIcon } from '@iconify/vue'
import { ref, watch } from 'vue'
import type { Ref } from 'vue'
export function useIconify(name: Ref<string>) {
const icon = ref<IconifyIcon | null>(null)
const loaded = ref(false)
async function loadIconComponent() {
if (icon.value)
return
if (!__VUEPRESS_SSR__) {
try {
loaded.value = false
const cached = await loadIcon(name.value)
icon.value = cached
}
finally {
loaded.value = true
}
}
else {
loaded.value = true
}
}
watch(() => name.value, loadIconComponent, { immediate: true })
return { icon, loaded }
}

View File

@ -1 +0,0 @@
export * from './iconify.js'

View File

@ -1,11 +0,0 @@
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import Iconify from './components/Iconify.vue'
declare const __VP_ICONIFY_NAME__: string
export default defineClientConfig({
enhance({ app }) {
const name = __VP_ICONIFY_NAME__ || 'Iconify'
app.component(name, Iconify)
},
}) as ClientConfig

View File

@ -1,6 +0,0 @@
import { iconifyPlugin } from './plugin.js'
export * from './plugin.js'
/** @deprecated 请使用 具名导出 替代 默认导出 */
export default iconifyPlugin

View File

@ -1,24 +0,0 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
export interface IconifyPluginOptions {
componentName?: string
color?: string
size?: string | number
}
export function iconifyPlugin({
componentName = 'Iconify',
size = '1em',
color = 'currentColor',
}: IconifyPluginOptions = {}): Plugin {
return {
name: '@vuepress-plume/plugin-iconify',
define: {
__VP_ICONIFY_NAME__: componentName,
__VP_ICONIFY_SIZE__: size,
__VP_ICONIFY_COLOR__: color,
},
clientConfigFile: path.resolve(getDirname(import.meta.url), '../client/config.js'),
}
}

View File

@ -1,6 +0,0 @@
declare module '*.vue' {
const comp: any
export default comp
}
declare const __VUEPRESS_SSR__: boolean

View File

@ -1,38 +0,0 @@
import { type Options, defineConfig } from 'tsup'
const clientExternal: (string | RegExp)[] = [
/.*\.vue$/,
/.*\.css$/,
]
export default defineConfig(() => {
const DEFAULT_OPTIONS: Options = {
dts: true,
sourcemap: false,
splitting: false,
format: 'esm',
}
return [
// node
{
...DEFAULT_OPTIONS,
entry: ['./src/node/index.ts'],
outDir: './lib/node',
target: 'node18',
},
// client/composables/index.js
{
...DEFAULT_OPTIONS,
entry: ['./src/client/composables/index.ts'],
outDir: './lib/client/composables',
external: clientExternal,
},
// client/config.js
{
...DEFAULT_OPTIONS,
entry: ['./src/client/config.ts'],
outDir: './lib/client',
external: clientExternal,
},
]
})

View File

@ -106,12 +106,13 @@ onUnmounted(() => {
z-index: 1;
box-sizing: border-box;
display: block;
padding: 1.3rem 1.5rem;
padding: 20px 24px;
overflow-x: auto;
font-family: var(--vp-font-family-mono);
font-size: var(--vp-code-font-size);
-webkit-hyphens: none;
hyphens: none;
line-height: var(--vp-code-line-height);
color: transparent;
text-align: left;
word-break: normal;
@ -139,7 +140,7 @@ onUnmounted(() => {
}
:deep(div[class*="language-"].line-numbers-mode) + .code-repl-input {
padding-left: 1rem;
margin-left: 2rem;
padding-left: 24px;
margin-left: 32px;
}
</style>

View File

@ -10,6 +10,7 @@ import { parseRect } from '../../utils/parseRect.js'
export interface IconCacheItem {
className: string
background: boolean
content: string
}
@ -18,6 +19,8 @@ const iconDataCache = new Map<string, any>()
const URL_CONTENT_RE = /(url\([\s\S]+?\))/
const CSS_PATH = 'internal/md-power/icons.css'
let locate: ((name: string) => any) | undefined
function resolveOption(opt?: boolean | IconsOptions): Required<IconsOptions> {
const options = typeof opt === 'object' ? opt : {}
options.prefix ??= 'vp-mdi'
@ -65,19 +68,18 @@ export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) {
if (!isInstalled)
return
if (cache.has(iconName))
return cache.get(iconName)!.className
if (cache.has(iconName)) {
const item = cache.get(iconName)!
return `${item.className}${item.background ? ' bg' : ''}`
}
const item: IconCacheItem = {
className: `${prefix}-${nanoid()}`,
content: '',
...genIcon(iconName),
}
cache.set(iconName, item)
genIconContent(iconName, (content) => {
item.content = content
writeCss()
})
return item.className
writeCss()
return `${item.className}${item.background ? ' bg' : ''}`
}
async function initIcon() {
@ -89,6 +91,11 @@ export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) {
return
}
if (!locate) {
const mod = await interopDefault(import('@iconify/json'))
locate = mod.locate
}
return await writeCss()
}
@ -97,12 +104,13 @@ export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) {
function getDefaultContent(options: Required<IconsOptions>) {
const { prefix, size, color } = options
return `[class^="${prefix}-"],
[class*=" ${prefix}-"] {
return `[class^="${prefix}-"] {
display: inline-block;
width: ${size};
height: ${size};
vertical-align: middle;
}
[class^="${prefix}-"]:not(.bg) {
color: inherit;
background-color: ${color};
-webkit-mask: var(--svg) no-repeat;
@ -110,24 +118,29 @@ function getDefaultContent(options: Required<IconsOptions>) {
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
[class^="${prefix}-"].bg {
background-color: transparent;
background-image: var(--svg);
background-repeat: no-repeat;
background-size: 100% 100%;
}
`
}
let locate: ((name: string) => any) | undefined
async function genIconContent(iconName: string, cb: (content: string) => void) {
function genIcon(iconName: string): {
content: string
background: boolean
} {
if (!locate) {
const mod = await interopDefault(import('@iconify/json'))
locate = mod.locate
return { content: '', background: false }
}
const [collect, name] = iconName.split(':')
let iconJson: any = iconDataCache.get(collect)
if (!iconJson) {
const filename = locate(collect)
try {
iconJson = JSON.parse(await fs.readFile(filename, 'utf-8'))
iconJson = JSON.parse(fs.readFileSync(filename, 'utf-8'))
iconDataCache.set(collect, iconJson)
}
catch {
@ -135,14 +148,19 @@ async function genIconContent(iconName: string, cb: (content: string) => void) {
}
}
const data = getIconData(iconJson, name)
if (!data)
return logger.error(`[plugin-md-power] Can not read icon in ${collect}, ${name} is missing!`)
if (!data) {
logger.error(`[plugin-md-power] Can not read icon in ${collect}, ${name} is missing!`)
return { content: '', background: false }
}
const content = getIconContentCSS(data, {
height: data.height || 24,
})
const match = content.match(URL_CONTENT_RE)
return cb(match ? match[1] : '')
return {
content: match ? match[1] : '',
background: !data.body.includes('currentColor'),
}
}
function existsSync(fp: string) {

View File

@ -53,6 +53,7 @@ export default defineConfig(() => {
entry: ['./src/client/config.ts'],
outDir: './lib/client',
external: clientExternal,
dts: false,
},
// client/index.js
{

View File

@ -19,11 +19,10 @@ import {
transformerRenderWhitespace,
} from '@shikijs/transformers'
import type { HighlighterOptions, ThemeOptions } from './types.js'
import { LRUCache, attrsToLines, resolveLanguage } from './utils/index.js'
import { attrsToLines, resolveLanguage } from './utils/index.js'
import { defaultHoverInfoProcessor, transformerTwoslash } from './twoslash/rendererTransformer.js'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
const cache = new LRUCache<string, string>(64)
const vueRE = /-vue$/
const mustacheRE = /\{\{.*?\}\}/g
@ -32,7 +31,6 @@ const decorationsRE = /^\/\/ @decorations:(.*)\n/
export async function highlight(
theme: ThemeOptions,
options: HighlighterOptions,
isDev: boolean,
): Promise<(str: string, lang: string, attrs: string) => string> {
const {
defaultHighlightLang: defaultLang = '',
@ -95,14 +93,6 @@ export async function highlight(
let lang = resolveLanguage(language) || defaultLang
const vPre = vueRE.test(lang) ? '' : 'v-pre'
const key = str + language + attrs
if (isDev) {
const rendered = cache.get(key)
if (rendered)
return rendered
}
if (lang) {
const langLoaded = loadedLanguages.includes(lang as any)
if (!langLoaded && !isPlainLang(lang) && !isSpecialLang(lang)) {
@ -183,9 +173,6 @@ export async function highlight(
const rendered = restoreMustache(highlighted)
if (isDev)
cache.set(key, rendered)
return rendered
}
catch (e) {

View File

@ -51,7 +51,7 @@ export function shikiPlugin({
extendsMarkdown: async (md, app) => {
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
md.options.highlight = await highlight(theme, options, app.env.isDev)
md.options.highlight = await highlight(theme, options)
md.use(highlightLinesPlugin)
md.use<PreWrapperOptions>(preWrapperPlugin, {

56
pnpm-lock.yaml generated
View File

@ -124,18 +124,6 @@ importers:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))
plugins/plugin-iconify:
dependencies:
'@iconify/vue':
specifier: ^4.1.2
version: 4.1.2(vue@3.4.33(typescript@5.5.3))
vue:
specifier: ^3.4.33
version: 3.4.33(typescript@5.5.3)
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))
plugins/plugin-md-power:
dependencies:
'@iconify/utils':
@ -259,6 +247,12 @@ importers:
theme:
dependencies:
'@iconify/utils':
specifier: ^2.1.25
version: 2.1.25
'@iconify/vue':
specifier: ^4.1.2
version: 4.1.2(vue@3.4.33(typescript@5.5.3))
'@pengzhanbo/utils':
specifier: ^1.1.2
version: 1.1.2
@ -268,9 +262,6 @@ importers:
'@vuepress-plume/plugin-fonts':
specifier: workspace:*
version: link:../plugins/plugin-fonts
'@vuepress-plume/plugin-iconify':
specifier: workspace:*
version: link:../plugins/plugin-iconify
'@vuepress-plume/plugin-search':
specifier: workspace:*
version: link:../plugins/plugin-search
@ -283,6 +274,9 @@ importers:
'@vuepress/plugin-active-header-links':
specifier: 2.0.0-rc.39
version: 2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))
'@vuepress/plugin-cache':
specifier: 2.0.0-rc.39
version: 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))
'@vuepress/plugin-comment':
specifier: 2.0.0-rc.39
version: 2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))
@ -343,6 +337,9 @@ importers:
katex:
specifier: ^0.16.11
version: 0.16.11
local-pkg:
specifier: ^0.5.0
version: 0.5.0
nanoid:
specifier: ^5.0.7
version: 5.0.7
@ -361,6 +358,10 @@ importers:
vuepress-plugin-md-power:
specifier: workspace:*
version: link:../plugins/plugin-md-power
devDependencies:
'@iconify/json':
specifier: ^2.2.229
version: 2.2.229
packages:
@ -1934,6 +1935,11 @@ packages:
peerDependencies:
vuepress: 2.0.0-rc.14
'@vuepress/plugin-cache@2.0.0-rc.39':
resolution: {integrity: sha512-PVsC797lGMuu8L7jtW9vv2hYM+d5qq5fbWwBJuSyRXEdpcwryhAjGWnz9F19dYe5KWLYG6EbCoANTQObmiyBag==}
peerDependencies:
vuepress: 2.0.0-rc.14
'@vuepress/plugin-comment@2.0.0-rc.39':
resolution: {integrity: sha512-/oCS+0wH/MtE4c1HUKlqH/tj70oXSz/tfR1hsHj8F8wiZ+IVJxexvtzMKk0vdRmYnH4nqeZh6dg5ggSJjrLEZQ==}
peerDependencies:
@ -3981,14 +3987,13 @@ packages:
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
engines: {node: '>=0.10.0'}
lru-cache@10.0.1:
resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==}
engines: {node: 14 || >=16.14}
lru-cache@10.0.2:
resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==}
engines: {node: 14 || >=16.14}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.0.0:
resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==}
engines: {node: 20 || >=22}
@ -7269,6 +7274,11 @@ snapshots:
- '@vue/composition-api'
- typescript
'@vuepress/plugin-cache@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))':
dependencies:
lru-cache: 10.4.3
vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))
'@vuepress/plugin-comment@2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))':
dependencies:
'@vuepress/helper': 2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))
@ -9534,11 +9544,11 @@ snapshots:
longest@2.0.1: {}
lru-cache@10.0.1: {}
lru-cache@10.0.2:
dependencies:
semver: 7.6.0
semver: 7.6.3
lru-cache@10.4.3: {}
lru-cache@11.0.0: {}
@ -10331,7 +10341,7 @@ snapshots:
path-scurry@1.10.1:
dependencies:
lru-cache: 10.0.1
lru-cache: 10.0.2
minipass: 5.0.0
path-scurry@2.0.0:

View File

@ -59,17 +59,25 @@
"tsup:watch": "tsup --config tsup.config.ts --watch"
},
"peerDependencies": {
"@iconify/json": "^2",
"vuepress": "2.0.0-rc.14"
},
"peerDependenciesMeta": {
"@iconify/json": {
"optional": true
}
},
"dependencies": {
"@iconify/utils": "^2.1.25",
"@iconify/vue": "^4.1.2",
"@pengzhanbo/utils": "^1.1.2",
"@vuepress-plume/plugin-content-update": "workspace:*",
"@vuepress-plume/plugin-fonts": "workspace:*",
"@vuepress-plume/plugin-iconify": "workspace:*",
"@vuepress-plume/plugin-search": "workspace:*",
"@vuepress-plume/plugin-shikiji": "workspace:*",
"@vuepress/helper": "2.0.0-rc.39",
"@vuepress/plugin-active-header-links": "2.0.0-rc.39",
"@vuepress/plugin-cache": "2.0.0-rc.39",
"@vuepress/plugin-comment": "2.0.0-rc.39",
"@vuepress/plugin-docsearch": "2.0.0-rc.39",
"@vuepress/plugin-git": "2.0.0-rc.38",
@ -90,10 +98,14 @@
"gray-matter": "^4.0.3",
"json2yaml": "^1.1.0",
"katex": "^0.16.11",
"local-pkg": "^0.5.0",
"nanoid": "^5.0.7",
"vue": "^3.4.33",
"vue-router": "^4.4.0",
"vuepress-plugin-md-enhance": "2.0.0-rc.52",
"vuepress-plugin-md-power": "workspace:*"
},
"devDependencies": {
"@iconify/json": "^2.2.229"
}
}

View File

@ -286,4 +286,10 @@ watchPostEffect(() => {
background-color: var(--vp-c-gutter);
}
}
@media print {
.vp-navbar .hamburger {
display: none;
}
}
</style>

View File

@ -50,7 +50,7 @@ const routeLocale = useRouteLocale()
}
:deep(.logo) {
height: var(--vp-nav-logo-height, 24px);
height: min(var(--vp-nav-logo-height, 24px), 48px);
margin-right: 8px;
}
</style>

View File

@ -2,35 +2,83 @@
import { computed } from 'vue'
import { isLinkHttp } from 'vuepress/shared'
import { withBase } from 'vuepress/client'
import VPIconify from '@theme/VPIconify.vue'
import { useIconsData } from '../composables/index.js'
const props = defineProps<{
name: string | { svg: string }
size?: string | number
color?: string
}>()
const isLink = computed(() =>
typeof props.name === 'string' && (isLinkHttp(props.name) || props.name[0] === '/'),
)
const isSvg = computed(() => typeof props.name === 'object' && !!props.name.svg)
const iconsData = useIconsData()
const type = computed(() => {
if (typeof props.name === 'string' && (isLinkHttp(props.name) || props.name[0] === '/')) {
return 'link'
}
if (typeof props.name === 'object' && !!props.name.svg) {
return 'svg'
}
if (typeof props.name === 'string' && iconsData.value[props.name]) {
return 'local'
}
return 'remote'
})
const svg = computed(() => {
if (isSvg.value)
if (type.value === 'svg')
return (props.name as { svg: string }).svg
return ''
})
const link = computed(() => {
if (isLink.value) {
if (type.value === 'link') {
const link = props.name as string
return isLinkHttp(link) ? link : withBase(link)
}
return ''
})
const className = computed(() => {
if (type.value === 'local') {
const name = props.name as string
return iconsData.value[name] || ''
}
return ''
})
const size = computed(() => {
const size = props.size
if (!size)
return undefined
if (String(Number(size)) === size)
return `${size}px`
return size
})
const style = computed(() => ({
'background-color': props.color,
'width': size.value,
'height': size.value,
}))
</script>
<template>
<img v-if="isLink" class="vp__img" :src="link" alt="">
<span v-else-if="isSvg" class="vp-iconify" v-html="svg" />
<Icon v-else :name="name" />
<img v-if="type === 'link'" class="vp__img" :src="link" alt="" :style="{ height: size }">
<span
v-else-if="type === 'svg'"
class="vp-icon"
:style="style"
v-html="svg"
/>
<span
v-else-if="type === 'local' && className"
class="vp-icon" :class="[className]"
:style="style"
/>
<VPIconify v-else :name="(name as string)" :size="size" :color="color" />
</template>
<style scoped>

View File

@ -0,0 +1,68 @@
<script lang="ts" setup>
import type { IconifyIcon } from '@iconify/vue/offline'
import { Icon as OfflineIcon } from '@iconify/vue/offline'
import { loadIcon } from '@iconify/vue'
import { computed, ref, watch } from 'vue'
const props = withDefaults(
defineProps<{
name?: string
size?: string | number
color?: string
}>(),
{
name: '',
size: '',
color: '',
},
)
const icon = ref<IconifyIcon | null>(null)
const loaded = ref(false)
async function loadIconComponent() {
if (icon.value)
return
if (!__VUEPRESS_SSR__) {
try {
loaded.value = false
icon.value = await loadIcon(props.name)
}
finally {
loaded.value = true
}
}
else {
loaded.value = true
}
}
watch(() => props.name, loadIconComponent, { immediate: true })
const size = computed(() => {
const size = props.size || '1em'
if (String(Number(size)) === size)
return `${size}px`
return size
})
const color = computed(() => props.color || 'currentColor')
const bind = computed<any>(() => ({
icon: icon.value,
color: props.color,
height: size.value,
}))
</script>
<template>
<ClientOnly>
<span v-if="!loaded" class="vp-icon" :style="{ color, width: size, height: size }" />
<OfflineIcon
v-else-if="icon"
class="vp-iconify"
v-bind="bind"
/>
</ClientOnly>
</template>

View File

@ -44,6 +44,7 @@ onBeforeUnmount(() => {
.group + .group {
padding-top: 10px;
border-top: 1px solid var(--vp-c-divider);
transition: border var(--t-color);
}
@media (min-width: 960px) {

View File

@ -220,7 +220,7 @@ function onCaretClick() {
transition: color var(--t-color);
}
.item :deep(.vp-iconify) {
.item :deep(.vp-icon) {
margin: 0 0.25rem 0 0;
font-size: 0.9em;
color: var(--vp-c-text-2);
@ -240,21 +240,21 @@ function onCaretClick() {
color: var(--vp-c-text-1);
}
.vp-sidebar-item.level-0.is-active > .item > :deep(.vp-iconify),
.vp-sidebar-item.level-1.is-active > .item > :deep(.vp-iconify),
.vp-sidebar-item.level-2.is-active > .item > :deep(.vp-iconify),
.vp-sidebar-item.level-3.is-active > .item > :deep(.vp-iconify),
.vp-sidebar-item.level-4.is-active > .item > :deep(.vp-iconify),
.vp-sidebar-item.level-5.is-active > .item > :deep(.vp-iconify) {
.vp-sidebar-item.level-0.is-active > .item > :deep(.vp-icon),
.vp-sidebar-item.level-1.is-active > .item > :deep(.vp-icon),
.vp-sidebar-item.level-2.is-active > .item > :deep(.vp-icon),
.vp-sidebar-item.level-3.is-active > .item > :deep(.vp-icon),
.vp-sidebar-item.level-4.is-active > .item > :deep(.vp-icon),
.vp-sidebar-item.level-5.is-active > .item > :deep(.vp-icon) {
color: var(--vp-c-brand-1);
}
.vp-sidebar-item.level-0.is-link > .item:hover :deep(.vp-iconify),
.vp-sidebar-item.level-1.is-link > .item:hover :deep(.vp-iconify),
.vp-sidebar-item.level-2.is-link > .item:hover :deep(.vp-iconify),
.vp-sidebar-item.level-3.is-link > .item:hover :deep(.vp-iconify),
.vp-sidebar-item.level-4.is-link > .item:hover :deep(.vp-iconify),
.vp-sidebar-item.level-5.is-link > .item:hover :deep(.vp-iconify) {
.vp-sidebar-item.level-0.is-link > .item:hover :deep(.vp-icon),
.vp-sidebar-item.level-1.is-link > .item:hover :deep(.vp-icon),
.vp-sidebar-item.level-2.is-link > .item:hover :deep(.vp-icon),
.vp-sidebar-item.level-3.is-link > .item:hover :deep(.vp-icon),
.vp-sidebar-item.level-4.is-link > .item:hover :deep(.vp-icon),
.vp-sidebar-item.level-5.is-link > .item:hover :deep(.vp-icon) {
color: var(--vp-c-brand-1);
}

View File

@ -49,7 +49,7 @@ const icon = computed<string | { svg: string } | undefined>(() => {
box-shadow: var(--vp-shadow-2);
}
.vp-card-wrapper :deep(.vp-iconify),
.vp-card-wrapper :deep(.vp-icon),
.vp-card-wrapper :deep(.vp__img) {
margin: 0;
}

View File

@ -75,7 +75,7 @@ defineProps<{
content: "";
}
.vp-link-card .link :deep(.vp-iconify),
.vp-link-card .link :deep(.vp-icon),
.vp-link-card .link :deep(.vp__img) {
margin: 0;
}

View File

@ -0,0 +1,16 @@
import { icons } from '@internal/iconify'
import { ref } from 'vue'
import type { Ref } from 'vue'
type IconsData = Record<string, string>
type IconsDataRef = Ref<IconsData>
const iconsData: IconsDataRef = ref(icons)
export const useIconsData = (): IconsDataRef => iconsData
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateIcons = (data: IconsData) => {
iconsData.value = data
}
}

View File

@ -1,6 +1,7 @@
export * from './theme-data.js'
export * from './dark-mode.js'
export * from './data.js'
export * from './icons.js'
export * from './scroll-promise.js'
export * from './scroll-behavior.js'

View File

@ -5,6 +5,7 @@ import VPCard from '@theme/global/VPCard.vue'
import VPLinkCard from '@theme/global/VPLinkCard.vue'
import VPBadge from '@theme/global/VPBadge.vue'
import VPCardGrid from '@theme/global/VPCardGrid.vue'
import VPIcon from '@theme/VPIcon.vue'
export function globalComponents(app: App) {
app.component('Badge', VPBadge)
@ -36,13 +37,8 @@ export function globalComponents(app: App) {
return null
})
app.component('Icon', (props) => {
const Iconify = app.component('Iconify')
if (Iconify)
return h(Iconify, props)
return null
})
app.component('Icon', VPIcon)
app.component('VPIcon', VPIcon)
/** @deprecated */
app.component('HomeBox', VPHomeBox)

View File

@ -58,3 +58,10 @@ declare module '@internal/encrypt' {
encrypt,
}
}
declare module '@internal/iconify' {
const icons: Record<string, string>
export {
icons,
}
}

View File

@ -12,7 +12,7 @@ html:not(.dark) .vp-code span {
margin: 16px -24px;
overflow-x: auto;
background-color: var(--vp-code-block-bg);
transition: background-color 0.5s;
transition: background-color var(--t-color);
}
@media (min-width: 640px) {
@ -135,6 +135,16 @@ html:not(.dark) .vp-code span {
counter-increment: line-number;
}
@media print {
.vp-doc div[class*="language-"].line-numbers-mode {
padding-left: 0;
}
.vp-doc div[class*="language-"].line-numbers-mode .line-numbers {
display: none;
}
}
@media (max-width: 639px) {
.vp-doc li div[class*="language-"] {
border-radius: 8px 0 0 8px;

View File

@ -1,14 +1,18 @@
[class^="vpi-"],
[class*=" vpi-"],
.vp-icon {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
}
[class^="vpi-"].bg,
[class*=" vpi-"].bg,
.vp-icon.bg {
background-color: transparent;
background-image: var(--icon);
background-repeat: no-repeat;
background-size: 100% 100%;
}

View File

@ -8,8 +8,8 @@
white-space: nowrap;
}
.vp-iconify {
margin: 0.3em;
.vp-icon {
margin: 0 0.3em;
}
.smooth {

View File

@ -39,12 +39,10 @@ export function resolveOptions(
}
const notesList = resolveNotesOptions(localeOptions)
const localesNotesDirs = notesList
.flatMap(({ notes, dir }) => {
dir = removeLeadingSlash(dir || '')
return notes.map(note => normalizePath(`${dir}/${note.dir || ''}/`))
})
.filter(Boolean)
const localesNotesDirs = uniq(notesList
.flatMap(({ notes, dir }) =>
notes.map(note => removeLeadingSlash(normalizePath(`${dir}/${note.dir || ''}/`))),
))
const baseFrontmatter: AutoFrontmatterObject = {}

View File

@ -6,6 +6,7 @@ export function resolveThemeOptions({
plugins,
hostname,
configFile,
cache,
...localeOptions
}: PlumeThemeOptions) {
const pluginOptions = plugins ?? themePlugins ?? {}
@ -17,6 +18,7 @@ export function resolveThemeOptions({
}
return {
cache,
configFile,
pluginOptions,
hostname,

View File

@ -1,134 +0,0 @@
/**
* 使 shiki + twoslash markdown
* markdown render
* markdown render content hash
*
*
* vuepress/ecosystem
* vuepress/core
*
*
* 使 13s 1.2s
* vuepress shiki 0.5s
*/
import process from 'node:process'
import { fs, path } from 'vuepress/utils'
import type { App } from 'vuepress'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
import { hash } from './utils/index.js'
export interface CacheData {
content: string
env: MarkdownEnv
}
// { [filepath]: CacheDta }
export type Cache = Record<string, CacheData>
// { [filepath]: hash }
export type Metadata = Record<string, string>
const CACHE_DIR = 'markdown/rendered'
const META_FILE = '_metadata.json'
export async function extendsMarkdown(md: Markdown, app: App): Promise<void> {
if (app.env.isBuild && !fs.existsSync(app.dir.cache(CACHE_DIR))) {
return
}
const basename = app.dir.cache(CACHE_DIR)
await fs.ensureDir(basename)
const speed = checkIOSpeed(basename)
const metaFilepath = `${basename}/${META_FILE}`
const metadata = (await readFile<Metadata>(metaFilepath)) || {}
let timer: ReturnType<typeof setTimeout> | null = null
const update = (filepath: string, data: CacheData): void => {
writeFile(`${basename}/${filepath}`, data)
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(async () => writeFile(metaFilepath, metadata), 200)
}
const rawRender = md.render
md.render = (input, env: MarkdownEnv) => {
const filepath = env.filePathRelative
if (!filepath) {
return rawRender(input, env)
}
const key = hash(input)
const filename = normalizeFilename(filepath)
if (metadata[filepath] === key) {
const cached = readFileSync<CacheData>(`${basename}/${filename}`)
if (cached) {
Object.assign(env, cached.env)
return cached.content
}
else {
metadata[filepath] = ''
}
}
const start = performance.now()
const content = rawRender(input, env)
/**
* High-frequency I/O is also a time-consuming operation,
* therefore, for render operations with low overhead, caching is not performed.
*/
if (performance.now() - start > speed) {
metadata[filepath] = key
update(filename, { content, env })
}
return content
}
}
function normalizeFilename(filename: string): string {
return hash(filename).slice(0, 10)
}
async function readFile<T = any>(filepath: string): Promise<T | null> {
try {
const content = await fs.readFile(filepath, 'utf-8')
return JSON.parse(content) as T
}
catch {
return null
}
}
function readFileSync<T = any>(filepath: string): T | null {
try {
const content = fs.readFileSync(filepath, 'utf-8')
return JSON.parse(content) as T
}
catch {
return null
}
}
async function writeFile<T = any>(filepath: string, data: T): Promise<void> {
return await fs.writeFile(filepath, JSON.stringify(data), 'utf-8')
}
export function checkIOSpeed(cwd = process.cwd()): number {
try {
const tmp = path.join(cwd, 'tmp')
fs.writeFileSync(tmp, '{}', 'utf-8')
const start = performance.now()
readFileSync(tmp)
const end = performance.now()
fs.unlinkSync(tmp)
return end - start
}
catch {
return 0.15
}
}

View File

@ -1,10 +1,10 @@
import type { App, PluginConfig } from 'vuepress/core'
import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links'
import { cachePlugin } from '@vuepress/plugin-cache'
import { docsearchPlugin } from '@vuepress/plugin-docsearch'
import { gitPlugin } from '@vuepress/plugin-git'
import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe'
import { nprogressPlugin } from '@vuepress/plugin-nprogress'
import { iconifyPlugin } from '@vuepress-plume/plugin-iconify'
import { shikiPlugin } from '@vuepress-plume/plugin-shikiji'
import { commentPlugin } from '@vuepress/plugin-comment'
import { type MarkdownEnhancePluginOptions, mdEnhancePlugin } from 'vuepress-plugin-md-enhance'
@ -27,18 +27,19 @@ export interface SetupPluginOptions {
app: App
pluginOptions: PlumeThemePluginOptions
hostname?: string
cache?: false | 'memory' | 'filesystem'
}
export function getPlugins({
app,
pluginOptions,
hostname,
cache,
}: SetupPluginOptions): PluginConfig {
const isProd = !app.env.isDev
const plugins: PluginConfig = [
iconifyPlugin(),
fontsPlugin(),
contentUpdatePlugin(),
activeHeaderLinksPlugin({
@ -156,5 +157,9 @@ export function getPlugins({
plugins.push(seoPlugin({ hostname }))
}
if (cache !== false) {
plugins.push(cachePlugin({ type: cache || 'filesystem' }))
}
return plugins
}

View File

@ -5,6 +5,7 @@ import { prepareArticleTagColors } from './prepareArticleTagColor.js'
import { preparedBlogData } from './prepareBlogData.js'
import { prepareEncrypt } from './prepareEncrypt.js'
import { prepareSidebar } from './prepareSidebar.js'
import { prepareIcons } from './prepareIcons.js'
export async function prepareData(
app: App,
@ -15,6 +16,7 @@ export async function prepareData(
preparedBlogData(app, localeOptions, encrypt),
prepareSidebar(app, localeOptions),
prepareEncrypt(app, encrypt),
prepareIcons(app, localeOptions),
])
}

View File

@ -0,0 +1,196 @@
import type { App, Page } from 'vuepress'
import { isArray, isString, uniq } from '@pengzhanbo/utils'
import { fs } from 'vuepress/utils'
import { entries, isLinkAbsolute, isLinkHttp, isPlainObject } from '@vuepress/helper'
import { isPackageExists } from 'local-pkg'
import { getIconContentCSS, getIconData } from '@iconify/utils'
import type { NavItem, PlumeThemeLocaleOptions, Sidebar } from '../../shared/index.js'
import { interopDefault, logger, nanoid, resolveContent, writeTemp } from '../utils/index.js'
interface IconData {
className: string
background?: boolean
content: string
}
type CollectMap = Record<string, string[]>
type IconDataMap = Record<string, IconData>
const ICON_REGEXP = /<(?:VP)?Icon(?:ify)?([^>]*)>/g
const ICON_NAME_REGEXP = /name="([^"]+)"/
const URL_CONTENT_REGEXP = /(url\([\s\S]+\))/
const JS_FILENAME = 'internal/iconify.js'
const CSS_FILENAME = 'internal/iconify.css'
const isInstalled = isPackageExists('@iconify/json')
let locate!: ((name: string) => any)
// { iconName: { className, content } }
const cache: IconDataMap = {}
export async function prepareIcons(app: App, localeOptions: PlumeThemeLocaleOptions) {
if (!isInstalled) {
await writeTemp(app, JS_FILENAME, resolveContent(app, { name: 'icons', content: '{}' }))
return
}
const iconList: string[] = []
app.pages.forEach(page => iconList.push(...getIconsWithPage(page)))
iconList.push(...getIconWithThemeConfig(localeOptions))
const collectMap: CollectMap = {}
uniq(iconList).filter(icon => !cache[icon]).forEach((iconName) => {
const [collect, name] = iconName.split(':')
if (!collectMap[collect])
collectMap[collect] = []
collectMap[collect].push(name)
})
if (!locate) {
const mod = await interopDefault(import('@iconify/json'))
locate = mod.locate
}
const unknownList = (await Promise.all(
entries(collectMap).map(([collect, names]) => resolveCollect(collect, names)),
)).flat()
if (unknownList.length) {
logger.warn(`[iconify] Unknown icons: ${unknownList.join(', ')}`)
}
let cssCode = ''
const map: Record<string, string> = {}
for (const [iconName, { className, content, background }] of entries(cache)) {
map[iconName] = `${className}${background ? ' bg' : ''}`
cssCode += `.${className} {\n --icon: ${content};\n}\n`
}
await Promise.all([
writeTemp(app, CSS_FILENAME, cssCode),
writeTemp(app, JS_FILENAME, resolveContent(app, {
name: 'icons',
content: map,
before: `import './iconify.css'`,
})),
])
}
function getIconsWithPage(page: Page): string[] {
const list = page.contentRendered
.match(ICON_REGEXP)?.map(match => match.match(ICON_NAME_REGEXP)?.[1])
.filter(Boolean) as string[] || []
if (page.frontmatter.icon && isString(page.frontmatter.icon)) {
list.push(page.frontmatter.icon)
}
return list
}
function getIconWithThemeConfig(localeOptions: PlumeThemeLocaleOptions): string[] {
const list: string[] = []
// navbar notes sidebar
const locales = localeOptions.locales || {}
entries(locales).forEach(([, { navbar, sidebar, notes }]) => {
if (navbar) {
list.push(...getIconWithNavbar(navbar))
}
const sidebarList: Sidebar[] = Object.values(sidebar || {}) as Sidebar[]
if (notes) {
notes.notes.forEach((note) => {
if (note.sidebar)
sidebarList.push(note.sidebar)
})
}
sidebarList.forEach(sidebar => list.push(...getIconWithSidebar(sidebar)))
})
return list
}
function getIconWithNavbar(navbar: NavItem[]): string[] {
const list: string[] = []
navbar.forEach((item) => {
if (typeof item !== 'string') {
if (typeof item.icon === 'string' && !isLinkHttp(item.icon) && !isLinkAbsolute(item.icon))
list.push(item.icon)
if (item.items?.length)
list.push(...getIconWithNavbar(item.items))
}
})
return list
}
function getIconWithSidebar(sidebar: Sidebar): string[] {
const list: string[] = []
if (isArray(sidebar)) {
sidebar.forEach((item) => {
if (typeof item !== 'string') {
if (typeof item.icon === 'string' && !isLinkHttp(item.icon) && !isLinkAbsolute(item.icon))
list.push(item.icon)
if (item.items?.length)
list.push(...getIconWithSidebar(item.items))
}
})
}
else if (isPlainObject(sidebar)) {
entries(sidebar).forEach(([, item]) => {
if (typeof item !== 'string') {
if (isArray(item)) {
list.push(...getIconWithSidebar(item))
}
else if (item.items?.length) {
list.push(...getIconWithSidebar(item.items))
}
}
})
}
return list
}
async function resolveCollect(collect: string, names: string[]) {
const filepath = locate(collect)
const config = await readJSON(filepath)
if (!config) {
logger.warn(`[iconify] Can not find icon collect: ${collect}!`)
return []
}
const unknownList: string[] = []
for (const name of names) {
const data = getIconData(config, name)
const icon = `${collect}:${name}`
if (!data) {
unknownList.push(icon)
}
else if (!cache[icon]) {
const content = getIconContentCSS(data, {
height: data.height || 24,
})
const matched = content.match(URL_CONTENT_REGEXP)?.[1] ?? ''
/**
* @see - https://iconify.design/docs/libraries/utils/get-icon-css.html#options
*/
const background = !data.body.includes('currentColor')
cache[icon] = {
className: `vpi-${nanoid()}`,
background,
content: matched,
}
}
}
return unknownList
}
async function readJSON(filepath: string) {
try {
return await fs.readJSON(filepath, 'utf-8')
}
catch {
return null
}
}

View File

@ -26,7 +26,6 @@ import {
} from './autoFrontmatter/index.js'
import { prepareData, watchPrepare } from './prepare/index.js'
import { prepareThemeData } from './prepare/prepareThemeData.js'
import { extendsMarkdown } from './extendsMarkdown.js'
export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
const {
@ -34,6 +33,7 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
pluginOptions,
hostname,
configFile,
cache,
} = resolveThemeOptions(options)
return (app) => {
@ -65,7 +65,7 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
alias: resolveAlias(),
plugins: getPlugins({ app, pluginOptions, hostname }),
plugins: getPlugins({ app, pluginOptions, hostname, cache }),
onInitialized: async (app) => {
const { localeOptions } = await waitForConfigLoaded()
@ -90,14 +90,14 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
},
extendsPage: async (page) => {
const { localeOptions } = await waitForConfigLoaded()
await waitForAutoFrontmatter()
const { localeOptions, autoFrontmatter } = await waitForConfigLoaded()
if ((autoFrontmatter ?? pluginOptions.frontmatter) !== false) {
await waitForAutoFrontmatter()
}
extendsPageData(page as Page<PlumeThemePageData>, localeOptions)
resolvePageHead(page, localeOptions)
},
extendsMarkdown,
extendsBundlerOptions,
templateBuildRenderer,

View File

@ -7,5 +7,6 @@ export const logger = new Logger(THEME_NAME)
export * from './hash.js'
export * from './path.js'
export * from './package.js'
export * from './interopDefault.js'
export * from './resolveContent.js'
export * from './writeTemp.js'

View File

@ -0,0 +1,6 @@
export type Awaitable<T> = T | Promise<T>
export async function interopDefault<T>(m: Awaitable<T>): Promise<T extends { default: infer U } ? U : T> {
const resolved = await m
return (resolved as any).default || resolved
}

View File

@ -23,6 +23,13 @@ export interface PlumeThemeOptions extends PlumeThemeLocaleOptions {
*/
hostname?: string
/**
*
*
* @default 'filesystem'
*/
cache?: false | 'memory' | 'filesystem'
/**
*
*/
@ -33,6 +40,9 @@ export interface PlumeThemeOptions extends PlumeThemeLocaleOptions {
*/
configFile?: string
/**
* frontmatter
*/
autoFrontmatter?: false | Omit<AutoFrontmatter, 'frontmatter'>
}

View File

@ -60,13 +60,16 @@ export interface PlumeThemePluginOptions {
* @deprecated
* 使 [@vuepress/plugin-baidu-analytics](https://ecosystem.vuejs.press/zh/plugins/analytics/baidu-analytics.html) 代替
*/
baiduTongji?: never
baiduTongji?: false | { key: string }
/**
* @deprecated 使 `autoFrontmatter`
*/
frontmatter?: Omit<AutoFrontmatter, 'frontmatter'>
/**
*
*/
readingTime?: false | ReadingTimePluginOptions
/**

View File

@ -58,6 +58,7 @@ export default defineConfig((cli) => {
...DEFAULT_OPTIONS,
entry: ['./src/client/config.ts'],
outDir: './lib/client',
dts: false,
external: [
...clientExternal,
'./composables/index.js',