From 53dfcb83b13c8b11caa3df1646d12c5779395f21 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Thu, 28 Mar 2024 15:38:32 +0800 Subject: [PATCH 01/11] 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"] +} From cd7485c246291279dbf4a3b119073fb4b012f2f4 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Fri, 29 Mar 2024 00:35:17 +0800 Subject: [PATCH 02/11] feat(plugin-md-power): add `@[pdf](url)` syntax supported --- .../src/client/components/PDFViewer.vue | 39 +++++++ .../src/client/composables/pdf.ts | 107 ++++++++++++++++++ .../plugin-md-power/src/node/features/pdf.ts | 97 ++++++++++++++++ plugins/plugin-md-power/src/shared/pdf.ts | 18 +++ 4 files changed, 261 insertions(+) create mode 100644 plugins/plugin-md-power/src/client/components/PDFViewer.vue create mode 100644 plugins/plugin-md-power/src/client/composables/pdf.ts create mode 100644 plugins/plugin-md-power/src/node/features/pdf.ts create mode 100644 plugins/plugin-md-power/src/shared/pdf.ts diff --git a/plugins/plugin-md-power/src/client/components/PDFViewer.vue b/plugins/plugin-md-power/src/client/components/PDFViewer.vue new file mode 100644 index 00000000..edaecc8e --- /dev/null +++ b/plugins/plugin-md-power/src/client/components/PDFViewer.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/plugins/plugin-md-power/src/client/composables/pdf.ts b/plugins/plugin-md-power/src/client/composables/pdf.ts new file mode 100644 index 00000000..9a9a9669 --- /dev/null +++ b/plugins/plugin-md-power/src/client/composables/pdf.ts @@ -0,0 +1,107 @@ +import { ensureEndingSlash } from 'vuepress/shared' +import { withBase } from 'vuepress/client' +import type { PDFEmbedType, PDFTokenMeta } from '../../shared/pdf.js' +import { normalizeLink } from '../utils/link.js' +import { pluginOptions } from '../options.js' +import { checkIsMobile, checkIsSafari, checkIsiPad } from '../utils/is.js' + +function queryStringify(options: PDFTokenMeta): string { + const { page, noToolbar, zoom } = options + const params = [ + `page=${page}`, + `toolbar=${noToolbar ? 0 : 1}`, + `zoom=${zoom}`, + ] + + let queryString = params.join('&') + if (queryString) + queryString = `#${queryString}` + + return queryString +} + +export function renderPDF( + el: HTMLElement, + url: string, + embedType: PDFEmbedType, + options: PDFTokenMeta, +): void { + if (!pluginOptions.pdf) + return + url = normalizeLink(url) + const pdfOptions = pluginOptions.pdf === true ? {} : pluginOptions.pdf + const pdfjsUrl = pdfOptions.pdfjsUrl + ? `${ensureEndingSlash(withBase(pdfOptions.pdfjsUrl))}web/viewer.html` + : '' + const queryString = queryStringify(options) + + const source = embedType === 'pdfjs' + ? `${pdfjsUrl}?file=${encodeURIComponent(url)}${queryString}` + : `${url}${queryString}` + + const tagName = embedType === 'pdfjs' || embedType === 'iframe' + ? 'iframe' + : 'embed' + + el.innerHTML = '' + const pdf = document.createElement(tagName) + + pdf.className = 'pdf-viewer'; + (pdf as any).type = 'application/pdf' + pdf.title = options.title || 'PDF Viewer' + pdf.src = source + + if (pdf instanceof HTMLIFrameElement) + pdf.allow = 'fullscreen' + + el.appendChild(pdf) +} + +export function usePDF( + el: HTMLElement, + url: string, + options: PDFTokenMeta, +): void { + if (typeof window === 'undefined' || !window?.navigator?.userAgent) + return + + const { navigator } = window + const { userAgent } = navigator + + const isModernBrowser = typeof window.Promise === 'function' + + // Quick test for mobile devices. + const isMobileDevice = checkIsiPad(userAgent) || checkIsMobile(userAgent) + + // Safari desktop requires special handling + const isSafariDesktop = !isMobileDevice && checkIsSafari(userAgent) + + const isFirefoxWithPDFJS + = !isMobileDevice + && /firefox/iu.test(userAgent) + && userAgent.split('rv:').length > 1 + ? Number.parseInt(userAgent.split('rv:')[1].split('.')[0], 10) > 18 + : false + + // Determines whether PDF support is available + const supportsPDFs + // As of Sept 2020 no mobile browsers properly support PDF embeds + = !isMobileDevice + // We're moving into the age of MIME-less browsers. They mostly all support PDF rendering without plugins. + && (isModernBrowser + // Modern versions of Firefox come bundled with PDFJS + || isFirefoxWithPDFJS) + + if (!url) + return + + if (supportsPDFs || !isMobileDevice) { + const embedType = isSafariDesktop ? 'iframe' : 'embed' + return renderPDF(el, url, embedType, options) + } + + if (typeof pluginOptions.pdf === 'object' && pluginOptions.pdf.pdfjsUrl) + return renderPDF(el, url, 'pdfjs', options) + + el.innerHTML = `

