diff --git a/plugins/plugin-md-power/src/node/features/imageSize.ts b/plugins/plugin-md-power/src/node/features/imageSize.ts
index b1aca66f..ebfdfb7f 100644
--- a/plugins/plugin-md-power/src/node/features/imageSize.ts
+++ b/plugins/plugin-md-power/src/node/features/imageSize.ts
@@ -1,14 +1,46 @@
+import { URL } from 'node:url'
+import http from 'node:https'
+import { Buffer } from 'node:buffer'
import type { App } from 'vuepress'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
import { isLinkExternal } from '@vuepress/helper'
-import { fs, path } from '@vuepress/utils'
+import { fs, path } from 'vuepress/utils'
+import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
import imageSize from 'image-size'
+import { resolveAttrs } from '../utils/resolveAttrs.js'
-export function imageSizePlugin(app: App, md: Markdown): void {
- if (!app.env.isBuild)
+interface ImgSize {
+ width: number
+ height: number
+}
+
+const REG_IMG = /!\[.*?\]\(.*?\)/g
+const REG_IMG_TAG = /
/g
+const REG_IMG_TAG_SRC = /src(?:set)?=(['"])(.+?)\1/g
+const BADGE_LIST = [
+ 'https://img.shields.io',
+ 'https://badge.fury.io',
+ 'https://badgen.net',
+ 'https://forthebadge.com',
+ 'https://vercel.com/button',
+]
+
+export async function imageSizePlugin(
+ app: App,
+ md: Markdown,
+ type: boolean | 'local' | 'all' = false,
+) {
+ if (!app.env.isBuild || !type)
return
- const cache = new Map()
+ const cache = new Map()
+
+ if (type === 'all') {
+ try {
+ await scanRemoteImageSize(app, cache)
+ }
+ catch {}
+ }
const imageRule = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env: MarkdownEnv, self) => {
@@ -17,25 +49,77 @@ export function imageSizePlugin(app: App, md: Markdown): void {
const token = tokens[idx]
const src = token.attrGet('src')
-
- if (!src || src.startsWith('data:') || isLinkExternal(src))
- return imageRule(tokens, idx, options, env, self)
-
const width = token.attrGet('width')
const height = token.attrGet('height')
+ const size = resolveSize(src, width, height, env)
+
+ if (size) {
+ token.attrSet('width', `${size.width}`)
+ token.attrSet('height', `${size.height}`)
+ }
+
+ return imageRule(tokens, idx, options, env, self)
+ }
+
+ const rawHtmlBlockRule = md.renderer.rules.html_block!
+ const rawHtmlInlineRule = md.renderer.rules.html_inline!
+ md.renderer.rules.html_block = createHtmlRule(rawHtmlBlockRule)
+ md.renderer.rules.html_inline = createHtmlRule(rawHtmlInlineRule)
+
+ function createHtmlRule(rawHtmlRule: RenderRule): RenderRule {
+ return (tokens, idx, options, env, self) => {
+ const token = tokens[idx]
+ token.content = token.content.replace(REG_IMG_TAG, (raw, info) => {
+ const { attrs } = resolveAttrs(info)
+ const src = attrs.src || attrs.srcset
+ const size = resolveSize(src, attrs.width, attrs.height, env)
+
+ if (!size)
+ return raw
+
+ attrs.width = size.width
+ attrs.height = size.height
+
+ const imgAttrs = Object.entries(attrs)
+ .map(([key, value]) => typeof value === 'boolean' ? key : `${key}="${value}"`)
+ .join(' ')
+
+ return `
`
+ })
+ return rawHtmlRule(tokens, idx, options, env, self)
+ }
+ }
+
+ function resolveSize(
+ src: string | null | undefined,
+ width: string | null | undefined,
+ height: string | null | undefined,
+ env: MarkdownEnv,
+ ): false | ImgSize {
+ if (!src || src.startsWith('data:'))
+ return false
+
if (width && height)
- return imageRule(tokens, idx, options, env, self)
+ return false
- const filepath = resolveImageUrl(src, env)
+ const isExternal = isLinkExternal(src)
+ const filepath = isExternal ? src : resolveImageUrl(src, env, app)
- if (!cache.has(filepath)) {
- if (!filepath || !fs.existsSync(filepath))
- return imageRule(tokens, idx, options, env, self)
+ if (isExternal) {
+ if (!cache.has(filepath))
+ return false
+ }
+ else {
+ if (!cache.has(filepath)) {
+ if (!fs.existsSync(filepath))
+ return false
- const { width: w, height: h } = imageSize(filepath)
- if (!w || !h)
- return imageRule(tokens, idx, options, env, self)
- cache.set(filepath, { width: w, height: h })
+ const { width: w, height: h } = imageSize(filepath)
+ if (!w || !h)
+ return false
+
+ cache.set(filepath, { width: w, height: h })
+ }
}
const { width: originalWidth, height: originalHeight } = cache.get(filepath)!
@@ -44,32 +128,101 @@ export function imageSizePlugin(app: App, md: Markdown): void {
if (width && !height) {
const w = Number.parseInt(width, 10)
- token.attrSet('width', `${w}`)
- token.attrSet('height', `${Math.round(w / ratio)}`)
+ return { width: w, height: Math.round(w / ratio) }
}
else if (height && !width) {
const h = Number.parseInt(height, 10)
- token.attrSet('width', `${Math.round(h * ratio)}`)
- token.attrSet('height', `${h}`)
+ return { width: Math.round(h * ratio), height: h }
}
else {
- token.attrSet('width', `${originalWidth}`)
- token.attrSet('height', `${originalHeight}`)
+ return { width: originalWidth, height: originalHeight }
}
-
- return imageRule(tokens, idx, options, env, self)
- }
-
- function resolveImageUrl(src: string, env: MarkdownEnv): string {
- if (src[0] === '/')
- return app.dir.public(src.slice(1))
-
- if (env.filePathRelative)
- return app.dir.source(path.join(path.dirname(env.filePathRelative), src))
-
- if (env.filePath)
- return path.resolve(env.filePath, src)
-
- return ''
}
}
+
+function resolveImageUrl(src: string, env: MarkdownEnv, app: App): string {
+ if (src[0] === '/')
+ return app.dir.public(src.slice(1))
+
+ if (env.filePathRelative && src[0] === '.')
+ return app.dir.source(path.join(path.dirname(env.filePathRelative), src))
+
+ // fallback
+ if (env.filePath && (src[0] === '.' || src[0] === '/'))
+ return path.resolve(env.filePath, src)
+
+ return ''
+}
+
+export async function scanRemoteImageSize(
+ app: App,
+ cache: Map,
+) {
+ if (!app.env.isBuild)
+ return
+ const cwd = app.dir.source()
+ const files = await fs.readdir(cwd, { recursive: true })
+ const imgList: string[] = []
+ for (const file of files) {
+ const filepath = path.join(cwd, file)
+ if (
+ (await (fs.stat(filepath))).isFile()
+ && !filepath.includes('.vuepress')
+ && !filepath.includes('node_modules')
+ && filepath.endsWith('.md')
+ ) {
+ const content = await fs.readFile(filepath, 'utf-8')
+ // [xx](xxx)
+ const syntaxMatched = content.match(REG_IMG) ?? []
+ for (const img of syntaxMatched) {
+ const src = img.slice(img.indexOf('](') + 2, -1)
+ addList(src.split(/\s+/)[0])
+ }
+ //
or
+ const tagMatched = content.match(REG_IMG_TAG) ?? []
+ for (const img of tagMatched) {
+ const src = img.match(REG_IMG_TAG_SRC)?.[2] ?? ''
+ addList(src)
+ }
+ }
+ }
+
+ function addList(src: string) {
+ if (src && isLinkExternal(src)
+ && !imgList.includes(src)
+ && !BADGE_LIST.some(badge => src.startsWith(badge))
+ ) {
+ imgList.push(src)
+ }
+ }
+
+ await Promise.all(imgList.map(async (src) => {
+ if (!cache.has(src)) {
+ const { width, height } = await fetchImageSize(src)
+ if (width && height)
+ cache.set(src, { width, height })
+ }
+ }))
+}
+
+function fetchImageSize(src: string): Promise {
+ const link = new URL(src)
+
+ return new Promise((resolve) => {
+ http.get(link, async (stream) => {
+ const chunks: any[] = []
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ try {
+ const { width, height } = imageSize(Buffer.concat(chunks))
+ if (width && height) {
+ return resolve({ width, height })
+ }
+ }
+ catch {}
+ }
+ const { width, height } = imageSize(Buffer.concat(chunks))
+ resolve({ width: width!, height: height! })
+ }).on('error', () => resolve({ width: 0, height: 0 }))
+ })
+}
diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts
index 17ce150a..b3633c8c 100644
--- a/plugins/plugin-md-power/src/node/plugin.ts
+++ b/plugins/plugin-md-power/src/node/plugin.ts
@@ -39,7 +39,7 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P
},
extendsMarkdown: async (md: MarkdownIt, app) => {
- imageSizePlugin(app, md)
+ await imageSizePlugin(app, md, options.imageSize)
if (options.caniuse) {
const caniuse = options.caniuse === true ? {} : options.caniuse
diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts
index a9c5367d..b4ad7350 100644
--- a/plugins/plugin-md-power/src/shared/plugin.ts
+++ b/plugins/plugin-md-power/src/shared/plugin.ts
@@ -5,28 +5,121 @@ import type { PlotOptions } from './plot.js'
import type { ReplOptions } from './repl.js'
export interface MarkdownPowerPluginOptions {
+ /**
+ * 是否启用 PDF 嵌入语法
+ *
+ * `@[pdf](pdf_url)`
+ *
+ * @default false
+ */
pdf?: boolean | PDFOptions
// new syntax
+ /**
+ * 是否启用 iconify 图标嵌入语法
+ *
+ * `:[collect:icon_name]:`
+ *
+ * @default false
+ */
icons?: boolean | IconsOptions
+ /**
+ * 是否启用 隐秘文本 语法
+ *
+ * `!!plot_content!!`
+ *
+ * @default false
+ */
plot?: boolean | PlotOptions
// video embed
+ /**
+ * 是否启用 bilibili 视频嵌入
+ *
+ * `@[bilibili](bid)`
+ *
+ * @default false
+ */
bilibili?: boolean
+ /**
+ * 是否启用 youtube 视频嵌入
+ *
+ * `@[youtube](video_id)`
+ *
+ * @default false
+ */
youtube?: boolean
// code embed
+ /**
+ * 是否启用 codepen 嵌入
+ *
+ * `@[codepen](pen_id)`
+ *
+ * @default false
+ */
codepen?: boolean
/**
* @deprecated
*/
replit?: boolean
+ /**
+ * 是否启用 codeSandbox 嵌入
+ *
+ * `@[codesandbox](codesandbox_id)`
+ *
+ * @default false
+ */
codeSandbox?: boolean
+ /**
+ * 是否启用 jsfiddle 嵌入
+ *
+ * `@[jsfiddle](jsfiddle_id)`
+ *
+ * @default false
+ */
jsfiddle?: boolean
// container
+ /**
+ * 是否启用 REPL 容器语法
+ *
+ * @default false
+ */
repl?: false | ReplOptions
+ /**
+ * 是否启用 文件树 容器语法
+ *
+ * @default false
+ */
fileTree?: boolean
+ /**
+ * 是否启用 caniuse 嵌入语法
+ *
+ * `@[caniuse](feature_name)`
+ *
+ * @default false
+ */
caniuse?: boolean | CanIUseOptions
+
+ // enhance
+ /**
+ * 是否启用 自动填充 图片宽高属性
+ *
+ * __请注意,无论是否启用,该功能仅在构建生产包时生效__
+ *
+ * - 如果为 `true` ,等同于 `'local'`
+ * - 如果为 `local`,则仅对本地图片 添加 width 和 height
+ * - 如果为 `all`,则对所有图片(即包括 本地 和 远程) 添加 width 和 height
+ *
+ * 图片在加载过程中如果比较慢,从加载到完成的过程会导致页面布局不稳定,导致内容闪烁等。
+ * 此功能通过给图片添加 `width` 和 `height` 属性来解决该问题。
+ *
+ * 请谨慎使用 `all` 选项,该选项会在构建阶段发起网络请求,尝试加载远程图片以获取图片尺寸信息,
+ * 这可能会导致 构建时间变得更长(幸运的是获取尺寸信息只需要加载图片 几 KB 的数据包,因此耗时不会过长)
+ *
+ * @default false
+ */
+ imageSize?: boolean | 'local' | 'all'
}