mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-24 11:08:16 +08:00
175 lines
4.6 KiB
TypeScript
175 lines
4.6 KiB
TypeScript
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
|
|
}
|
|
}
|