@nu-appdev/northwestern-starlight-theme 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/README.md +1 -1
- package/config.ts +15 -7
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mermaid.d.ts +4 -5
- package/dist/mermaid.d.ts.map +1 -1
- package/dist/src/config-schema.d.ts +0 -81
- package/dist/src/config-schema.d.ts.map +1 -1
- package/dist/src/markdown-processor.d.ts +86 -0
- package/dist/src/markdown-processor.d.ts.map +1 -0
- package/dist/src/rehype-table-scroll.d.ts.map +1 -1
- package/index.ts +12 -4
- package/mermaid.ts +5 -5
- package/package.json +7 -6
- package/src/markdown-processor.ts +168 -0
- package/src/scripts/mermaid/fullscreen.ts +22 -1
- package/src/scripts/mermaid/overlay.ts +3 -0
- package/src/scripts/mermaid/ui.ts +182 -8
- package/src/styles/mermaid-toolbar.css +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.1] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Starlight plugins that extend the Markdown processor no longer stop working.** Astro 7 renders Markdown with the Sätteri processor and hands every integration the same live processor object. Plugins extend the pipeline by changing that object. The theme replaced it with a `unified()` processor from two `astro:config:setup` hooks, which run after Starlight has already set up its plugins, so everything those plugins had registered was thrown away.
|
|
15
|
+
|
|
16
|
+
The visible effect was `starlight-links-validator` 0.25 or newer: it validated zero links on every build and still printed "All internal links are valid", so broken internal links passed unnoticed. Any plugin that configures itself against the Sätteri processor was affected the same way.
|
|
17
|
+
|
|
18
|
+
`defineNorthwesternConfig()` now sets `markdown.processor` to `unified()` while `astro.config.*` is evaluated, before `starlight()` is created. No plugin ever sees Sätteri, and the theme only extends the processor it is given instead of replacing it. A processor you configure yourself is left alone.
|
|
19
|
+
|
|
20
|
+
- Mermaid no longer swaps the Markdown processor either. `astro-mermaid` 2.1 supports Sätteri on its own.
|
|
21
|
+
|
|
22
|
+
## [1.6.0] - 2026-07-28
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Fullscreen Mermaid diagrams can be downloaded as high-resolution PNG files. PNG exports render at up to 4× the diagram's intrinsic size, use the active Mermaid theme's configured canvas color for reliable contrast, and share the same descriptive filenames as SVG downloads.
|
|
27
|
+
|
|
10
28
|
## [1.5.1] - 2026-04-22
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -185,7 +203,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
185
203
|
- OpenAPI plugin compatibility with method badge preservation
|
|
186
204
|
- Reduced motion support for transitions
|
|
187
205
|
|
|
188
|
-
[Unreleased]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.
|
|
206
|
+
[Unreleased]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.6.1...HEAD
|
|
207
|
+
[1.6.1]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.6.0...v1.6.1
|
|
208
|
+
[1.6.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.5.1...v1.6.0
|
|
189
209
|
[1.5.1]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.5.0...v1.5.1
|
|
190
210
|
[1.5.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.4.0...v1.5.0
|
|
191
211
|
[1.4.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.3.2...v1.4.0
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
<p align="center">
|
|
7
7
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-22+-339933?style=flat&logo=node.js&logoColor=white" alt="Node Version"></a>
|
|
8
|
-
<a href="https://astro.build"><img src="https://img.shields.io/badge/Astro-5.x%20%7C%206.x-BC52EE?style=flat&logo=astro&logoColor=white" alt="Astro Version"></a>
|
|
8
|
+
<a href="https://astro.build"><img src="https://img.shields.io/badge/Astro-5.x%20%7C%206.x%20%7C%207.x-BC52EE?style=flat&logo=astro&logoColor=white" alt="Astro Version"></a>
|
|
9
9
|
<a href="https://starlight.astro.build"><img src="https://img.shields.io/badge/Starlight-0.32+-FF5D01?style=flat&logo=astro&logoColor=white" alt="Starlight Version"></a>
|
|
10
10
|
</p>
|
|
11
11
|
|
package/config.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./legacy-html-redirects";
|
|
16
16
|
import { type NorthwesternMermaidOptions, northwesternMermaid } from "./mermaid";
|
|
17
17
|
import { northwesternConfigOptionsSchema, validateSchema } from "./src/config-schema";
|
|
18
|
+
import { type MarkdownConfigLike, withUnifiedProcessor } from "./src/markdown-processor";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Vite plugin that enables `pluginLineNumbers` for both markdown code blocks
|
|
@@ -144,11 +145,11 @@ export type NorthwesternConfigOptions = Omit<AstroUserConfig, "integrations"> &
|
|
|
144
145
|
/**
|
|
145
146
|
* Mermaid diagram support.
|
|
146
147
|
*
|
|
147
|
-
* - `true` (default) — adds `northwesternMermaid()`
|
|
148
|
+
* - `true` (default) — adds `northwesternMermaid()` after `starlight()` with
|
|
148
149
|
* default options (branded colors, toolbar, dark mode). Requires `astro-mermaid`
|
|
149
150
|
* and `mermaid` to be installed.
|
|
150
151
|
* - `false` — disable Mermaid entirely
|
|
151
|
-
* - `object` — adds `northwesternMermaid(options)`
|
|
152
|
+
* - `object` — adds `northwesternMermaid(options)` after `starlight()` with
|
|
152
153
|
* custom options (toolbar, theme overrides, etc.)
|
|
153
154
|
*/
|
|
154
155
|
mermaid?: boolean | NorthwesternMermaidOptions;
|
|
@@ -255,9 +256,8 @@ export function defineNorthwesternConfig(options: NorthwesternConfigOptions): As
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
// Build theme config, handling mermaid dual-path.
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// The theme's built-in mermaid (via addIntegration inside config:setup) runs too late.
|
|
259
|
+
// astro-mermaid 2.1 detects Starlight's active Markdown processor, so the
|
|
260
|
+
// standalone integration is added immediately after Starlight below.
|
|
261
261
|
const themeConfig: NorthwesternThemeConfig = { ...theme };
|
|
262
262
|
let mermaidIntegration: AstroIntegration | undefined;
|
|
263
263
|
const hasAstroMermaid = hasOptionalPackage("astro-mermaid");
|
|
@@ -299,17 +299,17 @@ export function defineNorthwesternConfig(options: NorthwesternConfigOptions): As
|
|
|
299
299
|
};
|
|
300
300
|
const mergedPlugins = mergeStarlightPlugins(northwesternTheme(themeConfig), starlightPlugins, helperPlugins);
|
|
301
301
|
|
|
302
|
-
// Build integration array: before → [mermaid] →
|
|
302
|
+
// Build integration array: before → starlight → [mermaid] → after → [legacy-html-redirects]
|
|
303
303
|
// The legacy-html-redirects integration runs last so it rewrites redirect pages
|
|
304
304
|
// after every other astro:build:done hook has had a chance to emit them.
|
|
305
305
|
const integrations: AstroIntegration[] = [
|
|
306
306
|
...(extraIntegrations?.before ?? []),
|
|
307
|
-
...(mermaidIntegration ? [mermaidIntegration] : []),
|
|
308
307
|
starlight({
|
|
309
308
|
...restStarlightConfig,
|
|
310
309
|
expressiveCode: expressiveCodeConfig,
|
|
311
310
|
plugins: mergedPlugins,
|
|
312
311
|
}),
|
|
312
|
+
...(mermaidIntegration ? [mermaidIntegration] : []),
|
|
313
313
|
...(extraIntegrations?.after ?? []),
|
|
314
314
|
...(legacyHtmlRedirects ? [legacyHtmlRedirectsIntegration()] : []),
|
|
315
315
|
];
|
|
@@ -334,6 +334,14 @@ export function defineNorthwesternConfig(options: NorthwesternConfigOptions): As
|
|
|
334
334
|
return {
|
|
335
335
|
...astroConfig,
|
|
336
336
|
integrations,
|
|
337
|
+
// Pin the Markdown processor to `unified()` here, at `astro.config.*`
|
|
338
|
+
// evaluation time, so `starlight()` and every Starlight plugin observes it
|
|
339
|
+
// from the start. Doing this later from an `astro:config:setup` hook means
|
|
340
|
+
// replacing a processor plugins have already registered against — which
|
|
341
|
+
// silently drops those registrations (see `src/markdown-processor.ts`).
|
|
342
|
+
markdown: withUnifiedProcessor(
|
|
343
|
+
astroConfig.markdown as MarkdownConfigLike | undefined,
|
|
344
|
+
) as AstroUserConfig["markdown"],
|
|
337
345
|
...(mergedRedirects ? { redirects: mergedRedirects } : {}),
|
|
338
346
|
vite: {
|
|
339
347
|
...existingViteConfig,
|
package/dist/config.d.ts
CHANGED
|
@@ -33,11 +33,11 @@ export type NorthwesternConfigOptions = Omit<AstroUserConfig, "integrations"> &
|
|
|
33
33
|
/**
|
|
34
34
|
* Mermaid diagram support.
|
|
35
35
|
*
|
|
36
|
-
* - `true` (default) — adds `northwesternMermaid()`
|
|
36
|
+
* - `true` (default) — adds `northwesternMermaid()` after `starlight()` with
|
|
37
37
|
* default options (branded colors, toolbar, dark mode). Requires `astro-mermaid`
|
|
38
38
|
* and `mermaid` to be installed.
|
|
39
39
|
* - `false` — disable Mermaid entirely
|
|
40
|
-
* - `object` — adds `northwesternMermaid(options)`
|
|
40
|
+
* - `object` — adds `northwesternMermaid(options)` after `starlight()` with
|
|
41
41
|
* custom options (toolbar, theme overrides, etc.)
|
|
42
42
|
*/
|
|
43
43
|
mermaid?: boolean | NorthwesternMermaidOptions;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAErF,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAEH,KAAK,0BAA0B,EAElC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,0BAA0B,EAAuB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAErF,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAEH,KAAK,0BAA0B,EAElC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,0BAA0B,EAAuB,MAAM,WAAW,CAAC;AA4EjF;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACjC,WAAW,EAAE,eAAe,EAC5B,gBAAgB,EAAE,eAAe,EAAE,EACnC,aAAa,EAAE,eAAe,EAAE,GACjC,eAAe,EAAE,CAmBnB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GAAG;IAC5E,wFAAwF;IACxF,SAAS,EAAE,mBAAmB,CAAC;IAE/B;;;;OAIG;IACH,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAEhC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,0BAA0B,CAAC;IAE/C;;;;OAIG;IACH,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAE5B;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,OAAO,GAAG,0BAA0B,CAAC;IAE3D;;;;;OAKG;IACH,YAAY,CAAC,EAAE;QACX,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC5B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;KAC9B,CAAC;CACL,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,eAAe,CAyI5F"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKhE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,0BAA0B;IACvC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IAE9B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,uBAAuB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IAEtC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE5C;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AA6BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,MAAM,GAAE,uBAA4B,GAAG,eAAe,CAyP/F"}
|
package/dist/mermaid.d.ts
CHANGED
|
@@ -68,10 +68,9 @@ export declare const darkMermaidConfig: AstroMermaidOptions;
|
|
|
68
68
|
* **Note:** `defineNorthwesternConfig` handles Mermaid integration ordering.
|
|
69
69
|
* This function is only needed for manual setups.
|
|
70
70
|
*
|
|
71
|
-
* **Must be added
|
|
72
|
-
* `astro-mermaid`
|
|
73
|
-
*
|
|
74
|
-
* `starlight()` causes Mermaid code blocks to be treated as plain code.
|
|
71
|
+
* **Must be added after `starlight()` in the `integrations` array.** This lets
|
|
72
|
+
* `astro-mermaid` extend the active Markdown processor after Starlight and its
|
|
73
|
+
* plugins have configured it.
|
|
75
74
|
*
|
|
76
75
|
* @param options - Merged with Northwestern defaults. Set `toolbar: false` to
|
|
77
76
|
* disable the hover toolbar.
|
|
@@ -84,11 +83,11 @@ export declare const darkMermaidConfig: AstroMermaidOptions;
|
|
|
84
83
|
*
|
|
85
84
|
* export default defineConfig({
|
|
86
85
|
* integrations: [
|
|
87
|
-
* northwesternMermaid(), // Must come before starlight()
|
|
88
86
|
* starlight({
|
|
89
87
|
* plugins: [northwesternTheme()],
|
|
90
88
|
* title: "My Docs",
|
|
91
89
|
* }),
|
|
90
|
+
* northwesternMermaid(), // Must come after starlight()
|
|
92
91
|
* ],
|
|
93
92
|
* });
|
|
94
93
|
* ```
|
package/dist/mermaid.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid.d.ts","sourceRoot":"","sources":["../mermaid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAIzD;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA2B,SAAQ,mBAAmB;IACnE;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,MAAM,CAAC;AAwUvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,uBAAuB,GAAG,mBAAmB,CAmBlG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,mBAA8D,CAAC;AAElG;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,mBAA6D,CAAC;AAE9F
|
|
1
|
+
{"version":3,"file":"mermaid.d.ts","sourceRoot":"","sources":["../mermaid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAIzD;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA2B,SAAQ,mBAAmB;IACnE;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,MAAM,CAAC;AAwUvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,uBAAuB,GAAG,mBAAmB,CAmBlG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,mBAA8D,CAAC;AAElG;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,mBAA6D,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,0BAA+B,GAAG,gBAAgB,CAsD9F"}
|
|
@@ -12,9 +12,6 @@ export declare const nonEmptyStringSchema: z.ZodString;
|
|
|
12
12
|
* With `northwesternTheme()` directly, pass it as the function argument.
|
|
13
13
|
*/
|
|
14
14
|
export declare const northwesternThemeConfigSchema: z.ZodObject<{
|
|
15
|
-
/**
|
|
16
|
-
* Homepage hero layout configuration.
|
|
17
|
-
*/
|
|
18
15
|
homepage: z.ZodOptional<z.ZodObject<{
|
|
19
16
|
layout: z.ZodOptional<z.ZodEnum<{
|
|
20
17
|
centered: "centered";
|
|
@@ -23,20 +20,7 @@ export declare const northwesternThemeConfigSchema: z.ZodObject<{
|
|
|
23
20
|
showTitle: z.ZodOptional<z.ZodBoolean>;
|
|
24
21
|
imageWidth: z.ZodOptional<z.ZodString>;
|
|
25
22
|
}, z.core.$strict>>;
|
|
26
|
-
/**
|
|
27
|
-
* Mermaid diagram support.
|
|
28
|
-
*
|
|
29
|
-
* - `true`: auto-detect Mermaid packages and enable support when available
|
|
30
|
-
* - `false`: disable Mermaid integration entirely
|
|
31
|
-
* - `object`: merge custom Mermaid options with Northwestern defaults
|
|
32
|
-
*/
|
|
33
23
|
mermaid: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
|
|
34
|
-
/**
|
|
35
|
-
* Open Graph image generation.
|
|
36
|
-
*
|
|
37
|
-
* Generates a branded 1200x630 image for each docs page when `site`
|
|
38
|
-
* is configured in `astro.config.ts`.
|
|
39
|
-
*/
|
|
40
24
|
ogImage: z.ZodOptional<z.ZodBoolean>;
|
|
41
25
|
}, z.core.$strict>;
|
|
42
26
|
/**
|
|
@@ -46,11 +30,6 @@ export declare const northwesternThemeConfigSchema: z.ZodObject<{
|
|
|
46
30
|
* allows additional upstream `astro-mermaid` options to pass through unchanged.
|
|
47
31
|
*/
|
|
48
32
|
export declare const northwesternMermaidOptionsSchema: z.ZodObject<{
|
|
49
|
-
/**
|
|
50
|
-
* Show the hover toolbar on rendered diagrams.
|
|
51
|
-
*
|
|
52
|
-
* The toolbar provides fullscreen, download, and copy-source actions.
|
|
53
|
-
*/
|
|
54
33
|
toolbar: z.ZodOptional<z.ZodBoolean>;
|
|
55
34
|
}, z.core.$loose>;
|
|
56
35
|
/**
|
|
@@ -69,20 +48,8 @@ export declare const legacyHtmlRedirectsOptionsSchema: z.ZodObject<{
|
|
|
69
48
|
* the rest of Astro's top-level config to pass through untouched.
|
|
70
49
|
*/
|
|
71
50
|
export declare const northwesternConfigOptionsSchema: z.ZodObject<{
|
|
72
|
-
/**
|
|
73
|
-
* Full Starlight configuration object.
|
|
74
|
-
*
|
|
75
|
-
* This is forwarded to `starlight()` after Northwestern defaults and
|
|
76
|
-
* helper-managed integration ordering are applied.
|
|
77
|
-
*/
|
|
78
51
|
starlight: z.ZodObject<{}, z.core.$loose>;
|
|
79
|
-
/**
|
|
80
|
-
* Northwestern theme plugin options.
|
|
81
|
-
*/
|
|
82
52
|
theme: z.ZodOptional<z.ZodObject<{
|
|
83
|
-
/**
|
|
84
|
-
* Homepage hero layout configuration.
|
|
85
|
-
*/
|
|
86
53
|
homepage: z.ZodOptional<z.ZodObject<{
|
|
87
54
|
layout: z.ZodOptional<z.ZodEnum<{
|
|
88
55
|
centered: "centered";
|
|
@@ -91,66 +58,18 @@ export declare const northwesternConfigOptionsSchema: z.ZodObject<{
|
|
|
91
58
|
showTitle: z.ZodOptional<z.ZodBoolean>;
|
|
92
59
|
imageWidth: z.ZodOptional<z.ZodString>;
|
|
93
60
|
}, z.core.$strict>>;
|
|
94
|
-
/**
|
|
95
|
-
* Mermaid diagram support.
|
|
96
|
-
*
|
|
97
|
-
* - `true`: auto-detect Mermaid packages and enable support when available
|
|
98
|
-
* - `false`: disable Mermaid integration entirely
|
|
99
|
-
* - `object`: merge custom Mermaid options with Northwestern defaults
|
|
100
|
-
*/
|
|
101
61
|
mermaid: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
|
|
102
|
-
/**
|
|
103
|
-
* Open Graph image generation.
|
|
104
|
-
*
|
|
105
|
-
* Generates a branded 1200x630 image for each docs page when `site`
|
|
106
|
-
* is configured in `astro.config.ts`.
|
|
107
|
-
*/
|
|
108
62
|
ogImage: z.ZodOptional<z.ZodBoolean>;
|
|
109
63
|
}, z.core.$strict>>;
|
|
110
|
-
/**
|
|
111
|
-
* Mermaid diagram support.
|
|
112
|
-
*
|
|
113
|
-
* - `true`: add Northwestern Mermaid with defaults
|
|
114
|
-
* - `false`: disable Mermaid
|
|
115
|
-
* - `object`: add Northwestern Mermaid with custom options
|
|
116
|
-
*/
|
|
117
64
|
mermaid: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
118
|
-
/**
|
|
119
|
-
* Show the hover toolbar on rendered diagrams.
|
|
120
|
-
*
|
|
121
|
-
* The toolbar provides fullscreen, download, and copy-source actions.
|
|
122
|
-
*/
|
|
123
65
|
toolbar: z.ZodOptional<z.ZodBoolean>;
|
|
124
66
|
}, z.core.$loose>]>>;
|
|
125
|
-
/**
|
|
126
|
-
* Additional Starlight plugins to register after the Northwestern theme.
|
|
127
|
-
*/
|
|
128
67
|
plugins: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
129
|
-
/**
|
|
130
|
-
* Generate `.html` redirects for every content page and rewrite the
|
|
131
|
-
* emitted redirect pages so the URL hash is preserved on forward.
|
|
132
|
-
*
|
|
133
|
-
* - `true`: scan `src/content/docs` with the defaults
|
|
134
|
-
* - `false` (default): do nothing
|
|
135
|
-
* - `object`: scan a custom content directory
|
|
136
|
-
*/
|
|
137
68
|
legacyHtmlRedirects: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
138
69
|
contentDir: z.ZodOptional<z.ZodString>;
|
|
139
70
|
}, z.core.$strict>]>>;
|
|
140
|
-
/**
|
|
141
|
-
* Escape hatch for advanced integration ordering.
|
|
142
|
-
*
|
|
143
|
-
* Use `before` for integrations that must run before Mermaid/Starlight,
|
|
144
|
-
* and `after` for integrations that should be appended after Starlight.
|
|
145
|
-
*/
|
|
146
71
|
integrations: z.ZodOptional<z.ZodObject<{
|
|
147
|
-
/**
|
|
148
|
-
* Integrations added before Mermaid and Starlight.
|
|
149
|
-
*/
|
|
150
72
|
before: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
151
|
-
/**
|
|
152
|
-
* Integrations added after Starlight.
|
|
153
|
-
*/
|
|
154
73
|
after: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
155
74
|
}, z.core.$strict>>;
|
|
156
75
|
}, z.core.$loose>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["../../src/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,aAIoD,CAAC;AA6DtF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B
|
|
1
|
+
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["../../src/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,aAIoD,CAAC;AA6DtF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;kBAoCpC,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC;;iBAcvC,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC;;kBAcvC,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;iBAgFtC,CAAC;AA2BP;;GAEG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAQxF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for Astro's `markdown.processor` API.
|
|
3
|
+
*
|
|
4
|
+
* Astro 6.4 introduced `markdown.processor`, and Astro 7 changed its default
|
|
5
|
+
* from `unified()` (`@astrojs/markdown-remark`) to `satteri()`
|
|
6
|
+
* (`@astrojs/markdown-satteri`). Both are *live objects*: integrations and
|
|
7
|
+
* Starlight plugins extend the Markdown pipeline by mutating the processor
|
|
8
|
+
* instance they are handed — pushing into `processor.options.*Plugins`, or
|
|
9
|
+
* wrapping `processor.createRenderer`.
|
|
10
|
+
*
|
|
11
|
+
* That makes processor **replacement** destructive. Anything registered on the
|
|
12
|
+
* outgoing object is dropped, and because most registrations are optional
|
|
13
|
+
* behaviour, the failure is silent: `starlight-links-validator` swapped for a
|
|
14
|
+
* fresh processor simply validates zero links and reports success.
|
|
15
|
+
*
|
|
16
|
+
* The theme therefore installs a `unified()` processor in
|
|
17
|
+
* `defineNorthwesternConfig()` — at `astro.config.*` evaluation time, before
|
|
18
|
+
* `starlight()` is constructed and before any plugin can observe the default —
|
|
19
|
+
* and from then on only ever *extends* the processor it is given.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Structural shape of `markdown.processor`.
|
|
23
|
+
*
|
|
24
|
+
* Declared locally because `AstroUserConfig["markdown"]` only gained the
|
|
25
|
+
* `processor` key in Astro 6.4, and the theme supports Astro 5 and 6 as well.
|
|
26
|
+
*/
|
|
27
|
+
export interface MarkdownProcessorLike {
|
|
28
|
+
name: string;
|
|
29
|
+
options?: {
|
|
30
|
+
remarkPlugins?: unknown[];
|
|
31
|
+
rehypePlugins?: unknown[];
|
|
32
|
+
mdastPlugins?: unknown[];
|
|
33
|
+
hastPlugins?: unknown[];
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
createRenderer?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Structural shape of `markdown` in an Astro config, including the keys the
|
|
40
|
+
* installed Astro version may not know about yet.
|
|
41
|
+
*/
|
|
42
|
+
export interface MarkdownConfigLike {
|
|
43
|
+
processor?: MarkdownProcessorLike;
|
|
44
|
+
remarkPlugins?: unknown[];
|
|
45
|
+
rehypePlugins?: unknown[];
|
|
46
|
+
remarkRehype?: Record<string, unknown>;
|
|
47
|
+
gfm?: boolean;
|
|
48
|
+
smartypants?: unknown;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** A `unified()` processor from `@astrojs/markdown-remark`. */
|
|
52
|
+
export declare function isUnifiedProcessor(processor: MarkdownProcessorLike | undefined): processor is MarkdownProcessorLike & {
|
|
53
|
+
options: {
|
|
54
|
+
rehypePlugins: unknown[];
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Return a `markdown` config that is guaranteed to use a `unified()` processor.
|
|
59
|
+
*
|
|
60
|
+
* Call this while building the Astro config — before `starlight()` runs — so no
|
|
61
|
+
* plugin ever observes the Sätteri default. A processor the consumer supplied
|
|
62
|
+
* themselves is respected as-is (a non-`unified` one earns a warning, because
|
|
63
|
+
* several Starlight plugins the theme ships with only support `unified`).
|
|
64
|
+
*
|
|
65
|
+
* User-supplied `remarkPlugins` / `rehypePlugins` are deliberately *not* copied
|
|
66
|
+
* into the processor: Astro migrates those deprecated top-level arrays onto a
|
|
67
|
+
* `unified` processor itself (exactly once, tracked internally), and on Astro
|
|
68
|
+
* versions without `markdown.processor` they are the whole pipeline. Baking
|
|
69
|
+
* copies in here would run them twice.
|
|
70
|
+
*/
|
|
71
|
+
export declare function withUnifiedProcessor(markdown: MarkdownConfigLike | undefined, warn?: (message: string) => void): MarkdownConfigLike;
|
|
72
|
+
/**
|
|
73
|
+
* Register a rehype plugin on the active Markdown pipeline.
|
|
74
|
+
*
|
|
75
|
+
* Returns a `markdown` config patch to hand to `updateConfig()`, or `undefined`
|
|
76
|
+
* when the plugin was added to the live processor and no config update is
|
|
77
|
+
* needed. Mutating `processor.options` is how Astro itself extends a processor
|
|
78
|
+
* after config resolution — the object identity survives config validation
|
|
79
|
+
* precisely so that late registrations still apply.
|
|
80
|
+
*
|
|
81
|
+
* A non-`unified` processor is left alone. Replacing it here is what used to
|
|
82
|
+
* discard Sätteri-registered plugins from unrelated Starlight plugins, so the
|
|
83
|
+
* theme warns instead of swapping behind their back.
|
|
84
|
+
*/
|
|
85
|
+
export declare function addRehypePlugin(markdown: MarkdownConfigLike | undefined, plugin: unknown, warn?: (message: string) => void): MarkdownConfigLike | undefined;
|
|
86
|
+
//# sourceMappingURL=markdown-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-processor.d.ts","sourceRoot":"","sources":["../../src/markdown-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QACN,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC;QAC1B,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC;QAC1B,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;QACzB,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;QACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KAC1B,CAAC;IACF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAC/B,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,qBAAqB,GAAG,SAAS,GAC7C,SAAS,IAAI,qBAAqB,GAAG;IAAE,OAAO,EAAE;QAAE,aAAa,EAAE,OAAO,EAAE,CAAA;KAAE,CAAA;CAAE,CAEhF;AAkBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,kBAAkB,GAAG,SAAS,EACxC,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAmB,GAC/C,kBAAkB,CAwBpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,kBAAkB,GAAG,SAAS,EACxC,MAAM,EAAE,OAAO,EACf,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAmB,GAC/C,kBAAkB,GAAG,SAAS,CA6BhC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rehype-table-scroll.d.ts","sourceRoot":"","sources":["../../src/rehype-table-scroll.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAkB,IAAI,EAAE,MAAM,MAAM,CAAC;AAgCjD,MAAM,CAAC,OAAO,UAAU,iBAAiB,
|
|
1
|
+
{"version":3,"file":"rehype-table-scroll.d.ts","sourceRoot":"","sources":["../../src/rehype-table-scroll.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAkB,IAAI,EAAE,MAAM,MAAM,CAAC;AAgCjD,MAAM,CAAC,OAAO,UAAU,iBAAiB,WACvB,IAAI,UACrB"}
|
package/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import type { StarlightPlugin } from "@astrojs/starlight/types";
|
|
7
7
|
import { nonEmptyStringSchema, northwesternThemeConfigSchema, validateSchema } from "./src/config-schema";
|
|
8
|
+
import { addRehypePlugin, type MarkdownConfigLike } from "./src/markdown-processor";
|
|
8
9
|
import rehypeTableScroll from "./src/rehype-table-scroll";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -202,11 +203,18 @@ export default function northwesternTheme(config: NorthwesternThemeConfig = {}):
|
|
|
202
203
|
addIntegration({
|
|
203
204
|
name: "northwestern-theme-config",
|
|
204
205
|
hooks: {
|
|
205
|
-
"astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
|
|
206
|
+
"astro:config:setup": ({ config: astroConfig, updateConfig: updateAstroConfig }) => {
|
|
207
|
+
// Extend whichever Markdown pipeline is active — never replace it.
|
|
208
|
+
// `defineNorthwesternConfig()` has already pinned a `unified()`
|
|
209
|
+
// processor before any plugin could register against it.
|
|
210
|
+
const markdownConfig = addRehypePlugin(
|
|
211
|
+
astroConfig.markdown as MarkdownConfigLike | undefined,
|
|
212
|
+
rehypeTableScroll,
|
|
213
|
+
(message) => logger.warn(message),
|
|
214
|
+
);
|
|
215
|
+
|
|
206
216
|
updateAstroConfig({
|
|
207
|
-
markdown: {
|
|
208
|
-
rehypePlugins: [rehypeTableScroll],
|
|
209
|
-
},
|
|
217
|
+
...(markdownConfig ? { markdown: markdownConfig } : {}),
|
|
210
218
|
vite: {
|
|
211
219
|
plugins: [
|
|
212
220
|
{
|
package/mermaid.ts
CHANGED
|
@@ -421,10 +421,9 @@ export const darkMermaidConfig: AstroMermaidOptions = createNorthwesternMermaidC
|
|
|
421
421
|
* **Note:** `defineNorthwesternConfig` handles Mermaid integration ordering.
|
|
422
422
|
* This function is only needed for manual setups.
|
|
423
423
|
*
|
|
424
|
-
* **Must be added
|
|
425
|
-
* `astro-mermaid`
|
|
426
|
-
*
|
|
427
|
-
* `starlight()` causes Mermaid code blocks to be treated as plain code.
|
|
424
|
+
* **Must be added after `starlight()` in the `integrations` array.** This lets
|
|
425
|
+
* `astro-mermaid` extend the active Markdown processor after Starlight and its
|
|
426
|
+
* plugins have configured it.
|
|
428
427
|
*
|
|
429
428
|
* @param options - Merged with Northwestern defaults. Set `toolbar: false` to
|
|
430
429
|
* disable the hover toolbar.
|
|
@@ -437,11 +436,11 @@ export const darkMermaidConfig: AstroMermaidOptions = createNorthwesternMermaidC
|
|
|
437
436
|
*
|
|
438
437
|
* export default defineConfig({
|
|
439
438
|
* integrations: [
|
|
440
|
-
* northwesternMermaid(), // Must come before starlight()
|
|
441
439
|
* starlight({
|
|
442
440
|
* plugins: [northwesternTheme()],
|
|
443
441
|
* title: "My Docs",
|
|
444
442
|
* }),
|
|
443
|
+
* northwesternMermaid(), // Must come after starlight()
|
|
445
444
|
* ],
|
|
446
445
|
* });
|
|
447
446
|
* ```
|
|
@@ -478,6 +477,7 @@ export function northwesternMermaid(options: NorthwesternMermaidOptions = {}): A
|
|
|
478
477
|
hooks: {
|
|
479
478
|
async "astro:config:setup"(params) {
|
|
480
479
|
const { default: mermaid } = await mermaidModulePromise;
|
|
480
|
+
|
|
481
481
|
const mermaidIntegration = mermaid({
|
|
482
482
|
...lightConfig,
|
|
483
483
|
...overrides,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nu-appdev/northwestern-starlight-theme",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "A Northwestern-branded theme for Astro Starlight",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Danny Foster <danny@northwestern.edu>",
|
|
@@ -68,11 +68,12 @@
|
|
|
68
68
|
"src/"
|
|
69
69
|
],
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@
|
|
71
|
+
"@astrojs/markdown-remark": "^7.2.2",
|
|
72
|
+
"@expressive-code/plugin-line-numbers": "^0.44.1",
|
|
72
73
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
73
74
|
"khroma": "^2.1.0",
|
|
74
|
-
"satori": "^0.
|
|
75
|
-
"zod": "^4.3
|
|
75
|
+
"satori": "^0.29.0",
|
|
76
|
+
"zod": "^4.4.3"
|
|
76
77
|
},
|
|
77
78
|
"peerDependencies": {
|
|
78
79
|
"@astrojs/starlight": ">=0.32.0",
|
|
@@ -89,8 +90,8 @@
|
|
|
89
90
|
}
|
|
90
91
|
},
|
|
91
92
|
"devDependencies": {
|
|
92
|
-
"@types/hast": "^3.0.
|
|
93
|
-
"typescript": "^
|
|
93
|
+
"@types/hast": "^3.0.5",
|
|
94
|
+
"typescript": "^7.0.2"
|
|
94
95
|
},
|
|
95
96
|
"scripts": {
|
|
96
97
|
"build": "tsc -p tsconfig.build.json --noCheck"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for Astro's `markdown.processor` API.
|
|
3
|
+
*
|
|
4
|
+
* Astro 6.4 introduced `markdown.processor`, and Astro 7 changed its default
|
|
5
|
+
* from `unified()` (`@astrojs/markdown-remark`) to `satteri()`
|
|
6
|
+
* (`@astrojs/markdown-satteri`). Both are *live objects*: integrations and
|
|
7
|
+
* Starlight plugins extend the Markdown pipeline by mutating the processor
|
|
8
|
+
* instance they are handed — pushing into `processor.options.*Plugins`, or
|
|
9
|
+
* wrapping `processor.createRenderer`.
|
|
10
|
+
*
|
|
11
|
+
* That makes processor **replacement** destructive. Anything registered on the
|
|
12
|
+
* outgoing object is dropped, and because most registrations are optional
|
|
13
|
+
* behaviour, the failure is silent: `starlight-links-validator` swapped for a
|
|
14
|
+
* fresh processor simply validates zero links and reports success.
|
|
15
|
+
*
|
|
16
|
+
* The theme therefore installs a `unified()` processor in
|
|
17
|
+
* `defineNorthwesternConfig()` — at `astro.config.*` evaluation time, before
|
|
18
|
+
* `starlight()` is constructed and before any plugin can observe the default —
|
|
19
|
+
* and from then on only ever *extends* the processor it is given.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { unified } from "@astrojs/markdown-remark";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Structural shape of `markdown.processor`.
|
|
26
|
+
*
|
|
27
|
+
* Declared locally because `AstroUserConfig["markdown"]` only gained the
|
|
28
|
+
* `processor` key in Astro 6.4, and the theme supports Astro 5 and 6 as well.
|
|
29
|
+
*/
|
|
30
|
+
export interface MarkdownProcessorLike {
|
|
31
|
+
name: string;
|
|
32
|
+
options?: {
|
|
33
|
+
remarkPlugins?: unknown[];
|
|
34
|
+
rehypePlugins?: unknown[];
|
|
35
|
+
mdastPlugins?: unknown[];
|
|
36
|
+
hastPlugins?: unknown[];
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
createRenderer?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Structural shape of `markdown` in an Astro config, including the keys the
|
|
44
|
+
* installed Astro version may not know about yet.
|
|
45
|
+
*/
|
|
46
|
+
export interface MarkdownConfigLike {
|
|
47
|
+
processor?: MarkdownProcessorLike;
|
|
48
|
+
remarkPlugins?: unknown[];
|
|
49
|
+
rehypePlugins?: unknown[];
|
|
50
|
+
remarkRehype?: Record<string, unknown>;
|
|
51
|
+
gfm?: boolean;
|
|
52
|
+
smartypants?: unknown;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A `unified()` processor from `@astrojs/markdown-remark`. */
|
|
57
|
+
export function isUnifiedProcessor(
|
|
58
|
+
processor: MarkdownProcessorLike | undefined,
|
|
59
|
+
): processor is MarkdownProcessorLike & { options: { rehypePlugins: unknown[] } } {
|
|
60
|
+
return processor?.name === "unified" && Array.isArray(processor.options?.rehypePlugins);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Plugins registered against a non-`unified` processor (Sätteri's `mdastPlugins`
|
|
65
|
+
* / `hastPlugins`).
|
|
66
|
+
*
|
|
67
|
+
* These use a different plugin system than remark/rehype — a visitor object from
|
|
68
|
+
* `defineHastPlugin()`, not a unified transformer — so they cannot be carried
|
|
69
|
+
* across when a processor is replaced. Counting them is how the theme detects
|
|
70
|
+
* that a replacement would silently drop somebody else's work.
|
|
71
|
+
*/
|
|
72
|
+
function countProcessorPlugins(processor: MarkdownProcessorLike): number {
|
|
73
|
+
const { mdastPlugins, hastPlugins } = processor.options ?? {};
|
|
74
|
+
return (
|
|
75
|
+
(Array.isArray(mdastPlugins) ? mdastPlugins.length : 0) + (Array.isArray(hastPlugins) ? hastPlugins.length : 0)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return a `markdown` config that is guaranteed to use a `unified()` processor.
|
|
81
|
+
*
|
|
82
|
+
* Call this while building the Astro config — before `starlight()` runs — so no
|
|
83
|
+
* plugin ever observes the Sätteri default. A processor the consumer supplied
|
|
84
|
+
* themselves is respected as-is (a non-`unified` one earns a warning, because
|
|
85
|
+
* several Starlight plugins the theme ships with only support `unified`).
|
|
86
|
+
*
|
|
87
|
+
* User-supplied `remarkPlugins` / `rehypePlugins` are deliberately *not* copied
|
|
88
|
+
* into the processor: Astro migrates those deprecated top-level arrays onto a
|
|
89
|
+
* `unified` processor itself (exactly once, tracked internally), and on Astro
|
|
90
|
+
* versions without `markdown.processor` they are the whole pipeline. Baking
|
|
91
|
+
* copies in here would run them twice.
|
|
92
|
+
*/
|
|
93
|
+
export function withUnifiedProcessor(
|
|
94
|
+
markdown: MarkdownConfigLike | undefined,
|
|
95
|
+
warn: (message: string) => void = console.warn,
|
|
96
|
+
): MarkdownConfigLike {
|
|
97
|
+
const config = markdown ?? {};
|
|
98
|
+
|
|
99
|
+
if (config.processor) {
|
|
100
|
+
if (!isUnifiedProcessor(config.processor)) {
|
|
101
|
+
warn(
|
|
102
|
+
`[northwestern-starlight-theme] \`markdown.processor\` is set to \`${config.processor.name}()\`. ` +
|
|
103
|
+
"The theme's scrollable tables and Starlight plugins such as `starlight-image-zoom` require the " +
|
|
104
|
+
"`unified()` processor from `@astrojs/markdown-remark`. Remove `markdown.processor` to let the " +
|
|
105
|
+
"theme configure it, or set it to `unified({ ... })`.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...config,
|
|
114
|
+
processor: unified({
|
|
115
|
+
remarkRehype: config.remarkRehype,
|
|
116
|
+
gfm: config.gfm,
|
|
117
|
+
smartypants: config.smartypants,
|
|
118
|
+
} as Parameters<typeof unified>[0]) as MarkdownProcessorLike,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register a rehype plugin on the active Markdown pipeline.
|
|
124
|
+
*
|
|
125
|
+
* Returns a `markdown` config patch to hand to `updateConfig()`, or `undefined`
|
|
126
|
+
* when the plugin was added to the live processor and no config update is
|
|
127
|
+
* needed. Mutating `processor.options` is how Astro itself extends a processor
|
|
128
|
+
* after config resolution — the object identity survives config validation
|
|
129
|
+
* precisely so that late registrations still apply.
|
|
130
|
+
*
|
|
131
|
+
* A non-`unified` processor is left alone. Replacing it here is what used to
|
|
132
|
+
* discard Sätteri-registered plugins from unrelated Starlight plugins, so the
|
|
133
|
+
* theme warns instead of swapping behind their back.
|
|
134
|
+
*/
|
|
135
|
+
export function addRehypePlugin(
|
|
136
|
+
markdown: MarkdownConfigLike | undefined,
|
|
137
|
+
plugin: unknown,
|
|
138
|
+
warn: (message: string) => void = console.warn,
|
|
139
|
+
): MarkdownConfigLike | undefined {
|
|
140
|
+
const config = markdown ?? {};
|
|
141
|
+
const processor = config.processor;
|
|
142
|
+
|
|
143
|
+
// Astro < 6.4: the top-level plugin arrays are the entire pipeline.
|
|
144
|
+
if (!processor) {
|
|
145
|
+
return { rehypePlugins: [...(config.rehypePlugins ?? []), plugin] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isUnifiedProcessor(processor)) {
|
|
149
|
+
if (!processor.options.rehypePlugins.includes(plugin)) {
|
|
150
|
+
processor.options.rehypePlugins.push(plugin);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const registered = countProcessorPlugins(processor);
|
|
157
|
+
warn(
|
|
158
|
+
`[northwestern-starlight-theme] Markdown is rendered by the \`${processor.name}()\` processor, which the theme ` +
|
|
159
|
+
"does not extend. Scrollable table wrappers are disabled for this build" +
|
|
160
|
+
(registered > 0
|
|
161
|
+
? `, and converting the processor would drop ${registered} plugin(s) other integrations registered on it. `
|
|
162
|
+
: ". ") +
|
|
163
|
+
"Use `defineNorthwesternConfig()` (it installs a `unified()` processor for you) or set " +
|
|
164
|
+
"`markdown.processor: unified({ ... })` from `@astrojs/markdown-remark`.",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { installKeyboardAndFocus } from "./focus";
|
|
10
10
|
import { buildOverlay } from "./overlay";
|
|
11
11
|
import { createPanZoomController } from "./pan-zoom";
|
|
12
|
-
import { downloadDiagramSvg, flashSuccess, showToast, writeToClipboard } from "./ui";
|
|
12
|
+
import { downloadDiagramPng, downloadDiagramSvg, flashSuccess, showToast, writeToClipboard } from "./ui";
|
|
13
13
|
|
|
14
14
|
const OPEN_SCALE = "scale(0.97)";
|
|
15
15
|
const OPEN_TRANSITION = "transform 300ms cubic-bezier(0.16, 1, 0.3, 1)";
|
|
@@ -74,6 +74,27 @@ export function openFullscreen(
|
|
|
74
74
|
downloadDiagramSvg(diagramSvg, diagramContainer);
|
|
75
75
|
flashSuccess(button, "Downloaded!");
|
|
76
76
|
break;
|
|
77
|
+
case "download-png": {
|
|
78
|
+
const originalTitle = button.title;
|
|
79
|
+
button.disabled = true;
|
|
80
|
+
button.title = "Generating PNG…";
|
|
81
|
+
button.setAttribute("aria-busy", "true");
|
|
82
|
+
|
|
83
|
+
void downloadDiagramPng(diagramSvg, diagramContainer)
|
|
84
|
+
.then(() => {
|
|
85
|
+
button.title = originalTitle;
|
|
86
|
+
flashSuccess(button, "Downloaded!");
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {
|
|
89
|
+
button.title = originalTitle;
|
|
90
|
+
showToast(viewport, "PNG download failed");
|
|
91
|
+
})
|
|
92
|
+
.finally(() => {
|
|
93
|
+
button.disabled = false;
|
|
94
|
+
button.removeAttribute("aria-busy");
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
77
98
|
case "copy-svg":
|
|
78
99
|
writeToClipboard(diagramSvg.outerHTML, button);
|
|
79
100
|
showToast(viewport, "SVG copied to clipboard");
|
|
@@ -72,6 +72,9 @@ function buildControlsBar(diagramContainer: HTMLElement) {
|
|
|
72
72
|
bar.appendChild(separator);
|
|
73
73
|
|
|
74
74
|
bar.appendChild(createActionButton("Download SVG", "download-svg", ICON_PATHS.download, OVERLAY_BTN_CLASS));
|
|
75
|
+
bar.appendChild(
|
|
76
|
+
createActionButton("Download high-resolution PNG", "download-png", ICON_PATHS.image, OVERLAY_BTN_CLASS),
|
|
77
|
+
);
|
|
75
78
|
bar.appendChild(createActionButton("Copy SVG", "copy-svg", ICON_PATHS.copy, OVERLAY_BTN_CLASS));
|
|
76
79
|
|
|
77
80
|
const diagramSource = diagramSources.get(diagramContainer);
|
|
@@ -24,6 +24,7 @@ export const ICON_PATHS = {
|
|
|
24
24
|
"M8 4H6a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2v-2",
|
|
25
25
|
"M16 4h2a2 2 0 012 2v6a2 2 0 01-2 2h-8a2 2 0 01-2-2V6a2 2 0 012-2",
|
|
26
26
|
],
|
|
27
|
+
image: ["M3 5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V5z", "M8.5 8.5h.01", "M21 15l-5-5L5 21"],
|
|
27
28
|
code: ["M16 18l6-6-6-6", "M8 6l-6 6 6 6"],
|
|
28
29
|
check: ["M20 6L9 17l-5-5"],
|
|
29
30
|
} as const;
|
|
@@ -210,6 +211,20 @@ export async function writeToClipboard(text: string, triggerButton: HTMLButtonEl
|
|
|
210
211
|
// SVG download
|
|
211
212
|
// ---------------------------------------------------------------------------
|
|
212
213
|
|
|
214
|
+
type DownloadExtension = "png" | "svg";
|
|
215
|
+
|
|
216
|
+
function downloadBlob(blob: Blob, filename: string): void {
|
|
217
|
+
const url = URL.createObjectURL(blob);
|
|
218
|
+
const anchor = document.createElement("a");
|
|
219
|
+
anchor.href = url;
|
|
220
|
+
anchor.download = filename;
|
|
221
|
+
anchor.hidden = true;
|
|
222
|
+
document.body.appendChild(anchor);
|
|
223
|
+
anchor.click();
|
|
224
|
+
anchor.remove();
|
|
225
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
226
|
+
}
|
|
227
|
+
|
|
213
228
|
/**
|
|
214
229
|
* Download the diagram SVG with a descriptive filename derived from
|
|
215
230
|
* the site title, page slug, and diagram type.
|
|
@@ -218,12 +233,171 @@ export function downloadDiagramSvg(svg: SVGElement, container: Element): void {
|
|
|
218
233
|
const clone = svg.cloneNode(true) as SVGElement;
|
|
219
234
|
clone.setAttribute("xmlns", SVG_NS);
|
|
220
235
|
const blob = new Blob([clone.outerHTML], { type: "image/svg+xml" });
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
236
|
+
downloadBlob(blob, buildDownloadFilename(container, "svg"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// PNG download
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
const PNG_TARGET_SCALE = 4;
|
|
244
|
+
const MAX_CANVAS_DIMENSION = 8192;
|
|
245
|
+
const MAX_CANVAS_AREA = 16_000_000;
|
|
246
|
+
|
|
247
|
+
const RASTER_STYLE_PROPERTIES = [
|
|
248
|
+
"background-color",
|
|
249
|
+
"border-color",
|
|
250
|
+
"border-style",
|
|
251
|
+
"border-width",
|
|
252
|
+
"color",
|
|
253
|
+
"display",
|
|
254
|
+
"dominant-baseline",
|
|
255
|
+
"fill",
|
|
256
|
+
"fill-opacity",
|
|
257
|
+
"fill-rule",
|
|
258
|
+
"font-family",
|
|
259
|
+
"font-size",
|
|
260
|
+
"font-style",
|
|
261
|
+
"font-weight",
|
|
262
|
+
"letter-spacing",
|
|
263
|
+
"line-height",
|
|
264
|
+
"opacity",
|
|
265
|
+
"overflow",
|
|
266
|
+
"paint-order",
|
|
267
|
+
"shape-rendering",
|
|
268
|
+
"stroke",
|
|
269
|
+
"stroke-dasharray",
|
|
270
|
+
"stroke-dashoffset",
|
|
271
|
+
"stroke-linecap",
|
|
272
|
+
"stroke-linejoin",
|
|
273
|
+
"stroke-miterlimit",
|
|
274
|
+
"stroke-opacity",
|
|
275
|
+
"stroke-width",
|
|
276
|
+
"text-align",
|
|
277
|
+
"text-anchor",
|
|
278
|
+
"text-decoration",
|
|
279
|
+
"text-rendering",
|
|
280
|
+
"visibility",
|
|
281
|
+
"white-space",
|
|
282
|
+
"word-spacing",
|
|
283
|
+
] as const;
|
|
284
|
+
|
|
285
|
+
function parseSvgDimensions(svg: SVGElement): { width: number; height: number } {
|
|
286
|
+
const viewBox = svg.getAttribute("viewBox");
|
|
287
|
+
if (viewBox) {
|
|
288
|
+
const [, , width, height] = viewBox.split(/[\s,]+/).map(Number);
|
|
289
|
+
if (width > 0 && height > 0) return { width, height };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const bounds = svg.getBoundingClientRect();
|
|
293
|
+
return {
|
|
294
|
+
width: bounds.width > 0 ? bounds.width : 800,
|
|
295
|
+
height: bounds.height > 0 ? bounds.height : 600,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function calculatePngScale(width: number, height: number): number {
|
|
300
|
+
return Math.min(
|
|
301
|
+
PNG_TARGET_SCALE,
|
|
302
|
+
MAX_CANVAS_DIMENSION / width,
|
|
303
|
+
MAX_CANVAS_DIMENSION / height,
|
|
304
|
+
Math.sqrt(MAX_CANVAS_AREA / (width * height)),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function inlineRasterStyles(source: SVGElement, clone: SVGElement): void {
|
|
309
|
+
const sourceElements = [source, ...source.querySelectorAll("*")];
|
|
310
|
+
const cloneElements = [clone, ...clone.querySelectorAll("*")];
|
|
311
|
+
|
|
312
|
+
sourceElements.forEach((sourceElement, index) => {
|
|
313
|
+
const cloneElement = cloneElements[index] as SVGElement | HTMLElement | undefined;
|
|
314
|
+
if (!cloneElement || !("style" in cloneElement)) return;
|
|
315
|
+
|
|
316
|
+
const computedStyle = getComputedStyle(sourceElement);
|
|
317
|
+
for (const property of RASTER_STYLE_PROPERTIES) {
|
|
318
|
+
const value = computedStyle.getPropertyValue(property);
|
|
319
|
+
if (value) cloneElement.style.setProperty(property, value);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isTransparentColor(color: string): boolean {
|
|
325
|
+
return color === "transparent" || /,\s*0(?:\.0+)?\s*\)$/.test(color) || /\/\s*0(?:\.0+)?%?\s*\)$/.test(color);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function resolveMermaidCanvasColor(): string {
|
|
329
|
+
const mode = document.documentElement.dataset.theme === "light" ? "light" : "dark";
|
|
330
|
+
const fallback = mode === "light" ? "#fff" : "#1c1c1f";
|
|
331
|
+
const themeVariables = window.__NU_MERMAID_CONFIGS__?.[mode]?.themeVariables;
|
|
332
|
+
const configuredBackground =
|
|
333
|
+
typeof themeVariables === "object" && themeVariables !== null
|
|
334
|
+
? (themeVariables as Record<string, unknown>).background
|
|
335
|
+
: undefined;
|
|
336
|
+
|
|
337
|
+
if (typeof configuredBackground === "string") {
|
|
338
|
+
const probe = document.createElement("canvas").getContext("2d");
|
|
339
|
+
if (probe) {
|
|
340
|
+
probe.fillStyle = fallback;
|
|
341
|
+
probe.fillStyle = configuredBackground;
|
|
342
|
+
if (!isTransparentColor(probe.fillStyle)) return probe.fillStyle;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return fallback;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function canvasToPngBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
canvas.toBlob((blob) => {
|
|
352
|
+
if (blob) {
|
|
353
|
+
resolve(blob);
|
|
354
|
+
} else {
|
|
355
|
+
reject(new Error("The browser could not encode the Mermaid diagram as PNG."));
|
|
356
|
+
}
|
|
357
|
+
}, "image/png");
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Download a high-resolution PNG of the diagram.
|
|
363
|
+
*
|
|
364
|
+
* The raster target is 4× the SVG viewBox (capped to browser-safe canvas
|
|
365
|
+
* dimensions). The canvas is filled with the active Mermaid theme's configured
|
|
366
|
+
* background so every exported color keeps its intended contrast.
|
|
367
|
+
*/
|
|
368
|
+
export async function downloadDiagramPng(svg: SVGElement, container: Element): Promise<void> {
|
|
369
|
+
const { width, height } = parseSvgDimensions(svg);
|
|
370
|
+
const scale = calculatePngScale(width, height);
|
|
371
|
+
const canvas = document.createElement("canvas");
|
|
372
|
+
canvas.width = Math.max(1, Math.round(width * scale));
|
|
373
|
+
canvas.height = Math.max(1, Math.round(height * scale));
|
|
374
|
+
|
|
375
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
376
|
+
if (!context) throw new Error("The browser could not create a canvas for the Mermaid diagram.");
|
|
377
|
+
|
|
378
|
+
const clone = svg.cloneNode(true) as SVGElement;
|
|
379
|
+
clone.setAttribute("xmlns", SVG_NS);
|
|
380
|
+
clone.setAttribute("width", String(width));
|
|
381
|
+
clone.setAttribute("height", String(height));
|
|
382
|
+
clone.removeAttribute("style");
|
|
383
|
+
inlineRasterStyles(svg, clone);
|
|
384
|
+
|
|
385
|
+
const serializedSvg = new XMLSerializer().serializeToString(clone);
|
|
386
|
+
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serializedSvg)}`;
|
|
387
|
+
const image = new Image();
|
|
388
|
+
|
|
389
|
+
await new Promise<void>((resolve, reject) => {
|
|
390
|
+
image.onload = () => resolve();
|
|
391
|
+
image.onerror = () => reject(new Error("The Mermaid diagram could not be rasterized."));
|
|
392
|
+
image.src = svgUrl;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
context.fillStyle = resolveMermaidCanvasColor();
|
|
396
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
397
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
398
|
+
|
|
399
|
+
const pngBlob = await canvasToPngBlob(canvas);
|
|
400
|
+
downloadBlob(pngBlob, buildDownloadFilename(container, "png"));
|
|
227
401
|
}
|
|
228
402
|
|
|
229
403
|
// ---------------------------------------------------------------------------
|
|
@@ -252,7 +426,7 @@ function detectDiagramType(container: Element): string {
|
|
|
252
426
|
return slugify(keyword.replace(/[-_](v\d+|beta)$/i, "").replace(/(diagram|chart)$/i, "")) || "diagram";
|
|
253
427
|
}
|
|
254
428
|
|
|
255
|
-
function buildDownloadFilename(container: Element): string {
|
|
429
|
+
function buildDownloadFilename(container: Element, extension: DownloadExtension): string {
|
|
256
430
|
const site = extractSiteSlug();
|
|
257
431
|
const page = extractPageSlug();
|
|
258
432
|
const type = detectDiagramType(container);
|
|
@@ -261,5 +435,5 @@ function buildDownloadFilename(container: Element): string {
|
|
|
261
435
|
const sameType = allDiagrams.filter((d) => detectDiagramType(d) === type);
|
|
262
436
|
const disambiguator = sameType.length > 1 ? `-${sameType.indexOf(container as HTMLElement) + 1}` : "";
|
|
263
437
|
|
|
264
|
-
return `${site}_${page}-${type}${disambiguator}
|
|
438
|
+
return `${site}_${page}-${type}${disambiguator}.${extension}`;
|
|
265
439
|
}
|