diff --git a/packages/md-enhance/__tests__/unit/imageSize.spec.ts b/packages/md-enhance/__tests__/unit/imageSize.spec.ts new file mode 100644 index 000000000..f70af0c62 --- /dev/null +++ b/packages/md-enhance/__tests__/unit/imageSize.spec.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import MarkdownIt from "markdown-it"; +import { imageSize } from "../../src/node/markdown-it/imageSize"; + +describe("Image Size", () => { + const markdownIt = MarkdownIt({ linkify: true }).use(imageSize); + + it("Shoud render", () => { + expect(markdownIt.render(`![image](/logo.svg)`)).toEqual( + '

image

\n' + ); + + expect(markdownIt.render(`![image](/logo.svg =200x300)`)).toEqual( + '

image

\n' + ); + + expect(markdownIt.render(`![image](/logo.svg =200x)`)).toEqual( + '

image

\n' + ); + + expect(markdownIt.render(`![image](/logo.svg =x300)`)).toEqual( + '

image

\n' + ); + }); + + it("Shoud not render", () => { + expect(markdownIt.render(`![image](/logo.svg =abcxdef)`)).toEqual( + "

![image](/logo.svg =abcxdef)

\n" + ); + + expect(markdownIt.render(`![image](/logo.svg =abcx100)`)).toEqual( + "

![image](/logo.svg =abcx100)

\n" + ); + + expect(markdownIt.render(`![image](/logo.svg =200xdef)`)).toEqual( + "

![image](/logo.svg =200xdef)

\n" + ); + + expect(markdownIt.render(`![image](/logo.svg =12ax300)`)).toEqual( + "

![image](/logo.svg =12ax300)

\n" + ); + + expect(markdownIt.render(`![image](/logo.svg =200x12a)`)).toEqual( + "

![image](/logo.svg =200x12a)

\n" + ); + + expect(markdownIt.render(`![image](/logo.svg =200X300)`)).toEqual( + "

![image](/logo.svg =200X300)

\n" + ); + + expect(markdownIt.render(`![image](/logo.svg =200×300)`)).toEqual( + "

![image](/logo.svg =200×300)

\n" + ); + }); + + it("With title", () => { + expect(markdownIt.render(`![image](/logo.svg "title")`)).toEqual( + '

image

\n' + ); + + expect(markdownIt.render(`![image](/logo.svg "title" =200x300)`)).toEqual( + '

image

\n' + ); + + expect(markdownIt.render(`![image](/logo.svg "title" =200x)`)).toEqual( + '

image

\n' + ); + + expect(markdownIt.render(`![image](/logo.svg "title" =x300)`)).toEqual( + '

image

\n' + ); + }); +}); diff --git a/packages/md-enhance/src/node/markdown-it/codeGroup.ts b/packages/md-enhance/src/node/markdown-it/codeGroup.ts new file mode 100644 index 000000000..5edf8a58a --- /dev/null +++ b/packages/md-enhance/src/node/markdown-it/codeGroup.ts @@ -0,0 +1,25 @@ +import { container } from "../markdown-it"; + +import type { PluginSimple } from "markdown-it"; + +export const codeGroup: PluginSimple = (md) => { + md.use(container, { + name: "code-group", + openRender: () => { + return `\n`; + }, + closeRender: () => "\n", + }); + + md.use(container, { + name: "code-group-item", + openRender: (info: string): string => { + const isActive = info.split(":").pop() === "active"; + + return `\n`; + }, + closeRender: () => "\n", + }); +}; diff --git a/packages/md-enhance/src/node/markdown-it/imageSize.ts b/packages/md-enhance/src/node/markdown-it/imageSize.ts new file mode 100644 index 000000000..62dc78e93 --- /dev/null +++ b/packages/md-enhance/src/node/markdown-it/imageSize.ts @@ -0,0 +1,266 @@ +import type { PluginSimple } from "markdown-it"; +import type { RuleInline } from "markdown-it/lib/parser_inline"; +import type { default as Token } from "markdown-it/lib/token"; + +interface MarkdownReference { + href: string; + title?: string; +} + +export interface ImageEnv { + references?: Record; +} + +// Parse image size +// +const parseNextNumber = ( + str: string, + pos: number, + max: number +): { ok: boolean; pos: number; value: string } => { + let code; + const start = pos; + const result = { + ok: false, + pos: pos, + value: "", + }; + + code = str.charCodeAt(pos); + + while ( + (pos < max && code >= 0x30 /* 0 */ && code <= 0x39) /* 9 */ || + code === 0x25 /* % */ + ) { + code = str.charCodeAt(++pos); + } + + result.ok = true; + result.pos = pos; + result.value = str.slice(start, pos); + + return result; +}; + +const parseImageSize = ( + str: string, + pos: number, + max: number +): { ok: boolean; pos: number; width: string; height: string } => { + const result = { + ok: false, + pos: 0, + width: "", + height: "", + }; + + if (pos >= max) return result; + + if (str.charAt(pos) !== "=") return result; + + pos += 1; + + // size must follow = without any white spaces as follows + // (1) =300x200 + // (2) =300x + // (3) =x200 + const code = str.charCodeAt(pos); + + if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) + return result; + + // parse width + const width = parseNextNumber(str, pos, max); + + pos = width.pos; + + // next charactor must be 'x' + if (str.charAt(pos) !== "x") return result; + + pos += 1; + + // parse height + const height = parseNextNumber(str, pos, max); + + pos = height.pos; + + result.width = width.value; + result.height = height.value; + result.pos = pos; + result.ok = true; + + return result; +}; + +const imageSizeRule: RuleInline = (state, silent) => { + const env = state.env as ImageEnv; + const oldPos = state.pos; + const max = state.posMax; + + if (state.src.charAt(state.pos) !== "!") return false; + if (state.src.charAt(state.pos + 1) !== "[") return false; + + const labelStart = state.pos + 2; + const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false); + + // parser failed to find ']', so it's not a valid link + if (labelEnd < 0) return false; + + let pos = labelEnd + 1; + let code: number; + + let href = ""; + let title = ""; + let width = ""; + let height = ""; + + if (pos < max && state.src.charAt(pos) === "(") { + // + // Inline link + // + + // [link]( "title" ) + // ^^ skipping these spaces + pos += 1; + + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (!state.md.utils.isSpace(code) && code !== 0x0a) break; + } + + if (pos >= max) return false; + + // [link]( "title" ) + // ^^^^^^ parsing link destination + let res; + + res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax); + + if (res.ok) { + href = state.md.normalizeLink(res.str); + + if (state.md.validateLink(href)) pos = res.pos; + else href = ""; + } + + // [link]( "title" ) + // ^^ skipping these spaces + const start = pos; + + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (!state.md.utils.isSpace(code) && code !== 0x0a) break; + } + + // [link]( "title" ) + // ^^^^^^^ parsing link title + res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax); + + if (pos < max && start !== pos && res.ok) { + title = res.str; + pos = res.pos; + + // [link]( "title" ) + // ^^ skipping these spaces + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (!state.md.utils.isSpace(code) && code !== 0x0a) break; + } + } else title = ""; + + // [link]( "title" =WxH ) + // ^^^^ parsing image size + if (pos - 1 >= 0) { + code = state.src.charCodeAt(pos - 1); + + // there must be at least one white spaces + // between previous field and the size + if (code === 0x20) { + res = parseImageSize(state.src, pos, state.posMax); + if (res.ok) { + width = res.width; + height = res.height; + pos = res.pos; + + // [link]( "title" =WxH ) + // ^^ skipping these spaces + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0a) break; + } + } + } + } + + if (pos >= max || state.src.charAt(pos) !== ")") { + state.pos = oldPos; + + return false; + } + pos += 1; + } else { + let label = ""; + + // + // Link reference + // + if (typeof env.references === "undefined") return false; + + if (pos < max && state.src.charAt(pos) === "[") { + const start = pos + 1; + + pos = state.md.helpers.parseLinkLabel(state, pos); + + if (pos >= 0) label = state.src.slice(start, pos++); + else pos = labelEnd + 1; + } else pos = labelEnd + 1; + + // covers label === '' and label === undefined + // (collapsed reference link and shortcut reference link respectively) + if (!label) label = state.src.slice(labelStart, labelEnd); + + const ref = env.references[state.md.utils.normalizeReference(label)]; + + if (!ref) { + state.pos = oldPos; + + return false; + } + + href = ref.href; + title = ref.title || ""; + } + + // + // We found the end of the link, and know for a fact it's a valid link; + // so all that's left to do is to call tokenizer. + // + if (!silent) { + const content = state.src.slice(labelStart, labelEnd); + const tokens: Token[] = []; + + state.md.inline.parse(content, state.md, state.env, tokens); + + const token = state.push("image", "img", 0); + + token.attrs = [ + ["src", href], + ["alt", ""], + ] as [string, string][]; + if (title) token.attrs.push(["title", title]); + if (width !== "") token.attrs.push(["width", width]); + if (height !== "") token.attrs.push(["height", height]); + + token.children = tokens; + token.content = content; + } + + state.pos = pos; + state.posMax = max; + + return true; +}; + +export const imageSize: PluginSimple = (md) => { + md.inline.ruler.before("emphasis", "image", imageSizeRule); +}; diff --git a/packages/md-enhance/src/node/markdown-it/index.ts b/packages/md-enhance/src/node/markdown-it/index.ts index b97671dcd..9f455d1a0 100644 --- a/packages/md-enhance/src/node/markdown-it/index.ts +++ b/packages/md-enhance/src/node/markdown-it/index.ts @@ -1,6 +1,7 @@ export * from "./align"; export * from "./chart"; export * from "./codeDemo"; +export * from "./codeGroup"; export * from "./codeTabs"; export * from "./container"; export * from "./decodeUrl"; @@ -8,6 +9,7 @@ export * from "./echarts"; export * from "./flowchart"; export * from "./footnote"; export * from "./imageMark"; +export * from "./imageSize"; export * from "./katex"; export * from "./lazyLoad"; export * from "./mark"; diff --git a/packages/md-enhance/src/node/plugin.ts b/packages/md-enhance/src/node/plugin.ts index 24e2cd82c..991dd6b5c 100644 --- a/packages/md-enhance/src/node/plugin.ts +++ b/packages/md-enhance/src/node/plugin.ts @@ -5,6 +5,7 @@ import lineNumbers = require("@vuepress/markdown/lib/lineNumbers"); import { CODE_DEMO_DEFAULT_SETTING, chart, + codeGroup, codeTabs, decodeURL, echarts, @@ -12,6 +13,7 @@ import { footnote, katex, imageMark, + imageSize, include, lazyLoad, mark, @@ -147,6 +149,7 @@ export const mdEnhancePlugin: Plugin = ( if (options.lineNumbers !== false) md.use(lineNumbers); if (options.imageFix !== false) md.use(decodeURL); + // syntax if (getStatus("gfm")) md.options.linkify = true; if (getStatus("align")) md.use(align); if (getStatus("lazyLoad")) md.use(lazyLoad); @@ -155,38 +158,44 @@ export const mdEnhancePlugin: Plugin = ( imageMark, typeof options.imageMark === "object" ? options.imageMark : {} ); + if (getStatus("imageSize")) md.use(imageSize); if (getStatus("sup")) md.use(sup); if (getStatus("sub")) md.use(sub); if (footnoteEnable) md.use(footnote); - if (flowchartEnable) { - md.use(flowchart); - md.use(legacyFlowchart); - } if (getStatus("mark")) md.use(mark); if (tasklistEnable) md.use(tasklist, [ typeof options.tasklist === "object" ? options.tasklist : {}, ]); - if (chartEnable) md.use(chart); + + // addtional functions + if (texEnable) md.use(katex, katexOptions); + if (getStatus("include")) + md.use( + include, + context.sourceDir, + typeof options.include === "function" ? options.include : undefined + ); + if (getStatus("stylize")) md.use(stylize, options.stylize); + + // features + if (getStatus("codegroup")) md.use(codeGroup); if (codeTabsEnable) md.use(codeTabs); + if (tabsEnable) md.use(tabs); + if (flowchartEnable) { + md.use(flowchart); + md.use(legacyFlowchart); + } + if (chartEnable) md.use(chart); + if (echartsEnable) md.use(echarts); if (demoEnable) { md.use(normalDemo); md.use(vueDemo); md.use(reactDemo); md.use(legacyCodeDemo); } - if (echartsEnable) md.use(echarts); - if (getStatus("include")) - md.use( - include, - context.sourceDir, - typeof options.include === "function" ? options.include : undefined - ); if (mermaidEnable) md.use(mermaid); - if (texEnable) md.use(katex, katexOptions); if (presentationEnable) md.use(presentation); - if (getStatus("stylize")) md.use(stylize, options.stylize); - if (tabsEnable) md.use(tabs); }, plugins: getPluginConfig(options, context), diff --git a/packages/md-enhance/src/node/pluginConfig.ts b/packages/md-enhance/src/node/pluginConfig.ts index c9f9ccf85..30ef77bfd 100644 --- a/packages/md-enhance/src/node/pluginConfig.ts +++ b/packages/md-enhance/src/node/pluginConfig.ts @@ -51,31 +51,5 @@ export const getPluginConfig = ( ] ); - if (markdownOptions.codegroup || markdownOptions.enableAll) - config.push( - [ - "container", - { - type: "code-group", - before: () => `\n`, - after: () => "\n", - }, - ], - [ - "container", - { - type: "code-group-item", - before: (info: string): string => { - const isActive = info.split(":").pop() === "active"; - - return `\n`; - }, - after: () => "\n", - }, - ] - ); - return config; }; diff --git a/packages/md-enhance/src/types/options.d.ts b/packages/md-enhance/src/types/options.d.ts index 6b46cfc7c..2a0df13e6 100644 --- a/packages/md-enhance/src/types/options.d.ts +++ b/packages/md-enhance/src/types/options.d.ts @@ -156,6 +156,15 @@ export interface MarkdownEnhanceOptions { */ imageMark?: ImageMarkOptions | boolean; + /** + * Whether to enable image size mark support + * + * 是否启用图片大小标记支持。 + * + * @default false + */ + imageSize?: ImageMarkOptions | boolean; + /** * Whether to enable mark format support *