@tenphi/starlight 0.6.1 → 0.8.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/dist/index.js CHANGED
@@ -2,14 +2,107 @@ import { n as starlight, t as createStarlightCollection } from "./content-Cwk8rm
2
2
  import { a as resolveNavigationLayout } from "./navigation-CkQXI2Zb.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync } from "node:fs";
5
- import { cp, mkdir, readFile } from "node:fs/promises";
5
+ import { cp, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
6
6
  import { dirname, extname, join } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { COOKBOOK_COMPONENT_NAMES, assertValidDocs, createDocsGraph } from "@tenphi/docs";
9
9
  import { configure } from "@tenphi/tasty/core";
10
10
  import { tastyIntegration } from "@tenphi/tasty/ssr/astro";
11
+ import { renderMermaidSVG } from "beautiful-mermaid";
11
12
  import { apcaContrast, glaze, okhslToLinearSrgb, relativeLuminanceFromLinearRgb, variantToOkhsl } from "@tenphi/glaze";
12
13
  import "@tenphi/tasty";
14
+ //#region src/markdown/rehype-mermaid.ts
15
+ const sourceStyleDirective = /^\s*(?:classDef|style|linkStyle)\s+.*$/gim;
16
+ const accessibilityDirective = /^\s*acc(?:Title|Descr):\s*.*$/gim;
17
+ const svgStyleBlock = /\s*<style>[\s\S]*?<\/style>\s*/gi;
18
+ const supportedDiagram = /^(?:(?:flowchart|graph)(?:\s+(?:TB|TD|BT|RL|LR))?|stateDiagram(?:-v2)?|sequenceDiagram|classDiagram|erDiagram)\b/i;
19
+ /** Render supported Mermaid fences to theme-responsive SVG at build time. */
20
+ function rehypeMermaid() {
21
+ return (tree) => {
22
+ replaceMermaidCodeBlocks(tree);
23
+ };
24
+ }
25
+ /** Sätteri adapter for Astro's default Markdown processor. */
26
+ const satteriMermaid = {
27
+ name: "cookbook:mermaid",
28
+ element: {
29
+ filter: ["pre"],
30
+ visit(node, context) {
31
+ if (!isMermaidCodeBlock(node)) return;
32
+ try {
33
+ context.replaceNode(node, {
34
+ type: "raw",
35
+ value: renderMermaidElement(context.textContent(node))
36
+ });
37
+ } catch {
38
+ context.setProperty(node, "data-mermaid-state", "error");
39
+ }
40
+ }
41
+ }
42
+ };
43
+ function replaceMermaidCodeBlocks(parent) {
44
+ if (!parent.children) return;
45
+ for (const [index, child] of parent.children.entries()) {
46
+ if (isMermaidCodeBlock(child)) {
47
+ const source = textContent(child);
48
+ try {
49
+ parent.children[index] = {
50
+ type: "raw",
51
+ value: renderMermaidElement(source)
52
+ };
53
+ } catch {
54
+ child.properties = {
55
+ ...child.properties,
56
+ "data-mermaid-state": "error"
57
+ };
58
+ }
59
+ continue;
60
+ }
61
+ replaceMermaidCodeBlocks(child);
62
+ }
63
+ }
64
+ function renderMermaidElement(source) {
65
+ return `<div class="td-mermaid" data-mermaid-state="ready">${accessibleSvg(render(source), source)}</div>`;
66
+ }
67
+ function isMermaidCodeBlock(node) {
68
+ if (node.type !== "element" || node.tagName !== "pre") return false;
69
+ return node.properties?.dataLanguage === "mermaid" || node.properties?.["data-language"] === "mermaid";
70
+ }
71
+ function render(source) {
72
+ const header = source.split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("%%") && !/^acc(?:Title|Descr):/i.test(line));
73
+ if (!header || !supportedDiagram.test(header)) throw new Error("Unsupported Mermaid diagram type");
74
+ const safeSource = source.replace(sourceStyleDirective, "").replace(accessibilityDirective, "");
75
+ return renderMermaidSVG(safeSource, {
76
+ bg: "var(--surface-2-color)",
77
+ fg: "var(--text-color)",
78
+ line: "var(--text-soft-color)",
79
+ accent: "var(--accent-text-color)",
80
+ muted: "var(--text-soft-color)",
81
+ surface: "var(--surface-color)",
82
+ border: "var(--border-strong-color)",
83
+ font: "Onest Variable",
84
+ transparent: true
85
+ }).replace(svgStyleBlock, "");
86
+ }
87
+ function accessibleSvg(svg, source) {
88
+ const title = directive(source, "accTitle") ?? "Diagram";
89
+ const description = directive(source, "accDescr") ?? "Rendered from a Mermaid code block.";
90
+ return svg.replace("<svg ", `<svg role="img" aria-label="${escapeAttribute(title)}" `).replace(/(<svg\b[^>]*>)/, `$1<title>${escapeText(title)}</title><desc>${escapeText(description)}</desc>`);
91
+ }
92
+ function directive(source, name) {
93
+ return source.match(new RegExp(`^\\s*${name}:\\s*(.+)$`, "im"))?.[1]?.trim();
94
+ }
95
+ function textContent(node) {
96
+ if (node.type === "text") return node.value ?? "";
97
+ return node.children?.map(textContent).join("") ?? "";
98
+ }
99
+ function escapeAttribute(value) {
100
+ return escapeText(value).replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
101
+ }
102
+ function escapeText(value) {
103
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
104
+ }
105
+ //#endregion
13
106
  //#region src/theme/defaults.ts
