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(
+ '
\n'
+ );
+
+ expect(markdownIt.render(`![image](/logo.svg =200x300)`)).toEqual(
+ '\n'
+ );
+
+ expect(markdownIt.render(`![image](/logo.svg =200x)`)).toEqual(
+ '\n'
+ );
+
+ expect(markdownIt.render(`![image](/logo.svg =x300)`)).toEqual(
+ '\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(
+ '\n'
+ );
+
+ expect(markdownIt.render(`![image](/logo.svg "title" =200x300)`)).toEqual(
+ '\n'
+ );
+
+ expect(markdownIt.render(`![image](/logo.svg "title" =200x)`)).toEqual(
+ '\n'
+ );
+
+ expect(markdownIt.render(`![image](/logo.svg "title" =x300)`)).toEqual(
+ '\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
*