perf(plugin-md-power): improve icons rules
This commit is contained in:
parent
64e9a0f330
commit
f6ae1a1149
@ -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": {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './writer.js'
|
||||
export * from './plugin.js'
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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
15
pnpm-lock.yaml
generated
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user