14
107
  const DEFAULT_THEME_TOKENS = {
15
108
  $gap: "0.5rem",
@@ -109,80 +202,39 @@ function resolveDocsTheme(theme = {}) {
109
202
  ...theme.contrastLevel !== void 0 ? { contrastLevel: theme.contrastLevel } : {}
110
203
  };
111
204
  const surfaceFrom = theme.palette?.surface ?? "#ffffff";
112
- const surface = glaze.color({
205
+ const resolvedSurfaceSeed = glaze.color({
113
206
  from: surfaceFrom,
114
207
  mode: "auto",
115
208
  darkSaturation: .35
116
- });
117
- const surface2 = glaze.color({
118
- from: surfaceFrom,
119
- base: surface,
120
- tone: "-2",
121
- mode: "auto",
122
- saturationFactor: .9,
123
- darkSaturation: .325
124
- }, glazeOptions);
125
- const surface3 = glaze.color({
126
- from: surfaceFrom,
127
- base: surface2,
128
- tone: "-2",
129
- mode: "auto",
130
- saturationFactor: .8,
131
- darkSaturation: .3
132
- }, glazeOptions);
133
- const text = glaze.color({
134
- from: theme.palette?.text ?? "#20232a",
135
- base: surface,
136
- role: "text",
137
- contrast: { apca: [75, 90] },
138
- mode: "auto"
139
- }, glazeOptions);
140
- const textSoft = glaze.color({
141
- from: theme.palette?.textSoft ?? "#626875",
142
- base: surface,
143
- role: "text",
144
- contrast: { apca: [60, 75] },
145
- mode: "auto"
146
- }, glazeOptions);
147
- const accentText = glaze.color({
148
- from: brand.from,
149
- base: surface,
150
- role: "text",
151
- contrast: { apca: authoredTarget },
152
- mode: "auto"
153
- }, glazeOptions);
154
- const focus = glaze.color({
155
- from: brand.from,
156
- base: surface,
157
- role: "border",
158
- contrast: { apca: authoredTarget },
159
- mode: "auto"
160
- }, glazeOptions);
161
- const accentSurface = glaze.color({
162
- from: brand.from,
163
- mode: "fixed"
164
- });
165
- const accentSurfaceText = glaze.color({
166
- from: "#ffffff",
167
- base: accentSurface,
168
- role: "text",
169
- contrast: { apca: 60 },
170
- mode: "auto"
171
- });
172
- const resolvedSurface = surface.resolve();
173
- const resolvedAccent = accentText.resolve();
174
- const shadowTheme = glaze({
175
- hue: variantToOkhsl(resolvedSurface.light).h,
176
- saturation: variantToOkhsl(resolvedSurface.light).s * 100,
177
- darkHue: variantToOkhsl(resolvedSurface.dark).h,
178
- darkSaturation: variantToOkhsl(resolvedSurface.dark).s * 100
209
+ }).resolve();
210
+ const lightSurface = variantToOkhsl(resolvedSurfaceSeed.light);
211
+ const darkSurface = variantToOkhsl(resolvedSurfaceSeed.dark);
212
+ const colorTheme = glaze({
213
+ hue: lightSurface.h,
214
+ saturation: lightSurface.s * 100,
215
+ darkHue: darkSurface.h,
216
+ darkSaturation: Math.min(100, darkSurface.s * 100 / .35)
179
217
  }, void 0, glazeOptions);
180
- shadowTheme.colors({
218
+ colorTheme.colors({
181
219
  surface: {
182
220
  from: surfaceFrom,
183
221
  mode: "auto",
184
222
  darkSaturation: .35
185
223
  },
224
+ "surface-2": {
225
+ base: "surface",
226
+ tone: "-2",
227
+ mode: "auto",
228
+ saturation: .75,
229
+ darkSaturation: .275
230
+ },
231
+ "surface-3": {
232
+ base: "surface-2",
233
+ tone: "-2",
234
+ mode: "auto",
235
+ saturation: .65,
236
+ darkSaturation: .25
237
+ },
186
238
  text: {
187
239
  from: theme.palette?.text ?? "#20232a",
188
240
  base: "surface",
@@ -190,14 +242,176 @@ function resolveDocsTheme(theme = {}) {
190
242
  contrast: { apca: [75, 90] },
191
243
  mode: "auto"
192
244
  },
245
+ "text-soft": {
246
+ from: theme.palette?.textSoft ?? "#626875",
247
+ base: "surface",
248
+ role: "text",
249
+ contrast: { apca: [60, 75] },
250
+ mode: "auto"
251
+ },
252
+ "text-muted": mix("surface", "text", 66),
253
+ "surface-2-hover": mix("surface-2", "text", [3, 6]),
254
+ "surface-2-pressed": mix("surface-2", "text", [9, 14]),
255
+ "surface-3-hover": mix("surface-3", "text", [3, 6]),
256
+ "surface-3-pressed": mix("surface-3", "text", [9, 14]),
257
+ "accent-text": {
258
+ from: brand.from,
259
+ base: "surface",
260
+ role: "text",
261
+ contrast: { apca: [normalTarget, highTarget] },
262
+ mode: "auto"
263
+ },
264
+ focus: {
265
+ from: brand.from,
266
+ base: "surface",
267
+ role: "border",
268
+ contrast: { apca: [normalTarget, highTarget] },
269
+ mode: "auto"
270
+ },
271
+ "accent-surface": {
272
+ from: brand.from,
273
+ mode: "fixed"
274
+ },
275
+ "accent-surface-text": {
276
+ from: "#ffffff",
277
+ base: "accent-surface",
278
+ role: "text",
279
+ contrast: { apca: [60, 75] },
280
+ mode: "auto"
281
+ },
282
+ "accent-surface-subtle": mix("surface", "accent-surface", [12, 18]),
283
+ "accent-surface-2-subtle": mix("surface-2", "accent-surface", [12, 18]),
193
284
  shadow: {
194
285
  type: "shadow",
195
286
  bg: "surface",
196
287
  fg: "text",
197
288
  intensity: [12, 20],
198
289
  tuning: { alphaMax: .28 }
290
+ },
291
+ overlay: {
292
+ type: "mix",
293
+ base: "surface",
294
+ target: "text",
295
+ value: [58, 68],
296
+ blend: "transparent"
297
+ },
298
+ clear: {
299
+ from: "#ffffff",
300
+ mode: "fixed",
301
+ opacity: 0
302
+ },
303
+ ...statusColors("orange", "#d97706"),
304
+ ...statusColors("green", "#16a34a"),
305
+ ...statusColors("blue", "#2563eb"),
306
+ ...statusColors("purple", "#9333ea"),
307
+ ...statusColors("red", "#dc2626")
308
+ });
309
+ const resolvedBrandSeed = glaze.color({
310
+ from: brand.from,
311
+ mode: "fixed"
312
+ }).resolve();
313
+ const lightBrand = variantToOkhsl(resolvedBrandSeed.light);
314
+ const darkBrand = variantToOkhsl(resolvedBrandSeed.dark);
315
+ const borderTheme = glaze({
316
+ hue: lightBrand.h,
317
+ saturation: lightBrand.s * 100,
318
+ darkHue: darkBrand.h,
319
+ darkSaturation: darkBrand.s * 100
320
+ }, void 0, glazeOptions);
321
+ borderTheme.colors({
322
+ surface: {
323
+ from: surfaceFrom,
324
+ mode: "auto",
325
+ darkSaturation: .35
326
+ },
327
+ border: {
328
+ base: "surface",
329
+ tone: ["-9", "-22"],
330
+ saturation: .205,
331
+ mode: "auto"
332
+ },
333
+ "border-strong": {
334
+ base: "surface",
335
+ tone: ["-20", "-38"],
336
+ saturation: .205,
337
+ mode: "auto"
338
+ }
339
+ });
340
+ const syntaxTheme = glaze(210, 90, glazeOptions);
341
+ syntaxTheme.colors({
342
+ bg: {
343
+ tone: 100,
344
+ saturation: .1
345
+ },
346
+ text: {
347
+ base: "bg",
348
+ tone: 0,
349
+ contrast: { wcag: ["AA", "AAA"] },
350
+ saturation: 0
351
+ },
352
+ comment: {
353
+ base: "bg",
354
+ contrast: { wcag: ["AA", "AAA"] },
355
+ saturation: .01,
356
+ hue: 210
357
+ },
358
+ punctuation: {
359
+ base: "bg",
360
+ contrast: { wcag: [6, "AAA"] },
361
+ saturation: .01,
362
+ hue: 210
363
+ },
364
+ keyword: {
365
+ base: "bg",
366
+ contrast: { wcag: ["AA", "AAA"] },
367
+ saturation: 80
368
+ },
369
+ string: {
370
+ base: "bg",
371
+ contrast: { wcag: ["AA", "AAA"] },
372
+ saturation: 80,
373
+ hue: 15
374
+ },
375
+ token: {
376
+ base: "bg",
377
+ contrast: { wcag: ["AA", "AAA"] },
378
+ saturation: 80,
379
+ hue: 125
380
+ },
381
+ property: {
382
+ base: "bg",
383
+ contrast: { wcag: ["AA", "AAA"] },
384
+ saturation: 80,
385
+ hue: 155
386
+ },
387
+ number: {
388
+ base: "bg",
389
+ contrast: { wcag: ["AA", "AAA"] },
390
+ saturation: 80,
391
+ hue: 70
392
+ },
393
+ function: {
394
+ base: "bg",
395
+ contrast: { wcag: ["AA", "AAA"] },
396
+ saturation: 80,
397
+ hue: 210
398
+ },
399
+ value: {
400
+ base: "bg",
401
+ contrast: { wcag: ["AA", "AAA"] },
402
+ saturation: 80,
403
+ hue: 210
404
+ },
405
+ operator: {
406
+ base: "bg",
407
+ contrast: { wcag: ["AA", "AAA"] },
408
+ saturation: 80,
409
+ hue: 340
199
410
  }
200
411
  });
412
+ const resolvedColors = colorTheme.resolve();
413
+ const resolvedSurface = requiredResolvedColor(resolvedColors, "surface");
414
+ const resolvedAccent = requiredResolvedColor(resolvedColors, "accent-text");
201
415
  const scores = {
202
416
  light: score(resolvedAccent.light, resolvedSurface.light),
203
417
  dark: score(resolvedAccent.dark, resolvedSurface.dark),
@@ -215,20 +429,39 @@ function resolveDocsTheme(theme = {}) {
215
429
  });
216
430
  }
217
431
  const outputOptions = { modes: { highContrast: true } };
218
- const shadow = shadowTheme.json(outputOptions).shadow;
219
- if (!shadow) throw new Error("The Glaze shadow token failed to resolve.");
432
+ const tastyOptions = {
433
+ ...outputOptions,
434
+ states: {
435
+ dark: "theme=dark | (@media(prefers-color-scheme: dark) & :not([data-theme]))",
436
+ highContrast: "contrast=more | (@media(prefers-contrast: more) & :not([data-contrast]))"
437
+ }
438
+ };
439
+ const resolvedPalette = colorTheme.json(outputOptions);
440
+ const colorTokens = colorTheme.tasty(tastyOptions);
441
+ const borderTokens = borderTheme.tasty(tastyOptions);
442
+ const syntaxTokens = glaze.palette({ syntax: syntaxTheme }).tasty({
443
+ ...tastyOptions,
444
+ prefix: true,
445
+ primary: false
446
+ });
220
447
  return {
221
448
  colors: {
222
- surface: surface.json(outputOptions),
223
- surface2: surface2.json(outputOptions),
224
- surface3: surface3.json(outputOptions),
225
- text: text.json(outputOptions),
226
- textSoft: textSoft.json(outputOptions),
227
- accentText: accentText.json(outputOptions),
228
- accentSurface: accentSurface.json(outputOptions),
229
- accentSurfaceText: accentSurfaceText.json(outputOptions),
230
- focus: focus.json(outputOptions),
231
- shadow
449
+ surface: requiredJsonColor(resolvedPalette, "surface"),
450
+ surface2: requiredJsonColor(resolvedPalette, "surface-2"),
451
+ surface3: requiredJsonColor(resolvedPalette, "surface-3"),
452
+ text: requiredJsonColor(resolvedPalette, "text"),
453
+ textSoft: requiredJsonColor(resolvedPalette, "text-soft"),
454
+ accentText: requiredJsonColor(resolvedPalette, "accent-text"),
455
+ accentSurface: requiredJsonColor(resolvedPalette, "accent-surface"),
456
+ accentSurfaceText: requiredJsonColor(resolvedPalette, "accent-surface-text"),
457
+ focus: requiredJsonColor(resolvedPalette, "focus"),
458
+ shadow: requiredJsonColor(resolvedPalette, "shadow")
459
+ },
460
+ colorTokens: {
461
+ ...colorTokens,
462
+ "#border": requiredJsonColor(borderTokens, "#border"),
463
+ "#border-strong": requiredJsonColor(borderTokens, "#border-strong"),
464
+ ...syntaxTokens
232
465
  },
233
466
  tokens: resolveThemeTokens(theme.tokens),
234
467
  presets: resolveTypographyPresets(theme.presets),
@@ -236,6 +469,44 @@ function resolveDocsTheme(theme = {}) {
236
469
  diagnostics
237
470
  };
238
471
  }
472
+ function mix(base, target, value, space = "okhsl") {
473
+ return {
474
+ type: "mix",
475
+ base,
476
+ target,
477
+ value,
478
+ space
479
+ };
480
+ }
481
+ function statusColors(name, from) {
482
+ return {
483
+ [name]: {
484
+ from,
485
+ base: "surface",
486
+ role: "border",
487
+ contrast: { apca: [30, 45] },
488
+ mode: "auto"
489
+ },
490
+ [`${name}-text`]: {
491
+ from,
492
+ base: "surface",
493
+ role: "text",
494
+ contrast: { apca: [60, 75] },
495
+ mode: "auto"
496
+ },
497
+ [`${name}-surface`]: mix("surface", name, [12, 18], "srgb")
498
+ };
499
+ }
500
+ function requiredResolvedColor(colors, name) {
501
+ const color = colors.get(name);
502
+ if (!color) throw new Error(`The Glaze ${name} color failed to resolve.`);
503
+ return color;
504
+ }
505
+ function requiredJsonColor(colors, name) {
506
+ const color = colors[name];
507
+ if (!color) throw new Error(`The Glaze ${name} token failed to export.`);
508
+ return color;
509
+ }
239
510
  function normalizeBrand(brand) {
240
511
  if (typeof brand === "object" && brand !== null && "from" in brand) return brand;
241
512
  return { from: brand ?? "#315efb" };
@@ -248,6 +519,215 @@ function luminance(variant) {
248
519
  return relativeLuminanceFromLinearRgb(okhslToLinearSrgb(h, s, l, variant.pastel));
249
520
  }
250
521
  //#endregion
522
+ //#region src/theme/shiki-theme.ts
523
+ const comment = "var(--syntax-comment-color)";
524
+ const punctuation = "var(--syntax-punctuation-color)";
525
+ const keyword = "var(--syntax-keyword-color)";
526
+ const string = "var(--syntax-string-color)";
527
+ const token = "var(--syntax-token-color)";
528
+ const property = "var(--syntax-property-color)";
529
+ const number = "var(--syntax-number-color)";
530
+ const func = "var(--syntax-function-color)";
531
+ const value = "var(--syntax-value-color)";
532
+ const operator = "var(--syntax-operator-color)";
533
+ const foreground = "var(--syntax-text-color)";
534
+ const background = "var(--syntax-bg-color)";
535
+ const shellLanguages = /* @__PURE__ */ new Set([
536
+ "bash",
537
+ "sh",
538
+ "shell",
539
+ "shellscript",
540
+ "zsh"
541
+ ]);
542
+ const shellPlaceholder = /<[A-Za-z][A-Za-z0-9_-]*>/g;
543
+ /**
544
+ * Shell grammars interpret documentation placeholders such as `<plan-id>` as
545
+ * redirections and can split the final character into an unscoped token. Keep
546
+ * the placeholder name visually coherent while retaining the operator color
547
+ * on the angle brackets.
548
+ */
549
+ const bashPlaceholderTransformer = {
550
+ name: "cookbook:bash-placeholders",
551
+ enforce: "post",
552
+ tokens(lines) {
553
+ if (!this.options.lang || !shellLanguages.has(this.options.lang)) return;
554
+ const ranges = [...this.source.matchAll(shellPlaceholder)].map((match) => ({
555
+ start: (match.index ?? 0) + 1,
556
+ end: (match.index ?? 0) + match[0].length - 1
557
+ }));
558
+ if (ranges.length === 0) return;
559
+ for (const line of lines) for (const highlighted of line) {
560
+ const start = highlighted.offset;
561
+ const end = start + highlighted.content.length;
562
+ if (ranges.some((range) => start < range.end && end > range.start)) highlighted.color = string;
563
+ }
564
+ return lines;
565
+ }
566
+ };
567
+ /**
568
+ * Shiki performs the grammatical classification, while every emitted color
569
+ * remains a reference to a Glaze-generated token owned by Tasty.
570
+ */
571
+ const tastyCodeTheme = {
572
+ name: "tasty-code",
573
+ type: "light",
574
+ fg: foreground,
575
+ bg: background,
576
+ colors: {
577
+ "editor.background": background,
578
+ "editor.foreground": foreground
579
+ },
580
+ settings: [
581
+ {
582
+ scope: [
583
+ "comment",
584
+ "comment.line",
585
+ "comment.block",
586
+ "punctuation.definition.comment"
587
+ ],
588
+ settings: {
589
+ foreground: comment,
590
+ fontStyle: "italic"
591
+ }
592
+ },
593
+ {
594
+ scope: [
595
+ "keyword",
596
+ "keyword.control",
597
+ "keyword.other",
598
+ "storage.type",
599
+ "storage.modifier",
600
+ "keyword.control.at-rule.tasty",
601
+ "keyword.control.at-rule.media.tasty",
602
+ "keyword.control.at-rule.media-type.tasty",
603
+ "keyword.control.at-rule.starting.tasty",
604
+ "keyword.control.state-alias.tasty"
605
+ ],
606
+ settings: { foreground: keyword }
607
+ },
608
+ {
609
+ scope: [
610
+ "string",
611
+ "string.quoted",
612
+ "string.template",
613
+ "string.quoted.attribute-value.tasty",
614
+ "string.unquoted.attribute-value.tasty"
615
+ ],
616
+ settings: { foreground: string }
617
+ },
618
+ {
619
+ scope: [
620
+ "support.constant.color.tasty-token",
621
+ "support.constant.color.tasty-token.builtin",
622
+ "constant.other.color.tasty-token",
623
+ "constant.other.color.tasty",
624
+ "constant.other.color.hex",
625
+ "constant.other.color.rgb-value"
626
+ ],
627
+ settings: { foreground: token }
628
+ },
629
+ {
630
+ scope: [
631
+ "constant.numeric",
632
+ "constant.numeric.tasty",
633
+ "constant.numeric.custom-unit.tasty",
634
+ "constant.numeric.css-with-unit",
635
+ "constant.numeric.bare.tasty",
636
+ "constant.numeric.css",
637
+ "constant.numeric.keyframe-step.tasty",
638
+ "constant.language.boolean.tasty"
639
+ ],
640
+ settings: { foreground: number }
641
+ },
642
+ {
643
+ scope: ["support.type.property-name.tasty", "variable.other.constant.tasty"],
644
+ settings: { foreground: property }
645
+ },
646
+ {
647
+ scope: ["variable", "variable.other"],
648
+ settings: { foreground }
649
+ },
650
+ {
651
+ scope: [
652
+ "entity.name.function",
653
+ "support.function",
654
+ "support.function.misc.css",
655
+ "entity.name.tag",
656
+ "entity.name.tag.tsx",
657
+ "entity.name.type.tasty",
658
+ "entity.name.tag.tasty"
659
+ ],
660
+ settings: { foreground: func }
661
+ },
662
+ {
663
+ scope: [
664
+ "support.constant.property-value.tasty",
665
+ "support.constant.property-value.tasty-display",
666
+ "support.constant.property-value.tasty-directional",
667
+ "support.constant.property-value.tasty-preset",
668
+ "support.constant.property-value.tasty-shape",
669
+ "support.constant.property-value.tasty-scrollbar",
670
+ "support.constant.property-value.tasty-state",
671
+ "support.constant.property-value.tasty-cursor",
672
+ "support.constant.property-value.tasty-overflow",
673
+ "support.constant.property-value.tasty-position",
674
+ "support.constant.property-value.tasty-flex",
675
+ "support.constant.property-value.tasty-font",
676
+ "support.constant.property-value.tasty-text",
677
+ "support.constant.property-value.tasty-alignment",
678
+ "support.constant.property-value.tasty-border-style",
679
+ "support.constant.property-value.tasty-whitespace",
680
+ "support.constant.property-value.tasty-global",
681
+ "support.constant.property-value.tasty-transition",
682
+ "support.constant.property-value.css-syntax",
683
+ "entity.other.attribute-name",
684
+ "entity.other.attribute-name.tasty",
685
+ "entity.other.attribute-name.pseudo-class.tasty",
686
+ "entity.other.attribute-name.pseudo-class.css",
687
+ "entity.other.attribute-name.class.tasty",
688
+ "entity.other.attribute-name.pseudo-element.css",
689
+ "punctuation.definition.entity.css"
690
+ ],
691
+ settings: { foreground: value }
692
+ },
693
+ {
694
+ scope: [
695
+ "keyword.operator",
696
+ "keyword.operator.logical.tasty",
697
+ "keyword.operator.arithmetic.css",
698
+ "keyword.operator.assignment",
699
+ "keyword.operator.selector-affix.tasty",
700
+ "keyword.operator.attribute-selector.tasty",
701
+ "keyword.operator.comparison.tasty"
702
+ ],
703
+ settings: { foreground: operator }
704
+ },
705
+ {
706
+ scope: [
707
+ "punctuation.definition.string",
708
+ "punctuation.separator",
709
+ "punctuation.definition.block",
710
+ "punctuation.definition.array",
711
+ "punctuation.section",
712
+ "punctuation.definition.auto-calc",
713
+ "punctuation.definition.attribute-selector",
714
+ "punctuation.definition.pseudo-class",
715
+ "punctuation.definition.fallback",
716
+ "meta.brace"
717
+ ],
718
+ settings: { foreground: punctuation }
719
+ },
720
+ {
721
+ scope: ["keyword.control.at-rule", "entity.name.tag.class.css"],
722
+ settings: { foreground: keyword }
723
+ },
724
+ {
725
+ scope: ["support.type.property-name.css", "meta.property-name.css"],
726
+ settings: { foreground: property }
727
+ }
728
+ ]
729
+ };
730
+ //#endregion
251
731
  //#region src/theme/tasty-config.ts
252
732
  const TASTY_UNITS = {
253
733
  x: "var(--gap)",
@@ -255,42 +735,11 @@ const TASTY_UNITS = {
255
735
  cr: "var(--card-radius)",
256
736
  bw: "var(--border-width)"
257
737
  };
258
- const LIGHT = "theme=light | (@media(prefers-color-scheme: light) & :not([data-theme]))";
259
- const HIGH_CONTRAST = "contrast=more | (@media(prefers-contrast: more) & :not([data-contrast]))";
260
738
  function tastyTokens(theme) {
261
739
  const tokens = Object.fromEntries(Object.entries(theme.tokens).filter(([name]) => name.startsWith("$")));
262
- Object.assign(tokens, {
263
- "#surface": colorStates(theme.colors.surface),
264
- "#surface-2": colorStates(theme.colors.surface2),
265
- "#surface-3": colorStates(theme.colors.surface3),
266
- "#surface-2-hover": mix("#text", 3, "#surface-2"),
267
- "#surface-2-pressed": mix("#text", 9, "#surface-2"),
268
- "#surface-3-hover": mix("#text", 3, "#surface-3"),
269
- "#surface-3-pressed": mix("#text", 9, "#surface-3"),
270
- "#text": colorStates(theme.colors.text),
271
- "#text-soft": colorStates(theme.colors.textSoft),
272
- "#border": mix("#text", 18, "#surface"),
273
- "#border-strong": mix("#text", 34, "#surface"),
274
- "#accent-text": colorStates(theme.colors.accentText),
275
- "#accent-surface": colorStates(theme.colors.accentSurface),
276
- "#accent-surface-text": colorStates(theme.colors.accentSurfaceText),
277
- "#focus": colorStates(theme.colors.focus),
278
- "#shadow": colorStates(theme.colors.shadow),
279
- "#overlay": mix("#text", 58, "transparent")
280
- });
740
+ Object.assign(tokens, theme.colorTokens);
281
741
  return tokens;
282
742
  }
283
- function colorStates(colors) {
284
- return {
285
- "": colors.dark,
286
- [LIGHT]: colors.light,
287
- [HIGH_CONTRAST]: colors.darkContrast,
288
- [`(${LIGHT}) & (${HIGH_CONTRAST})`]: colors.lightContrast
289
- };
290
- }
291
- function mix(foreground, percentage, background) {
292
- return `color-mix(in oklab, ${foreground} ${percentage}%, ${background})`;
293
- }
294
743
  //#endregion
295
744
  //#region src/components/component-styles.ts
296
745
  const sharedConfiguration = globalThis;
@@ -377,8 +826,10 @@ function cookbook(options = {}) {
377
826
  }
378
827
  };
379
828
  usingContentCollection = hasContentConfig(context.config.srcDir);
829
+ registerCookbookMarkdownPlugins(context.config.markdown.processor);
380
830
  const starlightIntegration = starlight({
381
831
  title: options.config?.site?.title ?? "Documentation",
832
+ expressiveCode: false,
382
833
  ...options.config?.site?.description ? { description: options.config.site.description } : {},
383
834
  ...options.config?.search?.enabled === false ? { pagefind: false } : {},
384
835
  ...!usingContentCollection ? { disable404Route: true } : {},
@@ -392,13 +843,21 @@ function cookbook(options = {}) {
392
843
  ];
393
844
  let markdownRuntime = {
394
845
  image: context.config.image,
395
- markdown: context.config.markdown,
846
+ markdown: {
847
+ ...context.config.markdown,
848
+ syntaxHighlight: "shiki",
849
+ shikiConfig: cookbookShikiConfig(context.config.markdown.shikiConfig)
850
+ },
396
851
  srcDir: context.config.srcDir
397
852
  };
398
853
  let markdownRenderer;
399
854
  context.updateConfig({
400
855
  base,
401
856
  output: "static",
857
+ markdown: {
858
+ syntaxHighlight: "shiki",
859
+ shikiConfig: cookbookShikiConfig(context.config.markdown.shikiConfig)
860
+ },
402
861
  vite: {
403
862
  ssr: { external: [
404
863
  "@tenphi/docs",
@@ -406,7 +865,7 @@ function cookbook(options = {}) {
406
865
  "react-dom",
407
866
  "react-dom/server"
408
867
  ] },
409
- plugins: [virtualDocsPlugin(async () => {
868
+ plugins: [stripStarlightStylesPlugin(starlightRoot), virtualDocsPlugin(async () => {
410
869
  const loaded = await loadGraph();
411
870
  return {
412
871
  entries: usingContentCollection ? loaded.entries : await Promise.all(loaded.entries.map(async (entry) => {
@@ -489,7 +948,11 @@ function cookbook(options = {}) {
489
948
  hooks: { "astro:config:setup": ({ config }) => {
490
949
  markdownRuntime = {
491
950
  image: config.image,
492
- markdown: config.markdown,
951
+ markdown: {
952
+ ...config.markdown,
953
+ syntaxHighlight: "shiki",
954
+ shikiConfig: cookbookShikiConfig(config.markdown.shikiConfig)
955
+ },
493
956
  srcDir: config.srcDir
494
957
  };
495
958
  } }
@@ -542,8 +1005,19 @@ function cookbook(options = {}) {
542
1005
  },
543
1006
  "astro:build:done": async (context) => {
544
1007
  await callInner(inner, "astro:build:done", context);
545
- if (!graph) return;
546
1008
  const output = fileURLToPath(context.dir);
1009
+ for (const relativePath of await readdir(output, { recursive: true })) {
1010
+ if (extname(relativePath) !== ".html") continue;
1011
+ const path = join(output, relativePath);
1012
+ const html = await readFile(path, "utf8");
1013
+ const sanitized = html.replace(/\s*<link\b(?=[^>]*rel="stylesheet")(?=[^>]*href="data:text\/css,")[^>]*>/g, "").replace(/\s*<style>\s*<\/style>/g, "").replace(/\sstyle="--sl-icon-size:\s*([^;\"]+);?"/g, " width=\"$1\" height=\"$1\"").replace(/\sstyle="--depth:\s*([^;\"]+);?"/g, " data-depth=\"$1\"").replace(/(<kbd\b[^>]*)\sstyle="display:\s*none;?"([^>]*>)/g, "$1$2").replace(/(<dialog\b[^>]*)\sstyle="padding:\s*0;?"([^>]*>)/g, "$1$2");
1014
+ if (sanitized !== html) await writeFile(path, sanitized);
1015
+ }
1016
+ const pagefindOutput = join(output, "pagefind");
1017
+ if (existsSync(pagefindOutput)) {
1018
+ for (const name of await readdir(pagefindOutput)) if (extname(name) === ".css") await unlink(join(pagefindOutput, name));
1019
+ }
1020
+ if (!graph) return;
547
1021
  for (const asset of graph.assets) {
548
1022
  if (!asset.sourcePath || !asset.publicPath) continue;
549
1023
  const target = join(output, asset.publicPath.replace(/^\//, ""));
@@ -554,6 +1028,53 @@ function cookbook(options = {}) {
554
1028
  }
555
1029
  };
556
1030
  }
1031
+ function cookbookShikiConfig(config) {
1032
+ const transformers = Array.isArray(config?.transformers) ? config.transformers : [];
1033
+ return {
1034
+ ...config,
1035
+ theme: tastyCodeTheme,
1036
+ transformers: transformers.includes(bashPlaceholderTransformer) ? transformers : [...transformers, bashPlaceholderTransformer]
1037
+ };
1038
+ }
1039
+ function registerCookbookMarkdownPlugins(processor) {
1040
+ if (processor.name === "unified") {
1041
+ const options = processor.options;
1042
+ const plugins = Array.isArray(options.rehypePlugins) ? options.rehypePlugins : [];
1043
+ if (!plugins.includes(rehypeMermaid)) plugins.push(rehypeMermaid);
1044
+ options.rehypePlugins = plugins;
1045
+ } else if (processor.name === "satteri") {
1046
+ const options = processor.options;
1047
+ const plugins = Array.isArray(options.hastPlugins) ? options.hastPlugins : [];
1048
+ if (!plugins.includes(satteriMermaid)) plugins.push(satteriMermaid);
1049
+ options.hastPlugins = plugins;
1050
+ }
1051
+ }
1052
+ function stripStarlightStylesPlugin(root) {
1053
+ const normalizedRoot = root.replaceAll("\\", "/");
1054
+ const emptyPrintId = "\0cookbook:empty-starlight-print";
1055
+ return {
1056
+ name: "cookbook-strip-starlight-css",
1057
+ enforce: "pre",
1058
+ resolveId(source, importer) {
1059
+ if (importer?.replaceAll("\\", "/").startsWith(`${normalizedRoot}/`) && source.endsWith("/style/print.css?url&no-inline")) return emptyPrintId;
1060
+ },
1061
+ load(id) {
1062
+ if (id === emptyPrintId) return "export default \"data:text/css,\";";
1063
+ },
1064
+ transform(code, id) {
1065
+ const normalizedId = id.replaceAll("\\", "/");
1066
+ if (!normalizedId.startsWith(`${normalizedRoot}/`)) return void 0;
1067
+ const [pathname, query = ""] = normalizedId.split("?", 2);
1068
+ const isStylesheet = pathname?.endsWith(".css");
1069
+ const isAstroStyle = pathname?.endsWith(".astro") && query.includes("type=style");
1070
+ if (!isStylesheet && !isAstroStyle) return void 0;
1071
+ return {
1072
+ code: "",
1073
+ map: null
1074
+ };
1075
+ }
1076
+ };
1077
+ }
557
1078
  function starlightContentUrl(route, srcDir) {
558
1079
  const slug = route === "/" ? "index" : route.replace(/^\/+|\/+$/g, "");
559
1080
  return new URL(`content/docs/${slug}.md`, srcDir);