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() 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 { 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 } export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) { const cache = new Map() 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) { 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 } }