perf(plugin-md-power): improve icons rules

This commit is contained in:
pengzhanbo 2024-08-09 01:12:13 +08:00
parent 64e9a0f330
commit f6ae1a1149
7 changed files with 14 additions and 234 deletions

View File

@ -37,19 +37,11 @@
"tsup": "tsup --config tsup.config.ts"
},
"peerDependencies": {
"@iconify/json": "^2",
"vuepress": "2.0.0-rc.14"
},
"peerDependenciesMeta": {
"@iconify/json": {
"optional": true
}
},
"dependencies": {
"@iconify/utils": "^2.1.30",
"@vuepress/helper": "2.0.0-rc.40",
"@vueuse/core": "^10.11.0",
"local-pkg": "^0.5.0",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.7",
"shiki": "^1.12.1",
@ -58,7 +50,6 @@
"vue": "^3.4.35"
},
"devDependencies": {
"@iconify/json": "^2.2.234",
"@types/markdown-it": "^14.1.2"
},
"publishConfig": {

View File

@ -6,13 +6,10 @@
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
import { parseRect } from '../../utils/parseRect.js'
type AddIcon = (iconName: string) => string | undefined
const [openTag, endTag] = [':[', ']:']
function createTokenizer(addIcon: AddIcon): RuleInline {
function createTokenizer(): RuleInline {
return (state, silent) => {
let found = false
const max = state.posMax
@ -56,31 +53,20 @@ function createTokenizer(addIcon: AddIcon): RuleInline {
state.posMax = state.pos
state.pos = start + 2
const [iconName, options = ''] = content.split(/\s+/)
const [name, options = ''] = content.split(/\s+/)
const [size, color] = options.split('/')
const open = state.push('iconify_open', 'span', 1)
open.markup = openTag
const icon = state.push('vp_iconify_open', 'VPIcon', 1)
icon.markup = openTag
const className = addIcon(iconName)
if (className)
open.attrSet('class', className)
let style = ''
if (name)
icon.attrSet('name', name)
if (size)
style += `width:${parseRect(size)};height:${parseRect(size)};`
icon.attrSet('size', size)
if (color)
style += `color:${color};`
icon.attrSet('color', color)
if (style)
open.attrSet('style', style)
const text = state.push('text', '', 0)
text.content = className ? '' : iconName
const close = state.push('iconify_close', 'span', -1)
const close = state.push('vp_iconify_close', 'VPIcon', -1)
close.markup = endTag
state.pos = state.posMax + 2
@ -90,9 +76,8 @@ function createTokenizer(addIcon: AddIcon): RuleInline {
}
}
export const iconsPlugin: PluginWithOptions<AddIcon> = (
export const iconsPlugin: PluginWithOptions<never> = (
md,
addIcon = () => '',
) => {
md.inline.ruler.before('emphasis', 'iconify', createTokenizer(addIcon))
md.inline.ruler.before('emphasis', 'iconify', createTokenizer())
}

View File

@ -1,2 +0,0 @@
export * from './writer.js'
export * from './plugin.js'

View File

@ -1,174 +0,0 @@
import { constants, promises as fsp } from 'node:fs'
import type { App } from 'vuepress/core'
import { getIconContentCSS, getIconData } from '@iconify/utils'
import { fs, logger } from 'vuepress/utils'
import { isPackageExists } from 'local-pkg'
import { customAlphabet } from 'nanoid'
import type { IconsOptions } from '../../../shared/index.js'
import { interopDefault } from '../../utils/package.js'
import { parseRect } from '../../utils/parseRect.js'
export interface IconCacheItem {
className: string
background: boolean
content: string
}
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 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'
options.color = options.color === 'currentColor' || !options.color ? 'currentcolor' : options.color
options.size = options.size ? parseRect(`${options.size}`) : '1em'
return options as Required<IconsOptions>
}
export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) {
const cache = new Map<string, IconCacheItem>()
const isInstalled = isPackageExists('@iconify/json')
const currentPath = app.dir.temp(CSS_PATH)
const write = async (content: string) => {
if (!content && app.env.isDev) {
if (existsSync(currentPath) && (await fsp.stat(currentPath)).isFile()) {
return
}
}
await app.writeTemp(CSS_PATH, content)
}
let timer: NodeJS.Timeout | null = null
const options = resolveOption(opt)
const prefix = options.prefix
const defaultContent = getDefaultContent(options)
async function writeCss() {
if (timer)
clearTimeout(timer)
timer = setTimeout(async () => {
let css = defaultContent
if (cache.size > 0) {
for (const [, { content, className }] of cache)
css += `.${className} {\n --svg: ${content};\n}\n`
await write(css)
}
}, 100)
}
function addIcon(iconName: string) {
if (!isInstalled)
return
if (cache.has(iconName)) {
const item = cache.get(iconName)!
return `${item.className}${item.background ? ' bg' : ''}`
}
const item: IconCacheItem = {
className: `${prefix}-${nanoid()}`,
...genIcon(iconName),
}
cache.set(iconName, item)
writeCss()
return `${item.className}${item.background ? ' bg' : ''}`
}
async function initIcon() {
if (!opt)
return await write('')
if (!isInstalled) {
logger.error('[plugin-md-power]: `@iconify/json` not found! Please install `@iconify/json` first.')
return
}
if (!locate) {
const mod = await interopDefault(import('@iconify/json'))
locate = mod.locate
}
return await writeCss()
}
return { addIcon, writeCss, initIcon }
}
function getDefaultContent(options: Required<IconsOptions>) {
const { prefix, size, color } = options
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;
mask: var(--svg) no-repeat;
-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%;
}
`
}
function genIcon(iconName: string): {
content: string
background: boolean
} {
if (!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(fs.readFileSync(filename, 'utf-8'))
iconDataCache.set(collect, iconJson)
}
catch {
logger.warn(`[plugin-md-power] Can not find icon, ${collect} is missing!`)
}
}
const data = getIconData(iconJson, name)
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 {
content: match ? match[1] : '',
background: !data.body.includes('currentColor'),
}
}
function existsSync(fp: string) {
try {
fs.accessSync(fp, constants.R_OK)
return true
}
catch {
return false
}
}

View File

@ -4,7 +4,7 @@ import { addViteOptimizeDepsInclude } from '@vuepress/helper'
import type { CanIUseOptions, MarkdownPowerPluginOptions } from '../shared/index.js'
import { caniusePlugin, legacyCaniuse } from './features/caniuse.js'
import { pdfPlugin } from './features/pdf.js'
import { createIconCSSWriter, iconsPlugin } from './features/icons/index.js'
import { iconsPlugin } from './features/icons.js'
import { bilibiliPlugin } from './features/video/bilibili.js'
import { youtubePlugin } from './features/video/youtube.js'
import { codepenPlugin } from './features/codepen.js'
@ -17,20 +17,15 @@ import { prepareConfigFile } from './prepareConfigFile.js'
export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin {
return (app) => {
const { initIcon, addIcon } = createIconCSSWriter(app, options.icons)
return {
name: 'vuepress-plugin-md-power',
// clientConfigFile: path.resolve(__dirname, '../client/config.js'),
clientConfigFile: app => prepareConfigFile(app, options),
define: {
__MD_POWER_INJECT_OPTIONS__: options,
},
onInitialized: async () => await initIcon(),
extendsBundlerOptions(bundlerOptions) {
if (options.repl) {
addViteOptimizeDepsInclude(
@ -57,7 +52,7 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
if (options.icons) {
// :[collect:name]:
md.use(iconsPlugin, addIcon)
md.use(iconsPlugin)
}
if (options.bilibili) {

View File

@ -14,8 +14,6 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
const imports = new Set<string>()
const enhances = new Set<string>()
imports.add(`import '@internal/md-power/icons.css'`)
if (options.pdf) {
imports.add(`import PDFViewer from '${CLIENT_FOLDER}components/PDFViewer.vue'`)
enhances.add(`app.component('PDFViewer', PDFViewer)`)

15
pnpm-lock.yaml generated
View File

@ -129,18 +129,12 @@ importers:
plugins/plugin-md-power:
dependencies:
'@iconify/utils':
specifier: ^2.1.30
version: 2.1.30
'@vuepress/helper':
specifier: 2.0.0-rc.40
version: 2.0.0-rc.40(typescript@5.5.4)(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.4)(yaml@2.5.0))(typescript@5.5.4)(vue@3.4.35(typescript@5.5.4)))
'@vueuse/core':
specifier: ^10.11.0
version: 10.11.0(vue@3.4.35(typescript@5.5.4))
local-pkg:
specifier: ^0.5.0
version: 0.5.0
markdown-it-container:
specifier: ^4.0.0
version: 4.0.0
@ -163,9 +157,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.4)(yaml@2.5.0))(typescript@5.5.4)(vue@3.4.35(typescript@5.5.4))
devDependencies:
'@iconify/json':
specifier: ^2.2.234
version: 2.2.234
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
@ -391,9 +382,6 @@ packages:
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
peerDependenciesMeta:
'@algolia/client-search':
optional: true
'@algolia/cache-browser-local-storage@4.20.0':
resolution: {integrity: sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==}
@ -5776,9 +5764,8 @@ snapshots:
'@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)':
dependencies:
algoliasearch: 4.20.0
optionalDependencies:
'@algolia/client-search': 4.20.0
algoliasearch: 4.20.0
'@algolia/cache-browser-local-storage@4.20.0':
dependencies: