@nu-appdev/northwestern-starlight-theme 1.5.1 → 1.6.0

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 CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.0] - 2026-07-28
11
+
12
+ ### Added
13
+
14
+ - 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.
15
+
10
16
  ## [1.5.1] - 2026-04-22
11
17
 
12
18
  ### Added
@@ -185,7 +191,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
185
191
  - OpenAPI plugin compatibility with method badge preservation
186
192
  - Reduced motion support for transitions
187
193
 
188
- [Unreleased]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.5.1...HEAD
194
+ [Unreleased]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.6.0...HEAD
195
+ [1.6.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.5.1...v1.6.0
189
196
  [1.5.1]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.5.0...v1.5.1
190
197
  [1.5.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.4.0...v1.5.0
191
198
  [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
@@ -144,11 +144,11 @@ export type NorthwesternConfigOptions = Omit<AstroUserConfig, "integrations"> &
144
144
  /**
145
145
  * Mermaid diagram support.
146
146
  *
147
- * - `true` (default) — adds `northwesternMermaid()` before `starlight()` with
147
+ * - `true` (default) — adds `northwesternMermaid()` after `starlight()` with
148
148
  * default options (branded colors, toolbar, dark mode). Requires `astro-mermaid`
149
149
  * and `mermaid` to be installed.
150
150
  * - `false` — disable Mermaid entirely
151
- * - `object` — adds `northwesternMermaid(options)` before `starlight()` with
151
+ * - `object` — adds `northwesternMermaid(options)` after `starlight()` with
152
152
  * custom options (toolbar, theme overrides, etc.)
153
153
  */
154
154
  mermaid?: boolean | NorthwesternMermaidOptions;
@@ -255,9 +255,8 @@ export function defineNorthwesternConfig(options: NorthwesternConfigOptions): As
255
255
  }
256
256
 
257
257
  // Build theme config, handling mermaid dual-path.
258
- // The standalone northwesternMermaid() integration MUST be added before starlight()
259
- // so its remark plugin processes mermaid code blocks before expressive-code.
260
- // The theme's built-in mermaid (via addIntegration inside config:setup) runs too late.
258
+ // astro-mermaid 2.1 detects Starlight's active Markdown processor, so the
259
+ // standalone integration is added immediately after Starlight below.
261
260
  const themeConfig: NorthwesternThemeConfig = { ...theme };
262
261
  let mermaidIntegration: AstroIntegration | undefined;
263
262
  const hasAstroMermaid = hasOptionalPackage("astro-mermaid");
@@ -299,17 +298,17 @@ export function defineNorthwesternConfig(options: NorthwesternConfigOptions): As
299
298
  };
300
299
  const mergedPlugins = mergeStarlightPlugins(northwesternTheme(themeConfig), starlightPlugins, helperPlugins);
301
300
 
302
- // Build integration array: before → [mermaid] → starlight → after → [legacy-html-redirects]
301
+ // Build integration array: before → starlight → [mermaid] → after → [legacy-html-redirects]
303
302
  // The legacy-html-redirects integration runs last so it rewrites redirect pages
304
303
  // after every other astro:build:done hook has had a chance to emit them.
305
304
  const integrations: AstroIntegration[] = [
306
305
  ...(extraIntegrations?.before ?? []),
307
- ...(mermaidIntegration ? [mermaidIntegration] : []),
308
306
  starlight({
309
307
  ...restStarlightConfig,
310
308
  expressiveCode: expressiveCodeConfig,
311
309
  plugins: mergedPlugins,
312
310
  }),
311
+ ...(mermaidIntegration ? [mermaidIntegration] : []),
313
312
  ...(extraIntegrations?.after ?? []),
314
313
  ...(legacyHtmlRedirects ? [legacyHtmlRedirectsIntegration()] : []),
315
314
  ];
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()` before `starlight()` with
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)` before `starlight()` with
40
+ * - `object` — adds `northwesternMermaid(options)` after `starlight()` with
41
41
  * custom options (toolbar, theme overrides, etc.)
42
42
  */
43
43
  mermaid?: boolean | NorthwesternMermaidOptions;
@@ -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;AA2EjF;;;;;;;;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,CAkI5F"}
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;AA2EjF;;;;;;;;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,CAiI5F"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE;;;;;;;;;;;;;;;;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,CAkP/F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE;;;;;;;;;;;;;;;;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,CAgQ/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 before `starlight()` in the `integrations` array.** The
72
- * `astro-mermaid` remark plugin needs to register before Starlight's rehype
73
- * processing, and Astro processes integrations in order. Placing it after
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` detect and extend Starlight's active Markdown processor,
73
+ * including Astro 7's Sätteri pipeline.
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
  * ```
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,0BAA+B,GAAG,gBAAgB,CAqD9F"}
1
+ {"version":3,"file":"mermaid.d.ts","sourceRoot":"","sources":["../mermaid.ts"],"names":[],"mappings":"AACA,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,CA+E9F"}
@@ -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;IAElC;;OAEG;;;;;;;;;IAKH;;;;;;OAMG;;IASH;;;;;OAKG;;kBAOL,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC;IAErC;;;;OAIG;;iBAQL,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC;;kBAcvC,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B;IAEpC;;;;;OAKG;;IAKH;;OAEG;;QApGH;;WAEG;;;;;;;;;QAKH;;;;;;WAMG;;QASH;;;;;WAKG;;;IA8EH;;;;;;OAMG;;QAnEH;;;;WAIG;;;IAqEH;;OAEG;;IAKH;;;;;;;OAOG;;;;IAMH;;;;;OAKG;;QAGK;;WAEG;;QAKH;;WAEG;;;iBAYb,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"}
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"}
@@ -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,KAC7B,MAAM,IAAI,UACrB"}
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
@@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { unified } from "@astrojs/markdown-remark";
6
7
  import type { StarlightPlugin } from "@astrojs/starlight/types";
7
8
  import { nonEmptyStringSchema, northwesternThemeConfigSchema, validateSchema } from "./src/config-schema";
8
9
  import rehypeTableScroll from "./src/rehype-table-scroll";
@@ -202,11 +203,25 @@ 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
+ const markdown = astroConfig.markdown ?? {};
208
+ const markdownConfig =
209
+ markdown.processor?.name === "satteri"
210
+ ? {
211
+ processor: unified({
212
+ remarkPlugins: markdown.remarkPlugins,
213
+ rehypePlugins: [...(markdown.rehypePlugins ?? []), rehypeTableScroll],
214
+ remarkRehype: markdown.remarkRehype,
215
+ gfm: markdown.gfm,
216
+ smartypants: markdown.smartypants,
217
+ }),
218
+ }
219
+ : {
220
+ rehypePlugins: [...(markdown.rehypePlugins ?? []), rehypeTableScroll],
221
+ };
222
+
206
223
  updateAstroConfig({
207
- markdown: {
208
- rehypePlugins: [rehypeTableScroll],
209
- },
224
+ markdown: markdownConfig,
210
225
  vite: {
211
226
  plugins: [
212
227
  {
package/mermaid.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { unified } from "@astrojs/markdown-remark";
1
2
  import type { AstroIntegration } from "astro";
2
3
  import type { AstroMermaidOptions } from "astro-mermaid";
3
4
  import { darken, isDark, lighten, mix, transparentize } from "khroma";
@@ -421,10 +422,9 @@ export const darkMermaidConfig: AstroMermaidOptions = createNorthwesternMermaidC
421
422
  * **Note:** `defineNorthwesternConfig` handles Mermaid integration ordering.
422
423
  * This function is only needed for manual setups.
423
424
  *
424
- * **Must be added before `starlight()` in the `integrations` array.** The
425
- * `astro-mermaid` remark plugin needs to register before Starlight's rehype
426
- * processing, and Astro processes integrations in order. Placing it after
427
- * `starlight()` causes Mermaid code blocks to be treated as plain code.
425
+ * **Must be added after `starlight()` in the `integrations` array.** This lets
426
+ * `astro-mermaid` detect and extend Starlight's active Markdown processor,
427
+ * including Astro 7's Sätteri pipeline.
428
428
  *
429
429
  * @param options - Merged with Northwestern defaults. Set `toolbar: false` to
430
430
  * disable the hover toolbar.
@@ -437,11 +437,11 @@ export const darkMermaidConfig: AstroMermaidOptions = createNorthwesternMermaidC
437
437
  *
438
438
  * export default defineConfig({
439
439
  * integrations: [
440
- * northwesternMermaid(), // Must come before starlight()
441
440
  * starlight({
442
441
  * plugins: [northwesternTheme()],
443
442
  * title: "My Docs",
444
443
  * }),
444
+ * northwesternMermaid(), // Must come after starlight()
445
445
  * ],
446
446
  * });
447
447
  * ```
@@ -478,6 +478,32 @@ export function northwesternMermaid(options: NorthwesternMermaidOptions = {}): A
478
478
  hooks: {
479
479
  async "astro:config:setup"(params) {
480
480
  const { default: mermaid } = await mermaidModulePromise;
481
+ let mermaidHookParams = params;
482
+ const markdown = params.config.markdown ?? {};
483
+
484
+ if (markdown.processor?.name === "satteri") {
485
+ const processor = unified({
486
+ remarkPlugins: markdown.remarkPlugins,
487
+ rehypePlugins: markdown.rehypePlugins,
488
+ remarkRehype: markdown.remarkRehype,
489
+ gfm: markdown.gfm,
490
+ smartypants: markdown.smartypants,
491
+ });
492
+ const unifiedMarkdown = {
493
+ ...markdown,
494
+ processor,
495
+ };
496
+
497
+ params.updateConfig({ markdown: { processor } });
498
+ mermaidHookParams = {
499
+ ...params,
500
+ config: {
501
+ ...params.config,
502
+ markdown: unifiedMarkdown,
503
+ },
504
+ };
505
+ }
506
+
481
507
  const mermaidIntegration = mermaid({
482
508
  ...lightConfig,
483
509
  ...overrides,
@@ -485,7 +511,7 @@ export function northwesternMermaid(options: NorthwesternMermaidOptions = {}): A
485
511
  mermaidConfig: mergedLightMermaidConfig,
486
512
  });
487
513
 
488
- await mermaidIntegration.hooks["astro:config:setup"]?.(params);
514
+ await mermaidIntegration.hooks["astro:config:setup"]?.(mermaidHookParams);
489
515
 
490
516
  params.injectScript(
491
517
  "page",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nu-appdev/northwestern-starlight-theme",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
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
- "@expressive-code/plugin-line-numbers": "^0.41.7",
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.26.0",
75
- "zod": "^4.3.6"
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.4",
93
- "typescript": "^6.0.2"
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"
@@ -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
- const url = URL.createObjectURL(blob);
222
- const anchor = document.createElement("a");
223
- anchor.href = url;
224
- anchor.download = buildDownloadFilename(container);
225
- anchor.click();
226
- URL.revokeObjectURL(url);
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}.svg`;
438
+ return `${site}_${page}-${type}${disambiguator}.${extension}`;
265
439
  }
@@ -80,6 +80,11 @@
80
80
  background: var(--nu-hover-bg);
81
81
  }
82
82
 
83
+ .nu-mermaid-btn:disabled {
84
+ cursor: wait;
85
+ opacity: 0.6;
86
+ }
87
+
83
88
  :root:not([data-theme="light"]) .nu-mermaid-btn:hover {
84
89
  background: var(--nu-hover-bg);
85
90
  }