From 53dfcb83b13c8b11caa3df1646d12c5779395f21 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Thu, 28 Mar 2024 15:38:32 +0800 Subject: [PATCH] feat(plugin-md-power): add `@[caniuse](feature)` syntax supported --- plugins/plugin-md-power/LICENSE | 21 ++ plugins/plugin-md-power/package.json | 69 +++++++ plugins/plugin-md-power/src/node/caniuse.ts | 179 ++++++++++++++++++ plugins/plugin-md-power/src/shared/caniuse.ts | 20 ++ plugins/plugin-md-power/tsconfig.build.json | 8 + 5 files changed, 297 insertions(+) create mode 100644 plugins/plugin-md-power/LICENSE create mode 100644 plugins/plugin-md-power/package.json create mode 100644 plugins/plugin-md-power/src/node/caniuse.ts create mode 100644 plugins/plugin-md-power/src/shared/caniuse.ts create mode 100644 plugins/plugin-md-power/tsconfig.build.json diff --git a/plugins/plugin-md-power/LICENSE b/plugins/plugin-md-power/LICENSE new file mode 100644 index 00000000..9f677c90 --- /dev/null +++ b/plugins/plugin-md-power/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (C) 2021 - PRESENT by pengzhanbo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/plugin-md-power/package.json b/plugins/plugin-md-power/package.json new file mode 100644 index 00000000..9ee37fc5 --- /dev/null +++ b/plugins/plugin-md-power/package.json @@ -0,0 +1,69 @@ +{ + "name": "@vuepress-plume/plugin-md-power", + "type": "module", + "version": "1.0.0-rc.47", + "description": "The Plugin for VuePres 2 - markdown power", + "author": "pengzhanbo ", + "license": "MIT", + "homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git", + "directory": "plugins/plugin-md-power" + }, + "bugs": { + "url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues" + }, + "exports": { + ".": { + "types": "./lib/node/index.d.ts", + "import": "./lib/node/index.js" + }, + "./client": { + "types": "./lib/client/index.d.ts", + "import": "./lib/client/index.js" + }, + "./package.json": "./package.json" + }, + "main": "lib/node/index.js", + "types": "./lib/node/index.d.ts", + "files": [ + "lib" + ], + "scripts": { + "build": "pnpm run copy && pnpm run ts", + "clean": "rimraf --glob ./lib ./*.tsbuildinfo", + "copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib", + "ts": "tsc -b tsconfig.build.json" + }, + "peerDependencies": { + "@iconify/json": "^2", + "vuepress": "2.0.0-rc.9" + }, + "peerDependenciesMeta": { + "@iconify/json": { + "optional": true + } + }, + "dependencies": { + "@iconify/utils": "^2.1.22", + "@vueuse/core": "^10.9.0", + "local-pkg": "^0.5.0", + "markdown-it-container": "^4.0.0", + "nanoid": "^5.0.6", + "vue": "^3.4.21" + }, + "devDependencies": { + "@iconify/json": "^2.2.196", + "@types/markdown-it": "^13.0.7" + }, + "publishConfig": { + "access": "public" + }, + "keyword": [ + "VuePress", + "vuepress plugin", + "markdown power", + "vuepress-plugin-md-power" + ] +} diff --git a/plugins/plugin-md-power/src/node/caniuse.ts b/plugins/plugin-md-power/src/node/caniuse.ts new file mode 100644 index 00000000..41cc85f6 --- /dev/null +++ b/plugins/plugin-md-power/src/node/caniuse.ts @@ -0,0 +1,179 @@ +/** + * @[caniuse embed{1,2,3,4}](feature_name) + * @[caniuse image](feature_name) + */ +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' + +// @[caniuse]() +const minLength = 12 + +// char codes of '@[caniuse' +const START_CODES = [64, 91, 99, 97, 110, 105, 117, 115, 101] + +// regexp to match the import syntax +const SYNTAX_RE = /^@\[caniuse(?:\s*?(embed|image)?(?:{([0-9,\-]*?)})?)\]\(([^)]*)\)/ + +function createCanIUseRuleBlock(defaultMode: CanIUseMode): RuleBlock { + return (state, startLine, endLine, silent) => { + const pos = state.bMarks[startLine] + state.tShift[startLine] + const max = state.eMarks[startLine] + + // return false if the length is shorter than min length + if (pos + minLength > max) + return false + + // check if it's matched the start + for (let i = 0; i < START_CODES.length; i += 1) { + if (state.src.charCodeAt(pos + i) !== START_CODES[i]) + return false + } + + // check if it's matched the syntax + const match = state.src.slice(pos, max).match(SYNTAX_RE) + if (!match) + return false + + // return true as we have matched the syntax + if (silent) + return true + + const [, mode, versions = '', feature] = match + + const meta: CanIUseTokenMeta = { + feature, + mode: (mode as CanIUseMode) || defaultMode, + versions, + } + + const token = state.push('caniuse', '', 0) + + token.meta = meta + token.map = [startLine, startLine + 1] + token.info = mode || defaultMode + + state.line = startLine + 1 + + return true + } +} + +function resolveCanIUse({ feature, mode, versions }: CanIUseTokenMeta): string { + if (!feature) + return '' + + if (mode === 'image') { + const link = 'https://caniuse.bitsofco.de/image/' + const alt = `Data on support for the ${feature} feature across the major browsers from caniuse.com` + return `

+ + + ${alt} +

` + } + + const periods = resolveVersions(versions) + const accessible = 'false' + const image = 'none' + const url = 'https://caniuse.bitsofco.de/embed/index.html' + const src = `${url}?feat=${feature}&periods=${periods}&accessible-colours=${accessible}&image-base=${image}` + + return `
` +} + +function resolveVersions(versions: string): string { + if (!versions) + return 'future_1,current,past_1,past_2' + + const list = versions + .split(',') + .map(v => Number(v.trim())) + .filter(v => !Number.isNaN(v) && v >= -5 && v <= 3) + + list.push(0) + + const uniq = [...new Set(list)].sort((a, b) => b - a) + const result: string[] = [] + uniq.forEach((v) => { + if (v < 0) + result.push(`past_${Math.abs(v)}`) + if (v === 0) + result.push('current') + if (v > 0) + result.push(`future_${v}`) + }) + return result.join(',') +} + +/** + * @example + * ```md + * @[caniuse](feature_name) + * ``` + */ +export const caniusePlugin: PluginWithOptions = ( + md, + { mode = 'embed' }: CanIUseOptions = {}, +): void => { + md.block.ruler.before( + 'import_code', + 'caniuse', + createCanIUseRuleBlock(mode), + { + alt: ['paragraph', 'reference', 'blockquote', 'list'], + }, + ) + + md.renderer.rules.caniuse = (tokens, index) => { + const token = tokens[index] + + const content = resolveCanIUse(token.meta) + token.content = content + + return content + } +} + +/** + * @deprecated use caniuse plugin + * + * 兼容旧语法 + * @example + * ```md + * :::caniuse + * ::: + * ``` + */ +export function legacyCaniuse( + md: Markdown, + { mode = 'embed' }: CanIUseOptions = {}, +): void { + const modeMap: CanIUseMode[] = ['image', 'embed'] + const isMode = (mode: CanIUseMode): boolean => modeMap.includes(mode) + + mode = isMode(mode) ? mode : modeMap[0] + const type = 'caniuse' + const validateReg = new RegExp(`^${type}\\s+(.*)$`) + + const validate = (info: string): boolean => { + return validateReg.test(info.trim()) + } + + const render = (tokens: Token[], index: number): string => { + const token = tokens[index] + if (token.nesting === 1) { + const info = token.info.trim().slice(type.length).trim() || '' + const feature = info.split(/\s+/)[0] + const versions = info.match(/\{(.*)\}/)?.[1] || '' + return feature ? resolveCanIUse({ feature, mode, versions }) : '' + } + else { + return '' + } + } + + md.use(container, type, { validate, render }) +} diff --git a/plugins/plugin-md-power/src/shared/caniuse.ts b/plugins/plugin-md-power/src/shared/caniuse.ts new file mode 100644 index 00000000..8c746dd6 --- /dev/null +++ b/plugins/plugin-md-power/src/shared/caniuse.ts @@ -0,0 +1,20 @@ +export type CanIUseMode = 'embed' | 'image' + +export interface CanIUseTokenMeta { + feature: string + mode: CanIUseMode + versions: string +} + +export interface CanIUseOptions { + /** + * 嵌入模式 + * + * embed 通过iframe嵌入,提供可交互视图 + * + * image 通过图片嵌入,静态 + * + * @default 'embed' + */ + mode?: CanIUseMode +} diff --git a/plugins/plugin-md-power/tsconfig.build.json b/plugins/plugin-md-power/tsconfig.build.json new file mode 100644 index 00000000..6bf67375 --- /dev/null +++ b/plugins/plugin-md-power/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src"] +}