Skip to content

Commit 59ee679

Browse files
committed
doc/starlight: Cleanup doxygen and add doxygen image endpoint
1 parent d3b83ff commit 59ee679

5 files changed

Lines changed: 163 additions & 57 deletions

File tree

doc/starlight/package-lock.json

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

doc/starlight/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@astrojs/rss": "^4.0.19",
1414
"@astrojs/starlight": "^0.41.3",
1515
"astro": "^7.1.1",
16+
"mrmime": "2.0.1",
1617
"rehype-github-emoji": "1.0.0",
1718
"sharp": "0.35.3",
1819
"starlight-image-zoom": "0.15.0",

doc/starlight/public/boards/nucleo-c031c6.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

doc/starlight/src/lib/doxygen_filter.ts

Lines changed: 118 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,24 @@ function stripDoxygenAnchors(line: string): string {
4545
return line.replace(/\s*\{#[^}]+\}\s*/g, "").trimEnd();
4646
}
4747

48+
/** Human-friendly labels for the Doxygen callout/aside commands we render. */
49+
const CALLOUT_LABELS: Record<string, string> = {
50+
warning: "Warning",
51+
attention: "Warning",
52+
important: "Warning",
53+
note: "Note",
54+
remark: "Note",
55+
tip: "Tip",
56+
hint: "Tip",
57+
};
58+
4859
/**
4960
* Map Doxygen callout/aside commands to human-friendly labels.
5061
* @param command The Doxygen callout command (e.g. "warning", "note", "tip", etc.).
5162
* @returns A human-friendly label for the callout (e.g. "Warning", "Note", "Tip"), or null if the command is not recognized as a callout.
5263
*/
5364
function getCalloutLabel(command: string): string | null {
54-
const key = command.toLowerCase();
55-
if (key === "warning" || key === "attention" || key === "important") {
56-
return "Warning";
57-
}
58-
if (key === "note" || key === "remark") {
59-
return "Note";
60-
}
61-
if (key === "tip" || key === "hint") {
62-
return "Tip";
63-
}
64-
return null;
65+
return CALLOUT_LABELS[command.toLowerCase()] ?? null;
6566
}
6667

6768
/**
@@ -100,20 +101,11 @@ export function extractDoxygenGroupTitleFromDoxygen(
100101
groupPrefix: string,
101102
fallback: string,
102103
): string {
103-
const lines = content.split("\n");
104-
const defgroupPattern = new RegExp(
105-
`@defgroup\\s+${groupPrefix}_[A-Za-z0-9._-]+\\s+(.+)$`
104+
const match = content.match(
105+
new RegExp(`@defgroup\\s+${groupPrefix}_[A-Za-z0-9._-]+\\s+(.+)$`, "m"),
106106
);
107107

108-
for (let i = 0; i < lines.length; i++) {
109-
const match = lines[i].match(defgroupPattern);
110-
if (!match) {
111-
continue;
112-
}
113-
return stripAsterisksAndQuotes(match[1]);
114-
}
115-
116-
return fallback;
108+
return match ? stripAsterisksAndQuotes(match[1]) : fallback;
117109
}
118110

119111
/**
@@ -184,40 +176,110 @@ function transformImageLine(line: string): string | null {
184176
}
185177

186178
/**
187-
* Transform Doxygen markdown content to remove unsupported directives and convert some to standard markdown.
188-
* @param content The raw Doxygen markdown content to transform.
189-
* @returns The transformed markdown content with unsupported directives removed and some converted to standard markdown.
190-
* @warn This function does not support all Doxygen directives and may not cover all edge cases.
191-
* It is intended to handle the most common directives found in RIOT board documentation,
192-
* but may need to be extended in the future to support additional directives or edge cases.
179+
* Astro site-relative path under which the images of the Doxygen source tree are
180+
* served.
193181
*/
194-
export function transformDoxygenMarkdown(content: string): string {
195-
// Split the content into lines and process each line to handle Doxygen directives.
196-
const sourceLines = content.split(/\r?\n/);
197-
const out: string[] = [];
198-
199-
for (let i = 0; i < sourceLines.length; i++) {
200-
const rawLine = sourceLines[i];
201-
const line = rawLine.trim();
202-
const brief = transformBriefLine(line);
203-
const callout = transformCalloutLine(line);
204-
const image = transformImageLine(line);
205-
206-
if (isGroupMetadata(line)) {
207-
// If desired, we could potentially use these to structure the content in the future,
208-
// but for now we just ignore them.
209-
} else if (brief !== null) {
210-
out.push(brief);
211-
} else if (callout !== null) {
212-
if (callout) {
213-
out.push(callout);
214-
}
215-
} else if (image !== null) {
216-
out.push(image);
217-
} else {
218-
out.push(stripDoxygenAnchors(replaceInlineDoxygenCommands(rawLine)));
219-
}
182+
const IMAGE_BASE_PATH = "/img";
183+
184+
/** File extensions that are treated as images when rewriting relative paths. */
185+
const IMAGE_EXTENSION_PATTERN = /\.(?:svg|png|jpe?g|gif|webp|avif)$/i;
186+
187+
/** URLs that must be kept as they are (absolute, protocol relative, data, anchors). */
188+
const ABSOLUTE_URL_PATTERN = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/|\/|#)/;
189+
190+
/**
191+
* Rewrite a single image reference to a path that the site can serve.
192+
*
193+
* Doxygen resolves image references through its IMAGE_PATH, because the docs sadly
194+
* only spell out the file name (e.g. `nucleo-f031k6-and-more.svg`) even though the
195+
* file lives in `doc/doxygen/src/pinouts/`.
196+
* The image will be hosted by starlight inside the `IMAGE_BASE_PATH`.
197+
*
198+
* @param target The image reference as written in the Doxygen documentation.
199+
* @returns The rewritten reference, or the unchanged target if it is not a relative image.
200+
*/
201+
function rewriteImageReference(target: string): string {
202+
if (
203+
ABSOLUTE_URL_PATTERN.test(target) ||
204+
!IMAGE_EXTENSION_PATTERN.test(target)
205+
) {
206+
return target;
220207
}
221208

222-
return out.join("\n");
209+
// Only the file name is relevant
210+
const fileName = target.split("/").pop();
211+
return `${IMAGE_BASE_PATH}/${fileName}`;
212+
}
213+
214+
/**
215+
* Rewrite the relative image references of a line of text, both in markdown
216+
* image syntax (`![alt](file.svg)`) and in raw `<img>` tags, which the board
217+
* and CPU docs use to be able to set an image width.
218+
*
219+
* @param line The line of text to rewrite the image references in.
220+
* @returns The line of text with all relative image references rewritten.
221+
*/
222+
function rewriteImageReferences(line: string): string {
223+
return line
224+
.replace(
225+
/(<img\b[^>]*?\ssrc\s*=\s*(["']))(.*?)\2/gi,
226+
(_match, prefix: string, quote: string, target: string) =>
227+
`${prefix}${rewriteImageReference(target)}${quote}`,
228+
)
229+
.replace(
230+
/(!\[[^\]]*\]\(\s*)([^)\s]+)/g,
231+
(_match, prefix: string, target: string) =>
232+
`${prefix}${rewriteImageReference(target)}`,
233+
);
234+
}
235+
236+
/**
237+
* Transform a single line of Doxygen markdown.
238+
* @param rawLine The untrimmed input line.
239+
* @returns The transformed line, or null if the line should be dropped.
240+
*/
241+
function transformDoxygenLine(rawLine: string): string | null {
242+
const line = rawLine.trim();
243+
244+
// Group metadata could be used to structure the content in the future,
245+
// but for now we just drop it.
246+
if (isGroupMetadata(line)) {
247+
return null;
248+
}
249+
250+
const brief = transformBriefLine(line);
251+
if (brief !== null) {
252+
return brief;
253+
}
254+
255+
// Callout commands without a human-friendly label (e.g. @experimental)
256+
// yield an empty string and are dropped.
257+
const callout = transformCalloutLine(line);
258+
if (callout !== null) {
259+
return callout || null;
260+
}
261+
262+
const image = transformImageLine(line);
263+
if (image !== null) {
264+
return rewriteImageReferences(image);
265+
}
266+
267+
return rewriteImageReferences(
268+
stripDoxygenAnchors(replaceInlineDoxygenCommands(rawLine)),
269+
);
270+
}
271+
272+
/**
273+
* Transform Doxygen markdown content to remove unsupported directives and
274+
* convert some to standard markdown.
275+
*
276+
* @param content The raw Doxygen markdown content to transform.
277+
* @returns The transformed markdown content.
278+
*/
279+
export function transformDoxygenMarkdown(content: string): string {
280+
return content
281+
.split(/\r?\n/)
282+
.map(transformDoxygenLine)
283+
.filter((line): line is string => line !== null)
284+
.join("\n");
223285
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Lasse Rosenow <Lasse.Rosenow@haw-hamburg.de>
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
6+
import type { APIRoute, GetStaticPaths } from "astro";
7+
import { promises as fs } from "node:fs";
8+
import { lookup } from "mrmime";
9+
import path from "node:path";
10+
11+
/**
12+
* Directories in this repo that contain images referenced by some markdown files.
13+
*/
14+
const IMAGE_DIRS = ["../doxygen/src/pinouts"];
15+
16+
/**
17+
* Generate paths for each image from the specified dirs.
18+
*/
19+
export const getStaticPaths: GetStaticPaths = async () => {
20+
const pathsPerDir = await Promise.all(
21+
IMAGE_DIRS.map(async (dir) =>
22+
(await fs.readdir(dir, { withFileTypes: true }))
23+
.filter((entry) => entry.isFile())
24+
.map((entry) => ({
25+
params: { image: entry.name },
26+
props: { filePath: path.join(dir, entry.name) },
27+
})),
28+
),
29+
);
30+
31+
return pathsPerDir.flat();
32+
};
33+
34+
/**
35+
* API route responding with the correct image.
36+
*/
37+
export const GET: APIRoute = async ({ props }) => {
38+
return new Response(await fs.readFile(props.filePath), {
39+
headers: {
40+
"Content-Type": lookup(props.filePath) ?? "application/octet-stream",
41+
},
42+
});
43+
};

0 commit comments

Comments
 (0)