commit
f90599c788
@ -19,7 +19,7 @@
|
||||
"echarts": "^5.5.0",
|
||||
"flowchart.ts": "^3.0.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"vue": "^3.4.29",
|
||||
"vue": "^3.4.30",
|
||||
"vuepress-theme-plume": "workspace:~"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -2,7 +2,7 @@ import config from '@pengzhanbo/eslint-config-vue'
|
||||
|
||||
export default config({
|
||||
// todo: 正则校验
|
||||
// 当前项目中的 正则 海冰不能完全通过 规则,存在 53 个问题
|
||||
// 当前项目中的 正则 还并不能完全通过 规则,存在 53 个问题
|
||||
// 但处理起来比较麻烦,因此将会作为一项比较长期的工作来完成。
|
||||
regexp: false,
|
||||
ignores: [
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"@vue/devtools-api": "6.6.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"create-filter": "^1.0.1",
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
"vuepress": "2.0.0-rc.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuepress-plume/plugin-content-update": "workspace:~",
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -52,10 +52,10 @@
|
||||
"local-pkg": "^0.5.0",
|
||||
"markdown-it-container": "^4.0.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"shiki": "^1.8.0",
|
||||
"tm-grammars": "^1.12.10",
|
||||
"shiki": "^1.9.0",
|
||||
"tm-grammars": "^1.12.11",
|
||||
"tm-themes": "^1.4.3",
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.221",
|
||||
|
||||
@ -51,12 +51,12 @@
|
||||
"cpx2": "^7.0.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.21.5",
|
||||
"execa": "^9.2.0",
|
||||
"execa": "^9.3.0",
|
||||
"netlify-cli": "^17.29.0",
|
||||
"portfinder": "^1.0.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.7"
|
||||
"@types/node": "^20.14.8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"@vue/devtools-api": "6.6.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"create-filter": "^1.0.1",
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
"dependencies": {
|
||||
"@netlify/functions": "^2.8.0",
|
||||
"leancloud-storage": "^4.15.2",
|
||||
"vue": "^3.4.29",
|
||||
"vue": "^3.4.30",
|
||||
"vue-router": "4.3.2",
|
||||
"vuepress-plugin-netlify-functions": "workspace:~"
|
||||
},
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
"mark.js": "^8.11.1",
|
||||
"minisearch": "^6.3.0",
|
||||
"p-map": "^7.0.2",
|
||||
"vue": "^3.4.29"
|
||||
"vue": "^3.4.30"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -36,8 +36,8 @@
|
||||
"vuepress": "2.0.0-rc.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shikijs/transformers": "^1.8.0",
|
||||
"@shikijs/twoslash": "^1.8.0",
|
||||
"@shikijs/transformers": "^1.9.0",
|
||||
"@shikijs/twoslash": "^1.9.0",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@vuepress/helper": "2.0.0-rc.37",
|
||||
"floating-vue": "^5.2.2",
|
||||
@ -45,9 +45,9 @@
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.2.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"shiki": "^1.8.0",
|
||||
"twoslash": "^0.2.8",
|
||||
"twoslash-vue": "^0.2.8"
|
||||
"shiki": "^1.9.0",
|
||||
"twoslash": "^0.2.9",
|
||||
"twoslash-vue": "^0.2.9"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ShikiTransformer } from 'shiki'
|
||||
import {
|
||||
addClassToHast,
|
||||
bundledLanguages,
|
||||
getHighlighter,
|
||||
createHighlighter,
|
||||
isPlainLang,
|
||||
isSpecialLang,
|
||||
} from 'shiki'
|
||||
@ -41,7 +41,7 @@ export async function highlight(
|
||||
languages = Object.keys(bundledLanguages),
|
||||
} = options
|
||||
|
||||
const highlighter = await getHighlighter({
|
||||
const highlighter = await createHighlighter({
|
||||
themes:
|
||||
typeof theme === 'object' && 'light' in theme && 'dark' in theme
|
||||
? [theme.light, theme.dark]
|
||||
|
||||
693
pnpm-lock.yaml
generated
693
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -96,7 +96,7 @@
|
||||
"katex": "^0.16.10",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"nanoid": "^5.0.7",
|
||||
"vue": "^3.4.29",
|
||||
"vue": "^3.4.30",
|
||||
"vue-router": "^4.4.0",
|
||||
"vuepress-plugin-md-enhance": "2.0.0-rc.50",
|
||||
"vuepress-plugin-md-power": "workspace:*"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { resolveRouteFullPath, useRouter, withBase } from 'vuepress/client'
|
||||
import { isLinkExternal } from 'vuepress/shared'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { useRouter, withBase } from 'vuepress/client'
|
||||
import { useLink } from '../composables/link.js'
|
||||
|
||||
interface Props {
|
||||
tag?: string
|
||||
@ -20,24 +20,13 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
target: undefined,
|
||||
rel: undefined,
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const isExternal = computed(
|
||||
() => props.href && isLinkExternal(props.href),
|
||||
)
|
||||
|
||||
const component = computed(() => {
|
||||
return props.tag || props.href ? 'a' : 'button'
|
||||
})
|
||||
|
||||
const link = computed(() => {
|
||||
if (!props.href)
|
||||
return undefined
|
||||
if (isExternal.value)
|
||||
return props.href
|
||||
return resolveRouteFullPath(props.href)
|
||||
})
|
||||
const { link, isExternal } = useLink(toRef(props, 'href'), toRef(props, 'target'))
|
||||
|
||||
function linkTo(e: Event) {
|
||||
if (!isExternal.value) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { resolveRouteFullPath, useRoute, useRouter, withBase } from 'vuepress/client'
|
||||
import { isLinkExternal } from '@vuepress/helper/client'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { useRouter, withBase } from 'vuepress/client'
|
||||
import { useLink } from '../composables/link.js'
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string
|
||||
@ -12,19 +12,10 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'))
|
||||
const isExternal = computed(
|
||||
() => (props.href && isLinkExternal(props.href)) || props.target === '_blank',
|
||||
)
|
||||
const link = computed(() => {
|
||||
if (!props.href)
|
||||
return undefined
|
||||
if (isExternal.value)
|
||||
return props.href
|
||||
return resolveRouteFullPath(props.href, route.path)
|
||||
})
|
||||
|
||||
const { link, isExternal } = useLink(toRef(props, 'href'), toRef(props, 'target'))
|
||||
|
||||
function linkTo(e: Event) {
|
||||
if (!isExternal.value) {
|
||||
|
||||
@ -19,6 +19,7 @@ export * from './blog-tags.js'
|
||||
export * from './blog-archives.js'
|
||||
export * from './tag-colors.js'
|
||||
|
||||
export * from './link.js'
|
||||
export * from './locale.js'
|
||||
export * from './route-query.js'
|
||||
export * from './watermark.js'
|
||||
|
||||
36
theme/src/client/composables/link.ts
Normal file
36
theme/src/client/composables/link.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { isLinkExternal } from '@vuepress/helper/client'
|
||||
import { resolveRouteFullPath, useRoute } from 'vuepress/client'
|
||||
import { type MaybeRefOrGetter, computed, toValue } from 'vue'
|
||||
import { useData } from './data.js'
|
||||
|
||||
const SEARCH_RE = /\.md(?:(?:#|\?).*)?$/
|
||||
|
||||
export function useLink(
|
||||
href: MaybeRefOrGetter<string | undefined>,
|
||||
target?: MaybeRefOrGetter<string | undefined>,
|
||||
) {
|
||||
const route = useRoute()
|
||||
const { page } = useData()
|
||||
|
||||
const isExternal = computed(
|
||||
() => {
|
||||
const link = toValue(href)
|
||||
const rawTarget = toValue(target)
|
||||
return (link && isLinkExternal(link)) || rawTarget === '_blank'
|
||||
},
|
||||
)
|
||||
|
||||
const link = computed(() => {
|
||||
const link = toValue(href)
|
||||
if (!link)
|
||||
return undefined
|
||||
if (isExternal.value)
|
||||
return link
|
||||
const currentPath = link.startsWith('./') && SEARCH_RE.test(link)
|
||||
? `/${page.value.filePathRelative!}`
|
||||
: route.path
|
||||
return resolveRouteFullPath(link, currentPath)
|
||||
})
|
||||
|
||||
return { isExternal, link }
|
||||
}
|
||||
111
theme/src/node/extendsMarkdown.ts
Normal file
111
theme/src/node/extendsMarkdown.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 针对主题使用了 shiki + twoslash, 以及各种各样的对 markdown 的扩展,
|
||||
* 导致了 markdown render 的速度变得越来越慢,如果每次启动都全量编译,那么时间开销会非常夸张。
|
||||
* 因此,对 markdown render 包装一层 缓存,通过 content hash 对比内容是否有更新,
|
||||
* 没有更新的直接应用缓存从而跳过编译过程,加快启动速度。
|
||||
*
|
||||
* 此功能计划做成独立的插件,但还不确定是放在 vuepress/ecosystem 还是在 主题插件内,
|
||||
* 也有可能到 vuepress/core 仓库中进行更深度的优化。
|
||||
* 因此,先在本主题中进行 实验性验证。
|
||||
*
|
||||
* 使用此功能后,本主题原本的启动耗时,由每次 13s 左右 优化到 二次启动时 1.2s 左右。
|
||||
* 基本只剩下 vuepress 本身的开销和 加载 shiki 所有语言带来 0.5s 左右的开销。
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import type { App } from 'vuepress'
|
||||
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
|
||||
import { fs } from 'vuepress/utils'
|
||||
|
||||
interface CacheContent {
|
||||
content: string
|
||||
env: MarkdownEnv
|
||||
}
|
||||
|
||||
const cacheDir = 'markdown/render'
|
||||
const metaFile = '_metadata.json'
|
||||
|
||||
export async function extendsMarkdown(md: Markdown, app: App): Promise<void> {
|
||||
// 如果是在 构建阶段,且缓存文件夹不存在,则不进行缓存
|
||||
// 因为构建阶段仅一次性产物,生成缓存资源反而会带来额外的开销
|
||||
if (app.env.isBuild && !fs.existsSync(app.dir.cache())) {
|
||||
return
|
||||
}
|
||||
|
||||
await fs.ensureDir(app.dir.cache(cacheDir))
|
||||
const metadata = await readMetadata(app)
|
||||
|
||||
const writeCache = (filepath: string, cache: CacheContent) => {
|
||||
const cachePath = app.dir.cache(cacheDir, filepath)
|
||||
const content = JSON.stringify(cache)
|
||||
fs.writeFileSync(cachePath, content, 'utf-8')
|
||||
}
|
||||
|
||||
const readCache = (filepath: string): CacheContent | null => {
|
||||
const cachePath = app.dir.cache(cacheDir, filepath)
|
||||
try {
|
||||
const content = fs.readFileSync(cachePath, 'utf-8')
|
||||
return JSON.parse(content) as CacheContent
|
||||
}
|
||||
catch {}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const rawRender = md.render
|
||||
md.render = (input, env: MarkdownEnv) => {
|
||||
const filepath = env.filePathRelative
|
||||
|
||||
if (!filepath) {
|
||||
return rawRender(input, env)
|
||||
}
|
||||
|
||||
const hash = getContentHash(input)
|
||||
const cachePath = normalizePath(filepath)
|
||||
|
||||
if (metadata[filepath] === hash) {
|
||||
const cache = readCache(cachePath)
|
||||
if (cache) {
|
||||
Object.assign(env, cache.env)
|
||||
return cache.content
|
||||
}
|
||||
}
|
||||
|
||||
metadata[filepath] = hash
|
||||
|
||||
const renderedContent = rawRender(input, env)
|
||||
|
||||
writeCache(cachePath, { content: renderedContent, env })
|
||||
updateMetadata(app, metadata)
|
||||
return renderedContent
|
||||
}
|
||||
}
|
||||
|
||||
async function readMetadata(app: App): Promise<Record<string, string>> {
|
||||
const filepath = app.dir.cache(cacheDir, metaFile)
|
||||
try {
|
||||
const content = await fs.readFile(filepath, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
}
|
||||
catch {}
|
||||
return {}
|
||||
}
|
||||
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
function updateMetadata(app: App, metadata: Record<string, string>) {
|
||||
const filepath = app.dir.cache(cacheDir, metaFile)
|
||||
timer && clearTimeout(timer)
|
||||
timer = setTimeout(
|
||||
async () => await fs.writeFile(filepath, JSON.stringify(metadata), 'utf-8'),
|
||||
200,
|
||||
)
|
||||
}
|
||||
|
||||
function normalizePath(filepath: string) {
|
||||
return getContentHash(filepath)
|
||||
}
|
||||
|
||||
function getContentHash(content: string): string {
|
||||
const hash = createHash('md5')
|
||||
hash.update(content)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
@ -13,6 +13,7 @@ import {
|
||||
templateBuildRenderer,
|
||||
} from './config/index.js'
|
||||
import { setupPrepare, watchPrepare } from './prepare/index.js'
|
||||
import { extendsMarkdown } from './extendsMarkdown.js'
|
||||
|
||||
export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
|
||||
const {
|
||||
@ -55,6 +56,8 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
|
||||
resolvePageHead(page, localeOptions)
|
||||
},
|
||||
|
||||
extendsMarkdown,
|
||||
|
||||
extendsBundlerOptions,
|
||||
|
||||
templateBuildRenderer,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user