Skip to content

Commit 0c1f217

Browse files
feat: Generate a JSON index
1 parent 2c07122 commit 0c1f217

File tree

6 files changed

+240
-6
lines changed

6 files changed

+240
-6
lines changed

main.typ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2025,7 +2025,7 @@ $ integral f dif x $
20252025

20262026
#set heading(numbering: none)
20272027

2028-
= #bbl(en: [Addendum], zh: [附录])
2028+
= #bbl(en: [Addendum], zh: [附录]) <addendum>
20292029

20302030
== #bbl(en: [List of sites], zh: [站点列表])
20312031

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
"glob-watcher": "^6.0.0"
1212
},
1313
"scripts": {
14-
"build": "tsc && vite build && node --experimental-strip-types scripts/build.ts",
14+
"build": "tsc && vite build && node --experimental-strip-types scripts/build.ts && pnpm generate-json-index",
1515
"dev": "node --experimental-strip-types scripts/dev.ts",
1616
"preview": "vite preview --base=/clreq/",
1717
"check-issues": "node --experimental-strip-types scripts/check_issues.ts",
18+
"generate-json-index": "node --experimental-strip-types scripts/generate-json-index.ts > dist/index.json",
1819
"patch-htmldiff": "node --experimental-strip-types scripts/patch-htmldiff.ts"
1920
},
2021
"devDependencies": {

scripts/check_issues.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ interface RepoNum {
2323
repo: string;
2424
num: string;
2525
}
26-
interface IssueMeta extends RepoNum {
26+
export interface IssueMeta extends RepoNum {
2727
note: string;
2828
closed: boolean;
2929
}
30-
interface PullMeta extends RepoNum {
30+
export interface PullMeta extends RepoNum {
3131
merged: boolean;
3232
rejected: boolean;
3333
}

scripts/generate-json-index.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* Generate a JSON index and print to stdout.
3+
*/
4+
5+
import assert from "node:assert";
6+
7+
import type { IssueMeta, PullMeta } from "./check_issues.ts";
8+
import { extraArgs } from "./config.ts";
9+
import { typst } from "./typst.ts";
10+
11+
type Priority = "ok" | "advanced" | "basic" | "broken" | "tbd" | "na";
12+
type GeneralPriority = Priority | "(inherited)";
13+
14+
// Parsed types
15+
16+
type Babel = { en: string; "zh-Hans": string };
17+
18+
type Heading = {
19+
level: number;
20+
body: Babel;
21+
label?: string;
22+
};
23+
type Section = {
24+
title: Babel;
25+
level: number;
26+
label?: string;
27+
priority: GeneralPriority;
28+
links: (
29+
| ({ type: "issue" } & IssueMeta)
30+
| ({ type: "pull" } & PullMeta)
31+
| ({ type: "workaround" } & WorkaroundMeta)
32+
)[];
33+
};
34+
35+
// Typst elements
36+
37+
type HeadingElem = {
38+
func: "heading";
39+
level: number;
40+
body: ContentElem;
41+
label?: string;
42+
};
43+
type SpaceElem = { func: "space" };
44+
type TextElem = { func: "text"; text: string };
45+
type RawElem = { func: "raw"; text: string };
46+
type Sequence<T> = {
47+
func: "sequence";
48+
children: T[];
49+
};
50+
/** Same as `Sequence<ContentElem>`, but without self-reference. */
51+
type SequenceElem = {
52+
func: "sequence";
53+
children: ContentElem[];
54+
};
55+
type StyledElem = {
56+
func: "styled";
57+
child: ContentElem;
58+
styles: "..";
59+
};
60+
/**
61+
* The body of a heading element.
62+
*
63+
* Only relevant fields and combinations are included.
64+
*/
65+
type ContentElem =
66+
| StyledElem
67+
| SequenceElem
68+
| SpaceElem
69+
| TextElem
70+
| {
71+
func: "elem";
72+
tag: "span";
73+
attrs: { lang: "en" | "zh-Hans" } | { style: string };
74+
body: TextElem | Sequence<TextElem | RawElem | SpaceElem>;
75+
};
76+
77+
type WorkaroundMeta = {
78+
dest: string;
79+
note: string | null;
80+
};
81+
82+
function* _parseContent(
83+
it: ContentElem | RawElem | Sequence<TextElem | RawElem | SpaceElem>,
84+
): Generator<
85+
| { action: "pop"; text: string }
86+
| { action: "switch-lang"; next: "en" | "zh-Hans" }
87+
> {
88+
const pop = (text: string) => ({ action: "pop" as const, text });
89+
const switchLang = (lang: "en" | "zh-Hans") => ({
90+
action: "switch-lang" as const,
91+
next: lang,
92+
});
93+
94+
switch (it.func) {
95+
// Structural elements
96+
case "sequence":
97+
for (const child of it.children) {
98+
yield* _parseContent(child);
99+
}
100+
break;
101+
case "styled":
102+
yield* _parseContent(it.child);
103+
break;
104+
105+
// Basic elements
106+
case "text":
107+
yield pop(it.text);
108+
break;
109+
case "space":
110+
yield pop(" ");
111+
break;
112+
case "raw":
113+
yield pop(`“${it.text}”`);
114+
break;
115+
116+
// HTML elements
117+
case "elem":
118+
if ("lang" in it.attrs) {
119+
const lang = it.attrs.lang;
120+
assert(
121+
lang === "en" || lang === "zh-Hans",
122+
`Invalid language: ${lang} in ${JSON.stringify(it)}`,
123+
);
124+
yield switchLang(lang);
125+
}
126+
yield* _parseContent(it.body);
127+
break;
128+
129+
default:
130+
throw new Error(`Reached unexpected element: ${JSON.stringify(it)}`);
131+
}
132+
}
133+
134+
function parseHeading(heading: HeadingElem): Heading {
135+
const { level, body, label } = heading;
136+
const parser = _parseContent(body);
137+
138+
const parsed = { en: "", "zh-Hans": "" };
139+
let lang: keyof typeof parsed = "en";
140+
141+
for (const p of parser) {
142+
switch (p.action) {
143+
case "pop":
144+
parsed[lang] += p.text;
145+
break;
146+
case "switch-lang":
147+
// Remove the space added by `babel` between languages.
148+
parsed[lang] = parsed[lang].trim();
149+
lang = p.next;
150+
break;
151+
default:
152+
throw new Error(`Invalid action: ${JSON.stringify(p)}`);
153+
}
154+
}
155+
156+
return { level, body: parsed, label };
157+
}
158+
159+
const data = (JSON.parse(
160+
await typst([
161+
"query",
162+
"index.typ", // This cannot be main.typ, or there will be an additional heading (`outline`'s title).
163+
[
164+
"selector.or(heading, <priority>, <issue>, <pull>, <workaround>)",
165+
".after(outline)",
166+
".before(<addendum>, inclusive: false)",
167+
].join(""),
168+
"--target=html",
169+
...extraArgs.pre,
170+
]),
171+
) as (
172+
| HeadingElem
173+
| (
174+
& { func: "metadata" }
175+
& (
176+
| { value: Priority; label: "<priority>" }
177+
| { value: IssueMeta; label: "<issue>" }
178+
| { value: PullMeta; label: "<pull>" }
179+
| { value: WorkaroundMeta; label: "<workaround>" }
180+
)
181+
)
182+
)[]).map((el) => {
183+
if (el.func === "heading") {
184+
return parseHeading(el);
185+
}
186+
return el;
187+
});
188+
189+
const sections: Section[] = [];
190+
191+
for (const it of data) {
192+
if ("level" in it) {
193+
const { body: title, level, label } = it;
194+
sections.push({ title, level, label, priority: "(inherited)", links: [] });
195+
} else {
196+
const last = sections.at(-1);
197+
assert(
198+
last !== undefined,
199+
`Metadata describe the heading before them, but there is no heading before this one: ${
200+
JSON.stringify(it)
201+
}`,
202+
);
203+
204+
switch (it.label) {
205+
case "<priority>":
206+
assert(
207+
last.priority === "(inherited)",
208+
`There are multiple priority levels marking the same section: heading = ${
209+
JSON.stringify(last.title)
210+
}, priority = [${last.priority}, ${it.value}, …]`,
211+
);
212+
last.priority = it.value;
213+
break;
214+
215+
case "<issue>":
216+
last.links.push({ type: "issue", ...it.value });
217+
break;
218+
case "<pull>":
219+
last.links.push({ type: "pull", ...it.value });
220+
break;
221+
case "<workaround>":
222+
last.links.push({ type: "workaround", ...it.value });
223+
break;
224+
225+
default:
226+
throw new Error(`Reached unexpected element: ${JSON.stringify(it)}`);
227+
}
228+
}
229+
}
230+
231+
console.log(JSON.stringify({ version: "2025-11-21", sections }, null, 2));

typ/prioritization.typ

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
data-priority-level: level,
8787
),
8888
)
89+
[#metadata(level)<priority>]
8990
}
9091

9192
/// Priority levels

typ/util.typ

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
if anchor != "" { [~(comment)] }
5252
} else { [~(#note)] }
5353
})
54-
[#metadata((repo: repo, num: num, note: repr(note), closed: closed)) <issue>]
54+
[#metadata((repo: repo, num: num, note: repr(note), closed: closed))<issue>]
5555
}
5656

5757
/// Link to a GitHub pull request
@@ -73,7 +73,7 @@
7373
if merged { icon.git-merge } else if rejected { icon.git-pull-request-closed } else { icon.git-pull-request }
7474
repo-num
7575
})
76-
[#metadata((repo: repo, num: num, merged: merged, rejected: rejected)) <pull>]
76+
[#metadata((repo: repo, num: num, merged: merged, rejected: rejected))<pull>]
7777
}
7878

7979
/// Link to a workaround
@@ -101,6 +101,7 @@
101101
icon.light-bulb
102102
body
103103
})
104+
[#metadata((dest: dest, note: note))<workaround>]
104105
}
105106

106107
/// A formatted description of the Unicode character for a given codepoint

0 commit comments

Comments
 (0)