This browser does not support embedding PDFs. Please download the PDF to view it: Download PDF

` +} diff --git a/plugins/plugin-md-power/src/node/features/pdf.ts b/plugins/plugin-md-power/src/node/features/pdf.ts new file mode 100644 index 00000000..a2a73c55 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/pdf.ts @@ -0,0 +1,97 @@ +/** + * @[pdf](/xxx) + * @[pdf 1](/xxx) + * @[pdf 1 no-toolbar width="100%" height="600px" zoom="1" ratio="1:1"](/xxx) + */ +import { path } from 'vuepress/utils' +import type { PluginWithOptions, Token } from 'markdown-it' +import type { RuleBlock } from 'markdown-it/lib/parser_block.js' +import type { PDFTokenMeta } from '../../shared/pdf.js' +import { resolveAttrs } from '../utils/resolveAttrs.js' +import { parseRect } from '../utils/parseRect.js' + +// @[pdf]() +const MIN_LENGTH = 8 + +// char codes of `@[pdf` +const START_CODES = [64, 91, 112, 100, 102] + +// regexp to match the import syntax +const SYNTAX_RE = /^@\[pdf(?:\s+(\d+))?(?:\s+([^]*?))?\]\(([^)]*?)\)/ + +function createPDFRuleBlock(): 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 + MIN_LENGTH > 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 [, page, info = '', src] = match + + const { attrs } = resolveAttrs(info) + + const meta: PDFTokenMeta = { + src, + page: +page || 1, + noToolbar: Boolean(attrs.noToolbar ?? false), + zoom: +attrs.zoom || 1, + width: attrs.width ? parseRect(attrs.width) : '100%', + height: attrs.height ? parseRect(attrs.height) : '', + ratio: attrs.ratio ? parseRect(attrs.ratio) : '', + title: path.basename(src || ''), + } + + const token = state.push('pdf', '', 0) + + token.meta = meta + token.map = [startLine, startLine + 1] + token.info = info + + state.line = startLine + 1 + + return true + } +} + +function resolvePDF(meta: PDFTokenMeta): string { + const { title, src, page, noToolbar, width, height, ratio, zoom } = meta + + return `` +} + +export const pdfPlugin: PluginWithOptions = (md) => { + md.block.ruler.before( + 'import_code', + 'pdf', + createPDFRuleBlock(), + { + alt: ['paragraph', 'reference', 'blockquote', 'list'], + }, + ) + + md.renderer.rules.pdf = (tokens, index) => { + const token = tokens[index] + + const content = resolvePDF(token.meta) + token.content = content + + return content + } +} diff --git a/plugins/plugin-md-power/src/shared/pdf.ts b/plugins/plugin-md-power/src/shared/pdf.ts new file mode 100644 index 00000000..001ca69d --- /dev/null +++ b/plugins/plugin-md-power/src/shared/pdf.ts @@ -0,0 +1,18 @@ +import type { SizeOptions } from './size' + +export type PDFEmbedType = 'iframe' | 'embed' | 'pdfjs' + +export interface PDFTokenMeta extends SizeOptions { + page?: number + noToolbar?: boolean + zoom?: number + src?: string + title?: string +} + +export interface PDFOptions { + /** + * pdfjs url + */ + pdfjsUrl?: string +} From 0715ee97bd6632344ab22f99f38679557686a663 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Fri, 29 Mar 2024 00:37:03 +0800 Subject: [PATCH 03/11] feat(plugin-md-power): add `@[bilibili](url)` syntax supported --- .../src/client/components/Bilibili.vue | 45 +++++++ .../src/node/features/video/bilibili.ts | 115 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 plugins/plugin-md-power/src/client/components/Bilibili.vue create mode 100644 plugins/plugin-md-power/src/node/features/video/bilibili.ts diff --git a/plugins/plugin-md-power/src/client/components/Bilibili.vue b/plugins/plugin-md-power/src/client/components/Bilibili.vue new file mode 100644 index 00000000..f72512c1 --- /dev/null +++ b/plugins/plugin-md-power/src/client/components/Bilibili.vue @@ -0,0 +1,45 @@ + + +