commit
e690d48d0c
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@ -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",
|
||||
|
||||
@ -56,7 +56,7 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
icon: 'lucide:box',
|
||||
collapsed: false,
|
||||
dir: '功能',
|
||||
items: ['代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap'],
|
||||
items: ['图标', '代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap'],
|
||||
},
|
||||
{
|
||||
text: '自定义',
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
@ -161,6 +161,26 @@ interface BlogOptions {
|
||||
}
|
||||
```
|
||||
|
||||
### cache
|
||||
|
||||
- 类型: `false | 'memory' | 'filesystem'`
|
||||
- 默认值: `filesystem`
|
||||
- 详情:
|
||||
|
||||
是否启用 编译缓存,或配置缓存方式
|
||||
|
||||
此配置项用于解决 VuePress 启动速度慢的问题,在首次启动服务时,对编译结果进行缓存,二次启动时
|
||||
直接读取缓存,跳过编译,从而加快启动速度。
|
||||
|
||||
- `false`:禁用 缓存
|
||||
- `'memory'`:使用内存缓存,此方式可获得更快的启动速度,但随着项目文件数量增加,内存占用会增加,
|
||||
适合文章数量较少的项目使用
|
||||
- `'filesystem'`:使用文件系统缓存,此方式可获得相对快且稳定的启动速度,更适合内容多的项目使用
|
||||
|
||||
::: warning
|
||||
该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。
|
||||
:::
|
||||
|
||||
### locales
|
||||
|
||||
- 类型: `Record<string, PlumeThemeLocaleConfig>`
|
||||
|
||||
@ -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
|
||||
|
||||
此功能默认不启用,你可以在配置文件中启用它。
|
||||
|
||||
85
docs/notes/theme/guide/功能/图标.md
Normal file
85
docs/notes/theme/guide/功能/图标.md
Normal 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`,也会自动解析为本地图标资源。
|
||||
:::
|
||||
@ -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
|
||||
|
||||
@ -25,6 +25,7 @@ export default defineConfig(() => {
|
||||
...DEFAULT_OPTIONS,
|
||||
entry: ['./src/client/config.ts'],
|
||||
outDir: './lib/client',
|
||||
dts: false,
|
||||
external: clientExternal,
|
||||
},
|
||||
]
|
||||
|
||||
@ -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.
|
||||
@ -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" />
|
||||
```
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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>
|
||||
@ -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 }
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './iconify.js'
|
||||
@ -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
|
||||
@ -1,6 +0,0 @@
|
||||
import { iconifyPlugin } from './plugin.js'
|
||||
|
||||
export * from './plugin.js'
|
||||
|
||||
/** @deprecated 请使用 具名导出 替代 默认导出 */
|
||||
export default iconifyPlugin
|
||||
@ -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'),
|
||||
}
|
||||
}
|
||||
6
plugins/plugin-iconify/src/shim.d.ts
vendored
6
plugins/plugin-iconify/src/shim.d.ts
vendored
@ -1,6 +0,0 @@
|
||||
declare module '*.vue' {
|
||||
const comp: any
|
||||
export default comp
|
||||
}
|
||||
|
||||
declare const __VUEPRESS_SSR__: boolean
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -53,6 +53,7 @@ export default defineConfig(() => {
|
||||
entry: ['./src/client/config.ts'],
|
||||
outDir: './lib/client',
|
||||
external: clientExternal,
|
||||
dts: false,
|
||||
},
|
||||
// client/index.js
|
||||
{
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
56
pnpm-lock.yaml
generated
@ -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:
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,4 +286,10 @@ watchPostEffect(() => {
|
||||
background-color: var(--vp-c-gutter);
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.vp-navbar .hamburger {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
68
theme/src/client/components/VPIconify.vue
Normal file
68
theme/src/client/components/VPIconify.vue
Normal 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>
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
16
theme/src/client/composables/icons.ts
Normal file
16
theme/src/client/composables/icons.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
7
theme/src/client/shim.d.ts
vendored
7
theme/src/client/shim.d.ts
vendored
@ -58,3 +58,10 @@ declare module '@internal/encrypt' {
|
||||
encrypt,
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@internal/iconify' {
|
||||
const icons: Record<string, string>
|
||||
export {
|
||||
icons,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vp-iconify {
|
||||
margin: 0.3em;
|
||||
.vp-icon {
|
||||
margin: 0 0.3em;
|
||||
}
|
||||
|
||||
.smooth {
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
196
theme/src/node/prepare/prepareIcons.ts
Normal file
196
theme/src/node/prepare/prepareIcons.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
6
theme/src/node/utils/interopDefault.ts
Normal file
6
theme/src/node/utils/interopDefault.ts
Normal 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
|
||||
}
|
||||
@ -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'>
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
/**
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user