From c2fc5ad7b110cb2bf316f9480d4556ac0c6de66f Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Fri, 29 Mar 2024 00:38:14 +0800 Subject: [PATCH] feat(plugin-md-power): add `:[icon]:` syntax supported --- .vscode/settings.json | 1 + docs/.vuepress/theme.ts | 7 + docs/package.json | 1 + plugins/plugin-md-power/package.json | 2 +- .../src/client/composables/setupCanIUse.ts | 20 +++ .../src/client/composables/size.ts | 55 ++++++++ plugins/plugin-md-power/src/client/config.ts | 33 +++++ plugins/plugin-md-power/src/client/index.ts | 1 + plugins/plugin-md-power/src/client/options.ts | 5 + plugins/plugin-md-power/src/client/shim.d.ts | 6 + .../plugin-md-power/src/client/utils/is.ts | 15 ++ .../plugin-md-power/src/client/utils/link.ts | 6 + .../src/node/{ => features}/caniuse.ts | 2 +- .../src/node/features/icons/index.ts | 2 + .../src/node/features/icons/plugin.ts | 96 +++++++++++++ .../src/node/features/icons/writer.ts | 128 ++++++++++++++++++ plugins/plugin-md-power/src/node/index.ts | 2 + .../src/node/markdown-it-container.d.ts | 6 + plugins/plugin-md-power/src/node/plugin.ts | 58 ++++++++ .../plugin-md-power/src/node/utils/package.ts | 6 + .../src/node/utils/parseRect.ts | 6 + .../src/node/utils/resolveAttrs.ts | 41 ++++++ .../src/node/utils/timeToSeconds.ts | 11 ++ plugins/plugin-md-power/src/shared/icons.ts | 19 +++ plugins/plugin-md-power/src/shared/index.ts | 3 + plugins/plugin-md-power/src/shared/plugin.ts | 11 ++ plugins/plugin-md-power/src/shared/size.ts | 5 + plugins/plugin-md-power/src/shared/video.ts | 25 ++++ plugins/tsconfig.build.json | 3 +- pnpm-lock.yaml | 87 ++++++++++-- theme/package.json | 4 +- theme/src/client/utils/color.ts | 17 --- theme/src/node/plugins.ts | 12 +- theme/src/shared/options/plugins.ts | 8 +- tsconfig.json | 3 + 35 files changed, 668 insertions(+), 39 deletions(-) create mode 100644 plugins/plugin-md-power/src/client/composables/setupCanIUse.ts create mode 100644 plugins/plugin-md-power/src/client/composables/size.ts create mode 100644 plugins/plugin-md-power/src/client/config.ts create mode 100644 plugins/plugin-md-power/src/client/index.ts create mode 100644 plugins/plugin-md-power/src/client/options.ts create mode 100644 plugins/plugin-md-power/src/client/shim.d.ts create mode 100644 plugins/plugin-md-power/src/client/utils/is.ts create mode 100644 plugins/plugin-md-power/src/client/utils/link.ts rename plugins/plugin-md-power/src/node/{ => features}/caniuse.ts (99%) create mode 100644 plugins/plugin-md-power/src/node/features/icons/index.ts create mode 100644 plugins/plugin-md-power/src/node/features/icons/plugin.ts create mode 100644 plugins/plugin-md-power/src/node/features/icons/writer.ts create mode 100644 plugins/plugin-md-power/src/node/index.ts create mode 100644 plugins/plugin-md-power/src/node/markdown-it-container.d.ts create mode 100644 plugins/plugin-md-power/src/node/plugin.ts create mode 100644 plugins/plugin-md-power/src/node/utils/package.ts create mode 100644 plugins/plugin-md-power/src/node/utils/parseRect.ts create mode 100644 plugins/plugin-md-power/src/node/utils/resolveAttrs.ts create mode 100644 plugins/plugin-md-power/src/node/utils/timeToSeconds.ts create mode 100644 plugins/plugin-md-power/src/shared/icons.ts create mode 100644 plugins/plugin-md-power/src/shared/index.ts create mode 100644 plugins/plugin-md-power/src/shared/plugin.ts create mode 100644 plugins/plugin-md-power/src/shared/size.ts create mode 100644 plugins/plugin-md-power/src/shared/video.ts delete mode 100644 theme/src/client/utils/color.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c567d76..21279bd6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -54,6 +54,7 @@ "vue" ], "cSpell.words": [ + "bilibili", "bumpp", "caniuse", "colours", diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 21d757c2..66896825 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -66,6 +66,13 @@ export const theme: Theme = themePlume({ mermaid: true, flowchart: true, }, + markdownPower: { + pdf: true, + caniuse: true, + bilibili: true, + youtube: true, + icons: true, + }, comment: { provider: 'Giscus', comment: true, diff --git a/docs/package.json b/docs/package.json index 5fa7292b..f87d48b6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,6 +12,7 @@ "vuepress": "2.0.0-rc.9" }, "dependencies": { + "@iconify/json": "^2.2.196", "@vuepress/bundler-vite": "2.0.0-rc.9", "anywhere": "^1.6.0", "chart.js": "^4.4.2", diff --git a/plugins/plugin-md-power/package.json b/plugins/plugin-md-power/package.json index 9ee37fc5..0fd00219 100644 --- a/plugins/plugin-md-power/package.json +++ b/plugins/plugin-md-power/package.json @@ -1,5 +1,5 @@ { - "name": "@vuepress-plume/plugin-md-power", + "name": "vuepress-plugin-md-power", "type": "module", "version": "1.0.0-rc.47", "description": "The Plugin for VuePres 2 - markdown power", diff --git a/plugins/plugin-md-power/src/client/composables/setupCanIUse.ts b/plugins/plugin-md-power/src/client/composables/setupCanIUse.ts new file mode 100644 index 00000000..713dac08 --- /dev/null +++ b/plugins/plugin-md-power/src/client/composables/setupCanIUse.ts @@ -0,0 +1,20 @@ +let isBind = false +export function setupCanIUse(): void { + if (isBind) + return + isBind = true + + window.addEventListener('message', (message) => { + const data = message.data + + if (typeof data === 'string' && data.includes('ciu_embed')) { + const [, feature, height] = data.split(':') + const el = document.querySelector(`.ciu_embed[data-feature="${feature}"]:not([data-skip])`) + if (el) { + const h = Number.parseInt(height) + 30 + ; (el.childNodes[0] as any).height = `${h}px` + el.setAttribute('data-skip', 'true') + } + } + }) +} diff --git a/plugins/plugin-md-power/src/client/composables/size.ts b/plugins/plugin-md-power/src/client/composables/size.ts new file mode 100644 index 00000000..1cdd1cdd --- /dev/null +++ b/plugins/plugin-md-power/src/client/composables/size.ts @@ -0,0 +1,55 @@ +import type { MaybeRef } from '@vueuse/core' +import { useEventListener } from '@vueuse/core' +import type { Ref, ShallowRef, ToRefs } from 'vue' +import { computed, isRef, onMounted, ref, shallowRef, toValue, watch } from 'vue' +import type { SizeOptions } from '../../shared/size.js' + +export interface SizeInfo { + el: ShallowRef + width: Ref + height: Ref + resize: () => void +} +export function useSize( + options: ToRefs, + extraHeight: MaybeRef = 0, +): SizeInfo { + const el = shallowRef() + const width = computed(() => toValue(options.width) || '100%') + const height = ref('auto') + + const getRadio = (ratio: number | string | undefined): number => { + if (typeof ratio === 'string') { + const [width, height] = ratio.split(':') + const parsedRadio = Number(width) / Number(height) + + if (!Number.isNaN(parsedRadio)) + return parsedRadio + } + + return typeof ratio === 'number' ? ratio : 16 / 9 + } + + const getHeight = (width: number): string => { + const height = toValue(options.height) + const ratio = getRadio(toValue(options.ratio)) + + return height || `${Number(width) / ratio + toValue(extraHeight)}px` + } + + const resize = (): void => { + if (el.value) + height.value = getHeight(el.value.offsetWidth) + } + + onMounted(() => { + resize() + if (isRef(extraHeight)) + watch(extraHeight, resize) + + useEventListener('orientationchange', resize) + useEventListener('resize', resize) + }) + + return { el, width, height, resize } +} diff --git a/plugins/plugin-md-power/src/client/config.ts b/plugins/plugin-md-power/src/client/config.ts new file mode 100644 index 00000000..a29aeef0 --- /dev/null +++ b/plugins/plugin-md-power/src/client/config.ts @@ -0,0 +1,33 @@ +import { defineClientConfig } from 'vuepress/client' +import type { ClientConfig } from 'vuepress/client' +import { pluginOptions } from './options.js' +import { setupCanIUse } from './composables/setupCanIUse.js' +import PDFViewer from './components/PDFViewer.vue' +import Bilibili from './components/Bilibili.vue' +import Youtube from './components/Youtube.vue' + +import '@internal/md-power/icons.css' + +declare const __VUEPRESS_SSR__: boolean + +export default defineClientConfig({ + enhance({ router, app }) { + if (pluginOptions.pdf) + app.component('PDFViewer', PDFViewer) + + if (pluginOptions.bilibili) + app.component('VideoBilibili', Bilibili) + + if (pluginOptions.youtube) + app.component('VideoYoutube', Youtube) + + if (__VUEPRESS_SSR__) + return + + if (pluginOptions.caniuse) { + router.afterEach(() => { + setupCanIUse() + }) + } + }, +}) as ClientConfig diff --git a/plugins/plugin-md-power/src/client/index.ts b/plugins/plugin-md-power/src/client/index.ts new file mode 100644 index 00000000..72593733 --- /dev/null +++ b/plugins/plugin-md-power/src/client/index.ts @@ -0,0 +1 @@ +export * from '../shared/index.js' diff --git a/plugins/plugin-md-power/src/client/options.ts b/plugins/plugin-md-power/src/client/options.ts new file mode 100644 index 00000000..10c4b9a0 --- /dev/null +++ b/plugins/plugin-md-power/src/client/options.ts @@ -0,0 +1,5 @@ +import type { MarkdownPowerPluginOptions } from '../shared/index.js' + +declare const __MD_POWER_INJECT_OPTIONS__: MarkdownPowerPluginOptions + +export const pluginOptions = __MD_POWER_INJECT_OPTIONS__ diff --git a/plugins/plugin-md-power/src/client/shim.d.ts b/plugins/plugin-md-power/src/client/shim.d.ts new file mode 100644 index 00000000..f07bbd33 --- /dev/null +++ b/plugins/plugin-md-power/src/client/shim.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { ComponentOptions } from 'vue' + + const comp: ComponentOptions + export default comp +} diff --git a/plugins/plugin-md-power/src/client/utils/is.ts b/plugins/plugin-md-power/src/client/utils/is.ts new file mode 100644 index 00000000..fa23aada --- /dev/null +++ b/plugins/plugin-md-power/src/client/utils/is.ts @@ -0,0 +1,15 @@ +export function checkIsMobile(ua: string): boolean { + return /\b(?:Android|iPhone)/i.test(ua) +} + +export function checkIsSafari(ua: string): boolean { + return /version\/([\w.]+) .*(mobile ?safari|safari)/i.test(ua) +} + +export function checkIsiPad(ua: string): boolean { + return [ + /\((ipad);[-\w),; ]+apple/i, + /applecoremedia\/[\w.]+ \((ipad)/i, + /\b(ipad)\d\d?,\d\d?[;\]].+ios/i, + ].some(item => item.test(ua)) +} diff --git a/plugins/plugin-md-power/src/client/utils/link.ts b/plugins/plugin-md-power/src/client/utils/link.ts new file mode 100644 index 00000000..600912c3 --- /dev/null +++ b/plugins/plugin-md-power/src/client/utils/link.ts @@ -0,0 +1,6 @@ +import { isLinkHttp } from 'vuepress/shared' +import { withBase } from 'vuepress/client' + +export function normalizeLink(url: string): string { + return isLinkHttp(url) ? url : withBase(url) +} diff --git a/plugins/plugin-md-power/src/node/caniuse.ts b/plugins/plugin-md-power/src/node/features/caniuse.ts similarity index 99% rename from plugins/plugin-md-power/src/node/caniuse.ts rename to plugins/plugin-md-power/src/node/features/caniuse.ts index 41cc85f6..2f5c3b70 100644 --- a/plugins/plugin-md-power/src/node/caniuse.ts +++ b/plugins/plugin-md-power/src/node/features/caniuse.ts @@ -6,7 +6,7 @@ import type { PluginWithOptions, Token } from 'markdown-it' import type { RuleBlock } from 'markdown-it/lib/parser_block.js' import type { Markdown } from 'vuepress/markdown' import container from 'markdown-it-container' -import type { CanIUseMode, CanIUseOptions, CanIUseTokenMeta } from '../shared/index.js' +import type { CanIUseMode, CanIUseOptions, CanIUseTokenMeta } from '../../shared/index.js' // @[caniuse]() const minLength = 12 diff --git a/plugins/plugin-md-power/src/node/features/icons/index.ts b/plugins/plugin-md-power/src/node/features/icons/index.ts new file mode 100644 index 00000000..714b313e --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/icons/index.ts @@ -0,0 +1,2 @@ +export * from './writer.js' +export * from './plugin.js' diff --git a/plugins/plugin-md-power/src/node/features/icons/plugin.ts b/plugins/plugin-md-power/src/node/features/icons/plugin.ts new file mode 100644 index 00000000..64a0bb50 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/icons/plugin.ts @@ -0,0 +1,96 @@ +/** + * :[mdi:11]: + * :[mdi:11 24px]: + * :[mid:11 /#ccc]: + * :[fluent-mdl2:toggle-filled 128px/#fff]: + */ +import type { PluginWithOptions } from 'markdown-it' +import type { RuleInline } from 'markdown-it/lib/parser_inline.js' +import { parseRect } from '../../utils/parseRect.js' + +type AddIcon = (iconName: string) => string | undefined + +function createTokenizer(addIcon: AddIcon): RuleInline { + return (state, silent) => { + let found = false + const max = state.posMax + const start = state.pos + + if (state.src.slice(start, start + 2) !== ':[') + return false + + if (silent) + return false + + // :[]: + if (max - start < 5) + return false + + state.pos = start + 2 + + while (state.pos < max) { + if (state.src.slice(state.pos, state.pos + 2) === ']:') { + found = true + break + } + + state.md.inline.skipToken(state) + } + + if (!found || start + 2 === state.pos) { + state.pos = start + + return false + } + const content = state.src.slice(start + 2, state.pos) + + // 不允许前后带有空格 + if (/^\s|\s$/.test(content)) { + state.pos = start + return false + } + + // found! + state.posMax = state.pos + state.pos = start + 2 + + const [iconName, options = ''] = content.split(/\s+/) + const [size, color] = options.split('/') + + const open = state.push('iconify_open', 'span', 1) + open.markup = ':[' + + const className = addIcon(iconName) + + if (className) + open.attrSet('class', className) + + let style = '' + if (size) + style += `width:${parseRect(size)};height:${parseRect(size)};` + + if (color) + style += `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) + close.markup = ']:' + + state.pos = state.posMax + 2 + state.posMax = max + + return true + } +} + +export const iconsPlugin: PluginWithOptions = ( + md, + addIcon = () => '', +) => { + md.inline.ruler.before('emphasis', 'iconify', createTokenizer(addIcon)) +} diff --git a/plugins/plugin-md-power/src/node/features/icons/writer.ts b/plugins/plugin-md-power/src/node/features/icons/writer.ts new file mode 100644 index 00000000..de389421 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/icons/writer.ts @@ -0,0 +1,128 @@ +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/icons.js' +import { interopDefault } from '../../utils/package.js' +import { parseRect } from '../../utils/parseRect.js' + +export interface IconCacheItem { + className: string + content: string +} + +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8) +const iconDataCache = new Map() +const URL_CONTENT_RE = /(url\([^]+?\))/ + +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 write = (content: string) => app.writeTemp('internal/md-power/icons.css', content) + + const options = resolveOption(opt) + const prefix = options.prefix + const defaultContent = getDefaultContent(options) + + async function writeCss() { + let css = defaultContent + + for (const [, { content, className }] of cache) + css += `.${className} {\n --svg: ${content};\n}\n` + + await write(css) + } + + function addIcon(iconName: string) { + if (!isInstalled) + return + + if (cache.has(iconName)) + return cache.get(iconName)!.className + + const item: IconCacheItem = { + className: `${prefix}-${nanoid()}`, + content: '', + } + cache.set(iconName, item) + genIconContent(iconName, (content) => { + item.content = content + writeCss() + }) + return item.className + } + + 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 + } + + return await writeCss() + } + + return { addIcon, writeCss, initIcon } +} + +function getDefaultContent(options: Required) { + const { prefix, size, color } = options + return `[class^="${prefix}-"], +[class*=" ${prefix}-"] { + display: inline-block; + width: ${size}; + height: ${size}; + vertical-align: middle; + 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%; +} +` +} + +let locate: ((name: string) => any) | undefined + +async function genIconContent(iconName: string, cb: (content: string) => void) { + if (!locate) { + const mod = await interopDefault(import('@iconify/json')) + locate = mod.locate + } + + const [collect, name] = iconName.split(':') + let iconJson: any = iconDataCache.get(collect) + if (!iconJson) { + const filename = locate(collect) + + try { + iconJson = JSON.parse(await fs.readFile(filename, 'utf-8')) + iconDataCache.set(collect, iconJson) + } + catch (e) { + logger.warn(`[plugin-md-power] Can not find icon, ${collect} is missing!`) + } + } + const data = getIconData(iconJson, name) + if (!data) + return logger.error(`[plugin-md-power] Can not read icon in ${collect}, ${name} is missing!`) + + const content = getIconContentCSS(data, { + height: data.height || 24, + }) + const match = content.match(URL_CONTENT_RE) + return cb(match ? match[1] : '') +} diff --git a/plugins/plugin-md-power/src/node/index.ts b/plugins/plugin-md-power/src/node/index.ts new file mode 100644 index 00000000..9c2a9e8f --- /dev/null +++ b/plugins/plugin-md-power/src/node/index.ts @@ -0,0 +1,2 @@ +export * from './plugin.js' +export * from '../shared/index.js' diff --git a/plugins/plugin-md-power/src/node/markdown-it-container.d.ts b/plugins/plugin-md-power/src/node/markdown-it-container.d.ts new file mode 100644 index 00000000..44347be7 --- /dev/null +++ b/plugins/plugin-md-power/src/node/markdown-it-container.d.ts @@ -0,0 +1,6 @@ +declare module 'markdown-it-container' { + import type { PluginWithParams } from 'markdown-it' + + const container: PluginWithParams + export = container +} diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts new file mode 100644 index 00000000..d40cdb5a --- /dev/null +++ b/plugins/plugin-md-power/src/node/plugin.ts @@ -0,0 +1,58 @@ +import type { Plugin } from 'vuepress/core' +import { getDirname, path } from 'vuepress/utils' +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 { bilibiliPlugin } from './features/video/bilibili.js' +import { youtubePlugin } from './features/video/youtube.js' + +const __dirname = getDirname(import.meta.url) + +export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin { + return (app) => { + const { initIcon, addIcon } = createIconCSSWriter(app, options.icons) + + return { + name: '@vuepress-plume/plugin-md-power', + + clientConfigFile: path.resolve(__dirname, '../client/config.js'), + + define: { + __MD_POWER_INJECT_OPTIONS__: options, + }, + + onInitialized: async () => await initIcon(), + + extendsMarkdown(md) { + if (options.caniuse) { + const caniuse = options.caniuse === true ? {} : options.caniuse + // @[caniuse](feature_name) + md.use(caniusePlugin, caniuse) + // 兼容旧语法 + legacyCaniuse(md, caniuse) + } + + if (options.pdf) { + // @[pdf](url) + md.use(pdfPlugin) + } + + if (options.icons) { + // :[collect:name]: + md.use(iconsPlugin, addIcon) + } + + if (options.bilibili) { + // @[bilibili](bvid aid cid) + md.use(bilibiliPlugin) + } + + if (options.youtube) { + // @[youtube](id) + md.use(youtubePlugin) + } + }, + } + } +} diff --git a/plugins/plugin-md-power/src/node/utils/package.ts b/plugins/plugin-md-power/src/node/utils/package.ts new file mode 100644 index 00000000..5be4386c --- /dev/null +++ b/plugins/plugin-md-power/src/node/utils/package.ts @@ -0,0 +1,6 @@ +export type Awaitable = T | Promise + +export async function interopDefault(m: Awaitable): Promise { + const resolved = await m + return (resolved as any).default || resolved +} diff --git a/plugins/plugin-md-power/src/node/utils/parseRect.ts b/plugins/plugin-md-power/src/node/utils/parseRect.ts new file mode 100644 index 00000000..8e8c41a1 --- /dev/null +++ b/plugins/plugin-md-power/src/node/utils/parseRect.ts @@ -0,0 +1,6 @@ +export function parseRect(str: string, unit = 'px'): string { + if (Number.parseFloat(str) === Number(str)) + return `${str}${unit}` + + return str +} diff --git a/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts b/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts new file mode 100644 index 00000000..4465d4fb --- /dev/null +++ b/plugins/plugin-md-power/src/node/utils/resolveAttrs.ts @@ -0,0 +1,41 @@ +const RE_ATTR_VALUE = /(?:^|\s+)(?[\w\d-]+)(?:=\s*(?['"])(?.+?)\k)?(?:\s+|$)/ + +export function resolveAttrs(info: string): { + attrs: Record + rawAttrs: string +} { + info = info.trim() + + if (!info) + return { rawAttrs: '', attrs: {} } + + const attrs: Record = {} + const rawAttrs = info + + let matched: RegExpMatchArray | null + + // eslint-disable-next-line no-cond-assign + while (matched = info.match(RE_ATTR_VALUE)) { + const { attr, value } = matched.groups || {} + attrs[attr] = value ?? true + info = info.slice(matched[0].length) + } + + Object.keys(attrs).forEach((key) => { + let value = attrs[key] + value = typeof value === 'string' ? value.trim() : value + if (value === 'true') + value = true + else if (value === 'false') + value = false + + attrs[key] = value + + if (key.includes('-')) { + const _key = key.replace(/-(\w)/g, (_, c) => c.toUpperCase()) + attrs[_key] = value + } + }) + + return { attrs, rawAttrs } +} diff --git a/plugins/plugin-md-power/src/node/utils/timeToSeconds.ts b/plugins/plugin-md-power/src/node/utils/timeToSeconds.ts new file mode 100644 index 00000000..44538856 --- /dev/null +++ b/plugins/plugin-md-power/src/node/utils/timeToSeconds.ts @@ -0,0 +1,11 @@ +export function timeToSeconds(time: string): number { + if (!time) + return 0 + + if (Number.parseFloat(time) === Number(time)) + return Number(time) + + const [s, m, h] = time.split(':').reverse().map(n => Number(n) || 0) + + return s + m * 60 + h * 3600 +} diff --git a/plugins/plugin-md-power/src/shared/icons.ts b/plugins/plugin-md-power/src/shared/icons.ts new file mode 100644 index 00000000..93ef6e56 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/icons.ts @@ -0,0 +1,19 @@ +export interface IconsOptions { + /** + * The prefix of the icon className + * @default 'vp-mdi' + */ + prefix?: string + + /** + * The size of the icon + * @default '1em' + */ + size?: string | number + + /** + * The color of the icon + * @default 'currentColor' + */ + color?: string +} diff --git a/plugins/plugin-md-power/src/shared/index.ts b/plugins/plugin-md-power/src/shared/index.ts new file mode 100644 index 00000000..18925820 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/index.ts @@ -0,0 +1,3 @@ +export * from './caniuse.js' +export * from './pdf.js' +export * from './plugin.js' diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts new file mode 100644 index 00000000..bfac1a53 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/plugin.ts @@ -0,0 +1,11 @@ +import type { CanIUseOptions } from './caniuse.js' +import type { PDFOptions } from './pdf.js' +import type { IconsOptions } from './icons.js' + +export interface MarkdownPowerPluginOptions { + caniuse?: boolean | CanIUseOptions + pdf?: boolean | PDFOptions + icons?: boolean | IconsOptions + bilibili?: boolean + youtube?: boolean +} diff --git a/plugins/plugin-md-power/src/shared/size.ts b/plugins/plugin-md-power/src/shared/size.ts new file mode 100644 index 00000000..b679c8e4 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/size.ts @@ -0,0 +1,5 @@ +export interface SizeOptions { + width?: string + height?: string + ratio?: number | string +} diff --git a/plugins/plugin-md-power/src/shared/video.ts b/plugins/plugin-md-power/src/shared/video.ts new file mode 100644 index 00000000..f04fa371 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/video.ts @@ -0,0 +1,25 @@ +import type { SizeOptions } from './size' + +export interface VideoOptions { + bilibili?: boolean + youtube?: boolean +} + +export interface BilibiliTokenMeta extends SizeOptions { + title?: string + bvid?: string + aid?: string + cid?: string + autoplay?: boolean + time?: string | number + page?: number +} + +export interface YoutubeTokenMeta extends SizeOptions { + title?: string + id: string + autoplay?: boolean + loop?: boolean + start?: string | number + end?: string | number +} diff --git a/plugins/tsconfig.build.json b/plugins/tsconfig.build.json index a3ed6c22..d5791746 100644 --- a/plugins/tsconfig.build.json +++ b/plugins/tsconfig.build.json @@ -15,7 +15,8 @@ { "path": "./plugin-page-collection/tsconfig.build.json" }, { "path": "./plugin-shikiji/tsconfig.build.json" }, { "path": "./plugin-content-update/tsconfig.build.json" }, - { "path": "./plugin-search/tsconfig.build.json" } + { "path": "./plugin-search/tsconfig.build.json" }, + { "path": "./plugin-md-power/tsconfig.build.json" } ], "files": [] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6a69aac..f7ebbb62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: docs: dependencies: + '@iconify/json': + specifier: ^2.2.196 + version: 2.2.196 '@vuepress/bundler-vite': specifier: 2.0.0-rc.9 version: 2.0.0-rc.9(@types/node@20.9.1)(typescript@5.4.3) @@ -202,6 +205,37 @@ importers: specifier: 2.0.0-rc.9 version: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(typescript@5.4.3)(vue@3.4.21) + plugins/plugin-md-power: + dependencies: + '@iconify/utils': + specifier: ^2.1.22 + version: 2.1.22 + '@vueuse/core': + specifier: ^10.9.0 + version: 10.9.0(vue@3.4.21) + local-pkg: + specifier: ^0.5.0 + version: 0.5.0 + markdown-it-container: + specifier: ^4.0.0 + version: 4.0.0 + nanoid: + specifier: ^5.0.6 + version: 5.0.6 + vue: + specifier: ^3.4.21 + version: 3.4.21(typescript@5.4.3) + vuepress: + specifier: 2.0.0-rc.9 + version: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(typescript@5.4.3)(vue@3.4.21) + devDependencies: + '@iconify/json': + specifier: ^2.2.196 + version: 2.2.196 + '@types/markdown-it': + specifier: ^13.0.7 + version: 13.0.7 + plugins/plugin-netlify-functions: dependencies: '@iarna/toml': @@ -346,9 +380,6 @@ importers: '@vuepress-plume/plugin-blog-data': specifier: workspace:* version: link:../plugins/plugin-blog-data - '@vuepress-plume/plugin-caniuse': - specifier: workspace:* - version: link:../plugins/plugin-caniuse '@vuepress-plume/plugin-content-update': specifier: workspace:* version: link:../plugins/plugin-content-update @@ -439,6 +470,9 @@ importers: vuepress-plugin-md-enhance: specifier: 2.0.0-rc.32 version: 2.0.0-rc.32(katex@0.16.9)(markdown-it@14.1.0)(typescript@5.4.3)(vuepress@2.0.0-rc.9) + vuepress-plugin-md-power: + specifier: workspace:* + version: link:../plugins/plugin-md-power packages: @@ -586,6 +620,17 @@ packages: engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>= 8.6.0'} dev: true + /@antfu/install-pkg@0.1.1: + resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==} + dependencies: + execa: 5.1.1 + find-up: 5.0.0 + dev: false + + /@antfu/utils@0.7.7: + resolution: {integrity: sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==} + dev: false + /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -1705,8 +1750,27 @@ packages: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} dev: false + /@iconify/json@2.2.196: + resolution: {integrity: sha512-hRZ0pq77N+mkAbZvFi/pfsKcspA8PyGSASc6zQoq6n/RSLxb8xAgORatVHyDl0ow7shcS+dvyiZI8xmr6yI2WA==} + dependencies: + '@iconify/types': 2.0.0 + pathe: 1.1.2 + /@iconify/types@2.0.0: resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + /@iconify/utils@2.1.22: + resolution: {integrity: sha512-6UHVzTVXmvO8uS6xFF+L/QTSpTzA/JZxtgU+KYGFyDYMEObZ1bu/b5l+zNJjHy+0leWjHI+C0pXlzGvv3oXZMA==} + dependencies: + '@antfu/install-pkg': 0.1.1 + '@antfu/utils': 0.7.7 + '@iconify/types': 2.0.0 + debug: 4.3.4(supports-color@9.2.2) + kolorist: 1.8.0 + local-pkg: 0.5.0 + mlly: 1.6.1 + transitivePeerDependencies: + - supports-color dev: false /@iconify/vue@4.1.1(vue@3.4.21): @@ -3958,7 +4022,7 @@ packages: '@vue/shared': 3.4.21 entities: 4.5.0 estree-walker: 2.0.2 - source-map-js: 1.0.2 + source-map-js: 1.2.0 /@vue/compiler-dom@3.4.21: resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} @@ -3976,8 +4040,8 @@ packages: '@vue/shared': 3.4.21 estree-walker: 2.0.2 magic-string: 0.30.7 - postcss: 8.4.35 - source-map-js: 1.0.2 + postcss: 8.4.38 + source-map-js: 1.2.0 /@vue/compiler-ssr@3.4.21: resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} @@ -8360,7 +8424,6 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true /find-up@6.3.0: resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} @@ -10134,6 +10197,10 @@ packages: resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} dev: true + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: false + /kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} dev: false @@ -10320,7 +10387,6 @@ packages: dependencies: mlly: 1.6.1 pkg-types: 1.0.3 - dev: true /locate-path@2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} @@ -10341,7 +10407,6 @@ packages: engines: {node: '>=10'} dependencies: p-locate: 5.0.0 - dev: true /locate-path@7.2.0: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} @@ -11498,7 +11563,7 @@ packages: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: acorn: 8.10.0 - pathe: 1.1.1 + pathe: 1.1.2 pkg-types: 1.0.3 ufo: 1.3.1 @@ -12278,7 +12343,6 @@ packages: engines: {node: '>=10'} dependencies: p-limit: 3.1.0 - dev: true /p-locate@6.0.0: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} @@ -12754,6 +12818,7 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 + dev: true /postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} diff --git a/theme/package.json b/theme/package.json index 3ce436d9..028388ff 100644 --- a/theme/package.json +++ b/theme/package.json @@ -59,7 +59,6 @@ "@vuepress-plume/plugin-auto-frontmatter": "workspace:*", "@vuepress-plume/plugin-baidu-tongji": "workspace:*", "@vuepress-plume/plugin-blog-data": "workspace:*", - "@vuepress-plume/plugin-caniuse": "workspace:*", "@vuepress-plume/plugin-content-update": "workspace:*", "@vuepress-plume/plugin-copy-code": "workspace:*", "@vuepress-plume/plugin-iconify": "workspace:*", @@ -88,6 +87,7 @@ "nanoid": "^5.0.6", "vue": "^3.4.21", "vue-router": "4.3.0", - "vuepress-plugin-md-enhance": "2.0.0-rc.32" + "vuepress-plugin-md-enhance": "2.0.0-rc.32", + "vuepress-plugin-md-power": "workspace:*" } } diff --git a/theme/src/client/utils/color.ts b/theme/src/client/utils/color.ts deleted file mode 100644 index 0bf7541c..00000000 --- a/theme/src/client/utils/color.ts +++ /dev/null @@ -1,17 +0,0 @@ -const colorList = [ - 'var(--vp-c-brand-1)', - 'var(--vp-c-brand-2)', - 'var(--vp-c-green-1)', - 'var(--vp-c-green-2)', - 'var(--vp-c-green-3)', - 'var(--vp-c-yellow-1)', - 'var(--vp-c-yellow-2)', - 'var(--vp-c-yellow-3)', - 'var(--vp-c-red-1)', - 'var(--vp-c-red-2)', - 'var(--vp-c-red-3)', -] - -export function getRandomColor() { - return colorList[Math.floor(Math.random() * colorList.length)] -} diff --git a/theme/src/node/plugins.ts b/theme/src/node/plugins.ts index e4aec2fe..0df1e038 100644 --- a/theme/src/node/plugins.ts +++ b/theme/src/node/plugins.ts @@ -10,7 +10,6 @@ import { themeDataPlugin } from '@vuepress/plugin-theme-data' import { autoFrontmatterPlugin } from '@vuepress-plume/plugin-auto-frontmatter' import { baiduTongjiPlugin } from '@vuepress-plume/plugin-baidu-tongji' import { blogDataPlugin } from '@vuepress-plume/plugin-blog-data' -import { caniusePlugin } from '@vuepress-plume/plugin-caniuse' import { copyCodePlugin } from '@vuepress-plume/plugin-copy-code' import { iconifyPlugin } from '@vuepress-plume/plugin-iconify' import { notesDataPlugin } from '@vuepress-plume/plugin-notes-data' @@ -22,6 +21,7 @@ import { seoPlugin } from '@vuepress/plugin-seo' import { sitemapPlugin } from '@vuepress/plugin-sitemap' import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update' import { searchPlugin } from '@vuepress-plume/plugin-search' +import { markdownPowerPlugin } from 'vuepress-plugin-md-power' import type { PlumeThemeEncrypt, PlumeThemeLocaleOptions, @@ -139,9 +139,6 @@ export function setupPlugins( })) } - if (options.caniuse !== false) - plugins.push(caniusePlugin(options.caniuse || { mode: 'embed' })) - if (options.externalLinkIcon !== false) { plugins.push(externalLinkIconPlugin({ locales: Object.entries(localeOptions.locales || {}).reduce( @@ -205,6 +202,13 @@ export function setupPlugins( )) } + if (options.markdownPower !== false) { + plugins.push(markdownPowerPlugin({ + caniuse: options.caniuse, + ...options.markdownPower || {}, + })) + } + if (options.comment) plugins.push(commentPlugin(options.comment)) diff --git a/theme/src/shared/options/plugins.ts b/theme/src/shared/options/plugins.ts index 5b29ff22..33e5b9e6 100644 --- a/theme/src/shared/options/plugins.ts +++ b/theme/src/shared/options/plugins.ts @@ -2,18 +2,20 @@ import type { DocsearchOptions } from '@vuepress/plugin-docsearch' import type { SearchPluginOptions } from '@vuepress-plume/plugin-search' import type { AutoFrontmatterOptions } from '@vuepress-plume/plugin-auto-frontmatter' import type { BaiduTongjiOptions } from '@vuepress-plume/plugin-baidu-tongji' -import type { CanIUsePluginOptions } from '@vuepress-plume/plugin-caniuse' import type { CopyCodeOptions } from '@vuepress-plume/plugin-copy-code' import type { ShikiPluginOptions } from '@vuepress-plume/plugin-shikiji' import type { CommentPluginOptions } from '@vuepress/plugin-comment' import type { MarkdownEnhanceOptions } from 'vuepress-plugin-md-enhance' import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time' +import type { MarkdownPowerPluginOptions } from 'vuepress-plugin-md-power' export interface PlumeThemePluginOptions { /** + * @deprecated 迁移至 `plugin-md-power` 插件 + * * 是否启用 can-i-use 插件 */ - caniuse?: false | CanIUsePluginOptions + caniuse?: false /** * 是否启用 external-link-icon 插件 @@ -54,6 +56,8 @@ export interface PlumeThemePluginOptions { markdownEnhance?: false | MarkdownEnhanceOptions + markdownPower?: false | MarkdownPowerPluginOptions + comment?: false | CommentPluginOptions sitemap?: false diff --git a/tsconfig.json b/tsconfig.json index 9fdaa2c3..fb4db8ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,9 @@ "@vuepress-plume/*/client": ["./plugins/*/src/client/index.ts"], "vuepress-plugin-netlify-functions": [ "./plugins/plugin-netlify-functions/src/node/index.ts" + ], + "vuepress-plugin-md-power": [ + "./plugins/plugin-md-power/src/node/index.ts" ] }, "types": ["webpack-env", "vite/client"]