Skip to content

Commit 192e419

Browse files
committed
feat(website): linkify changelog version headers
1 parent 64fba6d commit 192e419

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

website/public/releases/index.ejs.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@
2727
padding-top: 2rem;
2828
margin-top: 2rem;
2929
}
30+
31+
h3 a,
32+
h3 a:visited,
33+
h3 a:link {
34+
color: black;
35+
}
36+
h3 a:hover {
37+
text-decoration: underline;
38+
}
39+
@media (prefers-color-scheme: dark) {
40+
h3 a,
41+
h3 a:visited,
42+
h3 a:link {
43+
color: white;
44+
}
45+
}
3046
</style>
3147
</head>
3248
<body class="side-bar-nav">

website/src/release-documentation.mjs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See end of file for extended copyright information.
33

44
import MarkdownIt from "markdown-it";
5+
import assert from "node:assert";
56

67
let markdownParser = new MarkdownIt("commonmark");
78

@@ -10,6 +11,7 @@ export function releasesMarkdownToHTML(releasesMarkdown) {
1011
let tokens = markdownParser.parse(releasesMarkdown, env);
1112
tokens = removeTitle(tokens);
1213
tokens = demoteHeadings(tokens);
14+
tokens = linkifyVersions(tokens);
1315

1416
let html = markdownParser.renderer.render(
1517
tokens,
@@ -54,6 +56,81 @@ function demoteHeadings(markdownTokens) {
5456
});
5557
}
5658

59+
function linkifyVersions(markdownTokens) {
60+
markdownTokens = [...markdownTokens];
61+
62+
// @type Array<[number, number]>
63+
let versionHeadingSpanIndexes = [];
64+
for (let i = 0; i < markdownTokens.length; ++i) {
65+
let token = markdownTokens[i];
66+
if (token.tag === "h3") {
67+
if (token.type === "heading_open") {
68+
versionHeadingSpanIndexes.push([i]);
69+
}
70+
if (token.type === "heading_close") {
71+
versionHeadingSpanIndexes[versionHeadingSpanIndexes.length - 1].push(i);
72+
}
73+
}
74+
}
75+
76+
versionHeadingSpanIndexes.reverse();
77+
for (let [headingBeginIndex, headingEndIndex] of versionHeadingSpanIndexes) {
78+
let textTokens = markdownTokens.slice(
79+
headingBeginIndex + 1,
80+
headingEndIndex
81+
);
82+
let text = textTokens.map((token) => token.content).join("");
83+
let versionInfo = extractVersionInfoFromHeadingText(text);
84+
if (versionInfo === null) {
85+
continue;
86+
}
87+
let currentVersionID = versionInfo.version;
88+
89+
let headingOpenToken = markdownTokens[headingBeginIndex];
90+
let headingCloseToken = markdownTokens[headingEndIndex];
91+
let newTokens = [
92+
{
93+
...headingOpenToken,
94+
attrs: [...(headingOpenToken.attrs ?? []), ["id", currentVersionID]],
95+
},
96+
{
97+
type: "link_open",
98+
tag: "a",
99+
attrs: [["href", `#${currentVersionID}`]],
100+
},
101+
...textTokens,
102+
{
103+
type: "link_close",
104+
tag: "a",
105+
},
106+
headingCloseToken,
107+
];
108+
markdownTokens.splice(
109+
headingBeginIndex,
110+
headingEndIndex - headingBeginIndex,
111+
...newTokens
112+
);
113+
}
114+
115+
return markdownTokens;
116+
}
117+
118+
function extractVersionInfoFromHeadingText(text) {
119+
let match = text.match(
120+
/^(?<version>(?:\d+\.)*\d+) \((?<date>\d+-\d+-\d+)\)$/
121+
);
122+
if (match === null) {
123+
if (text !== "Unreleased") {
124+
console.warn(`warning: failed to parse version info from ${text}`);
125+
}
126+
return null;
127+
}
128+
return {
129+
version: match.groups.version,
130+
date: match.groups.date,
131+
};
132+
}
133+
57134
// quick-lint-js finds bugs in JavaScript programs.
58135
// Copyright (C) 2020 Matthew "strager" Glazar
59136
//

0 commit comments

Comments
 (0)