Skip to content

Commit 631abbb

Browse files
支持修改图片大小 (#16)
* 支持修改图片大小 * format code
1 parent f48b7bd commit 631abbb

File tree

8 files changed

+449
-9
lines changed

8 files changed

+449
-9
lines changed

examples/SupportedGrammer/problem-0.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ This is `inline code`
4444

4545
[NOI website](https://noi.cn/)
4646

47-
Inline ![](https://private-static.mrpython.top/cnoi-gen-test-img-small_e06124f5.jpg) image
47+
Inline ![img](https://private-static.mrpython.top/cnoi-gen-test-img-small_e06124f5.jpg) image
4848

4949
:::figure{caption=居中图片。在这里添加一些图片描述。}
5050
![1.jpg](https://private-static.mrpython.top/cnoi-gen-test-img_324e0508.jpg)
@@ -58,7 +58,11 @@ caption 参数是可选的。
5858
文本也可以放进去。
5959
:::
6060

61-
<https://luogu.com.cn>
61+
![small](https://private-static.mrpython.top/cnoi-gen-test-img_324e0508.jpg){height=4em}![small](https://private-static.mrpython.top/cnoi-gen-test-img_324e0508.jpg){width=4em}图片
62+
63+
支持的单位有 `pt`, `mm`, `cm`, `in`, `em` 和按页面比例的 `%`
64+
65+
简单链接:<https://luogu.com.cn>
6266

6367
---
6468

package-lock.json

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"remark-parse": "^11.0.0",
5151
"strict-event-emitter": "^0.5.1",
5252
"unified": "^11.0.5",
53+
"unist-util-visit": "^5.0.0",
5354
"use-immer": "^0.11.0",
5455
"xxhashjs": "^0.2.2"
5556
},
@@ -75,6 +76,7 @@
7576
"fontkit": "^2.0.4",
7677
"globals": "^16.4.0",
7778
"jiti": "^2.6.1",
79+
"mdast-util-to-hast": "^13.2.0",
7880
"pdf-to-img": "^5.0.0",
7981
"pngjs": "^7.0.0",
8082
"prettier": "^3.6.2",

src/compiler/processor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import remarkDirective from "remark-directive";
2+
import { unified } from "unified";
23
import remarkGfm from "remark-gfm";
34
import remarkMath from "remark-math";
45
import remarkParse from "remark-parse";
5-
import { unified } from "unified";
66
import remarkTypst from "./remarkTypst";
7+
import remarkImageAttr from "./remarkImageAttr";
78

89
const processor = unified()
910
.use(remarkParse)
1011
.use(remarkMath)
1112
.use(remarkGfm)
13+
.use(remarkImageAttr)
1214
.use(remarkDirective)
1315
.use(remarkTypst)
1416
.freeze();

src/compiler/remarkImageAttr.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { type Plugin } from "unified";
2+
import type * as mdast from "mdast";
3+
import { visit } from "unist-util-visit";
4+
5+
declare module "mdast" {
6+
interface ImageData {
7+
attr?: Record<string, string | undefined>;
8+
}
9+
interface ImageReferenceData {
10+
attr?: Record<string, string | undefined>;
11+
}
12+
}
13+
14+
export function parseAttr(
15+
s: string,
16+
): [attr: Record<string, string | undefined>, rest: string] | undefined {
17+
const attr: Record<string, string | undefined> = {};
18+
let status:
19+
| {
20+
type: "didn't start";
21+
}
22+
| {
23+
type: "ended";
24+
}
25+
| {
26+
type: "key";
27+
buf: string[];
28+
}
29+
| {
30+
type: "value unknown";
31+
key: string;
32+
}
33+
| {
34+
type:
35+
| "value without quote"
36+
| "value with single quote"
37+
| "value with double quote";
38+
key: string;
39+
buf: string[];
40+
}
41+
| { type: "wait for next" } = { type: "didn't start" };
42+
let len = 0;
43+
for (const c of s) {
44+
++len;
45+
if (c === "\n" || c === "\r" || c === "\t") break;
46+
if (status.type === "didn't start") {
47+
if (c !== "{") break;
48+
status = { type: "key", buf: [] };
49+
} else if (status.type === "key") {
50+
if (c === "=") {
51+
if (status.buf.length === 0) break;
52+
status = {
53+
type: "value unknown",
54+
key: status.buf.join("").trim(),
55+
};
56+
} else if (c === ",") {
57+
attr[status.buf.join("").trim()] = undefined;
58+
status = { type: "key", buf: [] };
59+
} else if (c === "}") {
60+
status = { type: "ended" };
61+
break;
62+
} else status.buf.push(c);
63+
} else if (status.type === "value unknown") {
64+
if (c === " ") continue;
65+
else if (c === "'")
66+
status = {
67+
type: "value with single quote",
68+
key: status.key,
69+
buf: [],
70+
};
71+
else if (c === '"')
72+
status = {
73+
type: "value with double quote",
74+
key: status.key,
75+
buf: [],
76+
};
77+
else if (c === ",") {
78+
attr[status.key] = "";
79+
status = { type: "key", buf: [] };
80+
} else if (c === "}") {
81+
attr[status.key] = "";
82+
status = { type: "ended" };
83+
break;
84+
} else if (c === "=") break;
85+
else
86+
status = {
87+
type: "value without quote",
88+
key: status.key,
89+
buf: [c],
90+
};
91+
} else if (status.type === "value without quote") {
92+
if (c === ",") {
93+
attr[status.key] = status.buf.join("").trim();
94+
status = { type: "key", buf: [] };
95+
} else if (c === "}") {
96+
attr[status.key] = status.buf.join("").trim();
97+
status = { type: "ended" };
98+
break;
99+
} else if (c === "=") break;
100+
else status.buf.push(c);
101+
} else if (status.type === "value with single quote") {
102+
if (c === "'") {
103+
attr[status.key] = status.buf.join("");
104+
status = { type: "wait for next" };
105+
} else status.buf.push(c);
106+
} else if (status.type === "value with double quote") {
107+
if (c === '"') {
108+
attr[status.key] = status.buf.join("");
109+
status = { type: "wait for next" };
110+
} else status.buf.push(c);
111+
} else if (status.type === "wait for next") {
112+
if (c === ",") {
113+
status = { type: "key", buf: [] };
114+
} else if (c === "}") {
115+
status = { type: "ended" };
116+
break;
117+
} else if (c !== " ") break;
118+
}
119+
}
120+
if (status.type !== "ended") return undefined;
121+
else return [attr, s.slice(len)];
122+
}
123+
124+
const remarkImageAttr: Plugin<[], mdast.Root, mdast.Root> = () => {
125+
return (tree) => {
126+
if (tree.type !== "root")
127+
throw new TypeError(`Expected root node, got ${tree.type}`);
128+
visit(tree, (node) => {
129+
if (!("children" in node)) return;
130+
for (let i = 1; i < node.children.length; ++i) {
131+
const pre = node.children[i - 1],
132+
cur = node.children[i];
133+
if (
134+
(pre.type !== "image" && pre.type !== "imageReference") ||
135+
cur.type !== "text"
136+
)
137+
continue;
138+
const res = parseAttr(cur.value);
139+
if (!res) continue;
140+
const [attr, rest] = res;
141+
if (!pre.data) pre.data = {};
142+
if (!pre.data.attr) pre.data.attr = {};
143+
Object.assign(pre.data.attr, attr);
144+
cur.value = rest;
145+
}
146+
node.children = node.children.filter((n) => n.type !== "text" || n.value);
147+
});
148+
};
149+
};
150+
151+
export default remarkImageAttr;

src/compiler/remarkTypst/compiler.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface CompilerContext {
2222
const TYPST_HEADER = `#import "utils.typ": *\n\n`;
2323
const FOOTNOTE_ID_PREFIX = "user-footnote: ";
2424

25+
export const TYPST_RELATIVE_VALUE_REGEX =
26+
/^(?: *[+-]? *(?:\d+(?:\.\d+)?|\.\d+)(?:pt|mm|cm|in|em|%))(?: *[+-] *(?:\d+(?:\.\d+)?|\.\d+)(?:pt|mm|cm|in|em|%))* *$/;
27+
2528
export function escapeTypstString(s: string) {
2629
return s
2730
.replace(/\\/g, "\\\\")
@@ -236,9 +239,15 @@ export const handlers = {
236239
image: (node, ctx) => {
237240
const { data, assets } = ctx;
238241
const assertID = "img-" + hash(node.url);
239-
data.push('#box(image("', assertID);
240-
if (node.alt) data.push('", alt: "', escapeTypstString(node.alt));
241-
data.push('"))');
242+
data.push('#box(image("', assertID, '"');
243+
for (const k of ["width", "height"] as const) {
244+
const val = node.data?.attr?.[k];
245+
if (typeof val !== "string" || !TYPST_RELATIVE_VALUE_REGEX.test(val))
246+
continue;
247+
data.push(`, ${k}: ${val}`);
248+
}
249+
if (node.alt) data.push(', alt: "', escapeTypstString(node.alt), '"');
250+
data.push("))");
242251
assets.push({
243252
assetUrl: node.url,
244253
filename: assertID,
@@ -264,9 +273,15 @@ export const handlers = {
264273
const def = definitionById.get(node.identifier);
265274
if (def) {
266275
const assertID = "img-" + hash(def.url);
267-
data.push('#box(image("', assertID);
268-
if (node.alt) data.push('", alt: "', escapeTypstString(node.alt));
269-
data.push('"))');
276+
data.push('#box(image("', assertID, '"');
277+
for (const k of ["width", "height"] as const) {
278+
const val = node.data?.attr?.[k];
279+
if (typeof val !== "string" || !TYPST_RELATIVE_VALUE_REGEX.test(val))
280+
continue;
281+
data.push(`, ${k}: ${val}`);
282+
}
283+
if (node.alt) data.push(', alt: "', escapeTypstString(node.alt), '"');
284+
data.push("))");
270285
assets.push({
271286
assetUrl: def.url,
272287
filename: assertID,

0 commit comments

Comments
 (0)