Skip to content
This repository has been archived by the owner on Apr 24, 2023. It is now read-only.

Commit

Permalink
feat(md-enhance): add imageSize
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Jun 4, 2022
1 parent 00d158a commit 0e4c35e
Show file tree
Hide file tree
Showing 7 changed files with 399 additions and 41 deletions.
73 changes: 73 additions & 0 deletions packages/md-enhance/__tests__/unit/imageSize.spec.ts
Original file line number Diff line number Diff line change
@@ -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(
'<p><img src="/logo.svg" alt="image"></p>\n'
);

expect(markdownIt.render(`![image](/logo.svg =200x300)`)).toEqual(
'<p><img src="/logo.svg" alt="image" width="200" height="300"></p>\n'
);

expect(markdownIt.render(`![image](/logo.svg =200x)`)).toEqual(
'<p><img src="/logo.svg" alt="image" width="200"></p>\n'
);

expect(markdownIt.render(`![image](/logo.svg =x300)`)).toEqual(
'<p><img src="/logo.svg" alt="image" height="300"></p>\n'
);
});

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

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

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

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

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

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

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

it("With title", () => {
expect(markdownIt.render(`![image](/logo.svg "title")`)).toEqual(
'<p><img src="/logo.svg" alt="image" title="title"></p>\n'
);

expect(markdownIt.render(`![image](/logo.svg "title" =200x300)`)).toEqual(
'<p><img src="/logo.svg" alt="image" title="title" width="200" height="300"></p>\n'
);

expect(markdownIt.render(`![image](/logo.svg "title" =200x)`)).toEqual(
'<p><img src="/logo.svg" alt="image" title="title" width="200"></p>\n'
);

expect(markdownIt.render(`![image](/logo.svg "title" =x300)`)).toEqual(
'<p><img src="/logo.svg" alt="image" title="title" height="300"></p>\n'
);
});
});
25 changes: 25 additions & 0 deletions packages/md-enhance/src/node/markdown-it/codeGroup.ts
Original file line number Diff line number Diff line change
@@ -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 `<CodeGroup>\n`;
},
closeRender: () => "</CodeGroup>\n",
});

md.use(container, {
name: "code-group-item",
openRender: (info: string): string => {
const isActive = info.split(":").pop() === "active";

return `<CodeGroupItem title="${
isActive ? info.replace(/:active$/, "") : info
}"${isActive ? " active" : ""}>\n`;
},
closeRender: () => "</CodeGroupItem>\n",
});
};
266 changes: 266 additions & 0 deletions packages/md-enhance/src/node/markdown-it/imageSize.ts
Original file line number Diff line number Diff line change
@@ -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<string, MarkdownReference>;
}

// 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]( <href> "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]( <href> "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]( <href> "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]( <href> "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]( <href> "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]( <href> "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]( <href> "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);
};
2 changes: 2 additions & 0 deletions packages/md-enhance/src/node/markdown-it/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export * from "./align";
export * from "./chart";
export * from "./codeDemo";
export * from "./codeGroup";
export * from "./codeTabs";
export * from "./container";
export * from "./decodeUrl";
export * from "./echarts";
export * from "./flowchart";
export * from "./footnote";
export * from "./imageMark";
export * from "./imageSize";
export * from "./katex";
export * from "./lazyLoad";
export * from "./mark";
Expand Down
Loading

0 comments on commit 0e4c35e

Please sign in to comment.