blume 0.5.2 → 0.5.4

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.
Files changed (137) hide show
  1. package/dist/cli/index.js +2235 -5942
  2. package/dist/cli/index.js.map +36 -65
  3. package/dist/types/core/config.d.ts +0 -8
  4. package/dist/types/core/data.d.ts +2 -2
  5. package/dist/types/core/schema.d.ts +58 -453
  6. package/dist/types/core/types.d.ts +1 -7
  7. package/docs/advanced/meta.ts +1 -8
  8. package/docs/advanced/skills.mdx +28 -0
  9. package/docs/configuration/ai.mdx +3 -1
  10. package/docs/configuration/index.mdx +13 -17
  11. package/docs/configuration/theming.mdx +3 -11
  12. package/docs/content/components.mdx +2 -53
  13. package/docs/content/i18n.mdx +1 -5
  14. package/docs/content/sources.mdx +1 -1
  15. package/docs/content/syntax.mdx +1 -1
  16. package/docs/index.mdx +0 -1
  17. package/docs/reference/cli.mdx +12 -13
  18. package/package.json +4 -6
  19. package/skills/blume/SKILL.md +71 -0
  20. package/skills/blume-update-docs/SKILL.md +52 -0
  21. package/skills/blume-update-docs/references/audit-checklist.md +46 -0
  22. package/src/ai/mcp/discovery.ts +3 -1
  23. package/src/ai/mcp/server.ts +3 -1
  24. package/src/astro/component-slots.ts +10 -2
  25. package/src/astro/generate.ts +15 -12
  26. package/src/astro/integration.ts +0 -21
  27. package/src/astro/templates.ts +62 -37
  28. package/src/cli/coalesce.ts +43 -0
  29. package/src/cli/commands/dev.ts +30 -22
  30. package/src/cli/commands/validate.ts +0 -2
  31. package/src/cli/dev-lock.ts +4 -2
  32. package/src/cli/index.ts +0 -2
  33. package/src/components/BlumePage.astro +0 -6
  34. package/src/components/Icon.astro +1 -12
  35. package/src/components/content/AccordionItem.astro +3 -6
  36. package/src/components/content/Badge.astro +1 -3
  37. package/src/components/content/Callout.astro +3 -9
  38. package/src/components/content/Card.astro +2 -3
  39. package/src/components/content/ColorItem.astro +8 -5
  40. package/src/components/content/Column.astro +1 -1
  41. package/src/components/content/Prompt.astro +8 -4
  42. package/src/components/content/Step.astro +3 -4
  43. package/src/components/content/Tab.astro +2 -3
  44. package/src/components/content/Tabs.astro +13 -2
  45. package/src/components/content/TypeTable.astro +5 -5
  46. package/src/components/content/Update.astro +1 -1
  47. package/src/components/content/mermaid-element.ts +20 -2
  48. package/src/components/islands/ask-ai.tsx +4 -8
  49. package/src/components/islands/base-path.ts +30 -0
  50. package/src/components/islands/hooks.ts +12 -8
  51. package/src/components/layout/Header.astro +26 -3
  52. package/src/components/layout/Logo.astro +5 -1
  53. package/src/components/layout/NavSelector.astro +1 -1
  54. package/src/components/layout/NavTree.astro +14 -14
  55. package/src/components/layout/PageActions.astro +17 -11
  56. package/src/components/layout/PageLayout.astro +42 -0
  57. package/src/components/layout/ReferenceLayout.astro +1 -0
  58. package/src/components/layout/RootLayout.astro +31 -2
  59. package/src/components/layout/Search.astro +9 -6
  60. package/src/components/layout/nav-utils.ts +9 -4
  61. package/src/components/layout/search/types.ts +16 -5
  62. package/src/components/openapi/ApiOverview.astro +4 -50
  63. package/src/components/openapi/ApiTagOperations.astro +42 -0
  64. package/src/components/openapi/ParametersTable.astro +1 -1
  65. package/src/components/openapi/SchemaProperty.astro +1 -1
  66. package/src/components/openapi/SchemaTable.astro +3 -3
  67. package/src/components/openapi/helpers.ts +17 -8
  68. package/src/components/openapi/snippets.ts +17 -4
  69. package/src/core/builtin-tags.ts +1 -3
  70. package/src/core/config.ts +19 -33
  71. package/src/core/data.ts +2 -2
  72. package/src/core/graph.ts +7 -5
  73. package/src/core/links.ts +5 -19
  74. package/src/core/meta.ts +1 -1
  75. package/src/core/navigation.ts +5 -6
  76. package/src/core/project-graph.ts +0 -5
  77. package/src/core/schema.ts +13 -90
  78. package/src/core/sources/filesystem.ts +19 -1
  79. package/src/core/sources/mdx-remote.ts +20 -4
  80. package/src/core/sources/normalize.ts +28 -6
  81. package/src/core/sources/resolve.ts +0 -13
  82. package/src/core/sources/watch.ts +45 -0
  83. package/src/core/types.ts +1 -8
  84. package/src/markdown/code-title.ts +6 -3
  85. package/src/markdown/package-install.ts +3 -1
  86. package/src/og/card.ts +16 -4
  87. package/src/openapi/parse.ts +197 -14
  88. package/src/openapi/render-mdx.ts +54 -11
  89. package/src/openapi/source.ts +19 -2
  90. package/src/search/sync/orama-cloud.ts +2 -0
  91. package/src/search/sync/typesense.ts +4 -0
  92. package/src/theme/entry.ts +45 -17
  93. package/src/theme/icons.ts +22 -104
  94. package/src/theme/palette.ts +35 -47
  95. package/src/theme/twoslash.ts +6 -1
  96. package/dist/types/core/bridge.d.ts +0 -24
  97. package/dist/types/migrate/mintlify/assets.d.ts +0 -8
  98. package/dist/types/migrate/mintlify/config.d.ts +0 -16
  99. package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
  100. package/docs/advanced/bridge.mdx +0 -76
  101. package/docs/advanced/migrate.mdx +0 -123
  102. package/src/astro/static-assets.ts +0 -117
  103. package/src/cli/commands/migrate.ts +0 -39
  104. package/src/components/content/ApiField.astro +0 -75
  105. package/src/components/content/ParamField.astro +0 -39
  106. package/src/components/content/RequestField.astro +0 -23
  107. package/src/components/content/ResponseField.astro +0 -23
  108. package/src/components/content/Warning.astro +0 -9
  109. package/src/core/assets.ts +0 -31
  110. package/src/core/bridge.ts +0 -102
  111. package/src/core/sources/mintlify.ts +0 -204
  112. package/src/migrate/fumadocs/config.ts +0 -155
  113. package/src/migrate/fumadocs/content.ts +0 -378
  114. package/src/migrate/fumadocs/frontmatter.ts +0 -18
  115. package/src/migrate/fumadocs/groups.ts +0 -237
  116. package/src/migrate/fumadocs/index.ts +0 -340
  117. package/src/migrate/fumadocs/meta.ts +0 -244
  118. package/src/migrate/migrate.ts +0 -53
  119. package/src/migrate/mintlify/assets.ts +0 -46
  120. package/src/migrate/mintlify/config.ts +0 -958
  121. package/src/migrate/mintlify/content.ts +0 -120
  122. package/src/migrate/mintlify/frontmatter.ts +0 -126
  123. package/src/migrate/mintlify/i18n.ts +0 -51
  124. package/src/migrate/mintlify/icons.ts +0 -128
  125. package/src/migrate/mintlify/index.ts +0 -372
  126. package/src/migrate/mintlify/snippets.ts +0 -306
  127. package/src/migrate/mintlify/transform.ts +0 -82
  128. package/src/migrate/nextra/content.ts +0 -46
  129. package/src/migrate/nextra/frontmatter.ts +0 -40
  130. package/src/migrate/nextra/index.ts +0 -374
  131. package/src/migrate/nextra/meta.ts +0 -266
  132. package/src/migrate/shared.ts +0 -728
  133. package/src/migrate/starlight/config.ts +0 -455
  134. package/src/migrate/starlight/content.ts +0 -78
  135. package/src/migrate/starlight/frontmatter.ts +0 -111
  136. package/src/migrate/starlight/i18n.ts +0 -54
  137. package/src/migrate/starlight/index.ts +0 -131
@@ -1,455 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
-
4
- import { join } from "pathe";
5
-
6
- import type { BlumeConfig, SidebarItemConfig } from "../../core/schema.ts";
7
- import type { LiteralValue } from "../shared.ts";
8
- import {
9
- asLiteralArray as asArray,
10
- asLiteralString as asString,
11
- isLiteralObject as isObject,
12
- parseLiteral,
13
- scanObject,
14
- stripJsComments,
15
- } from "../shared.ts";
16
-
17
- /**
18
- * Translate a Starlight `astro.config.*` into a `BlumeConfig`. The
19
- * `starlight({...})` options object is read statically out of the source text —
20
- * never executed — mirroring the Nextra `_meta` reader. Values that aren't pure
21
- * literals are skipped and reported, so a config that references imported assets
22
- * or plugins still yields a usable result instead of failing wholesale.
23
- */
24
-
25
- const CONFIG_FILES = [
26
- "astro.config.mjs",
27
- "astro.config.mts",
28
- "astro.config.ts",
29
- "astro.config.js",
30
- "astro.config.cjs",
31
- ];
32
-
33
- const STARLIGHT_IMPORT =
34
- /import\s+(?<name>\w+)\s+from\s+["']@astrojs\/starlight["']/u;
35
-
36
- type StarlightOptions = Record<string, LiteralValue>;
37
-
38
- /** Extract the object literal passed to `starlight(…)` from config source. */
39
- const extractStarlightOptions = (
40
- source: string
41
- ): StarlightOptions | "unparseable" | "missing" => {
42
- const clean = stripJsComments(source);
43
- const importName = STARLIGHT_IMPORT.exec(clean)?.groups?.name ?? "starlight";
44
- const call = new RegExp(`\\b${importName}\\s*\\(`, "u").exec(clean);
45
- if (!call) {
46
- return "missing";
47
- }
48
- let index = call.index + call[0].length;
49
- while (index < clean.length && /\s/u.test(clean[index] ?? "")) {
50
- index += 1;
51
- }
52
- if (clean[index] !== "{") {
53
- // `starlight()` with no/non-literal options — nothing to translate.
54
- return clean[index] === ")" ? {} : "unparseable";
55
- }
56
- const scan = scanObject(clean, index);
57
- if (!scan) {
58
- return "unparseable";
59
- }
60
- const parsed = parseLiteral(clean.slice(index, scan.end + 1));
61
- return isObject(parsed) ? parsed : "unparseable";
62
- };
63
-
64
- export interface LoadedStarlightConfig {
65
- options: StarlightOptions;
66
- warnings: string[];
67
- }
68
-
69
- /** Locate and read the Starlight options out of the project's astro config. */
70
- export const loadStarlightConfig = async (
71
- root: string
72
- ): Promise<LoadedStarlightConfig> => {
73
- const file = CONFIG_FILES.map((name) => join(root, name)).find((path) =>
74
- existsSync(path)
75
- );
76
- if (!file) {
77
- return {
78
- options: {},
79
- warnings: [
80
- "No astro.config.* found; wrote a default config — port your Starlight options by hand.",
81
- ],
82
- };
83
- }
84
-
85
- const source = await readFile(file, "utf-8");
86
- const result = extractStarlightOptions(source);
87
- if (result === "missing") {
88
- return {
89
- options: {},
90
- warnings: [
91
- "Could not find a starlight() call in astro.config.*; port your options by hand.",
92
- ],
93
- };
94
- }
95
- if (result === "unparseable") {
96
- return {
97
- options: {},
98
- warnings: [
99
- "Could not statically read the starlight() options; port your config by hand.",
100
- ],
101
- };
102
- }
103
- return { options: result, warnings: [] };
104
- };
105
-
106
- // ---------------------------------------------------------------------------
107
- // Field mappers
108
- // ---------------------------------------------------------------------------
109
-
110
- type LogoConfig = string | { alt?: string; dark?: string; light?: string };
111
-
112
- const mapLogo = (
113
- value: LiteralValue | undefined,
114
- warnings: string[]
115
- ): LogoConfig | undefined => {
116
- const logo = isObject(value) ? value : undefined;
117
- if (!logo) {
118
- return undefined;
119
- }
120
- const alt = asString(logo.alt);
121
- const src = asString(logo.src);
122
- const light = asString(logo.light);
123
- const dark = asString(logo.dark);
124
-
125
- let result: LogoConfig | undefined;
126
- if (src) {
127
- result = alt ? { alt, dark: src, light: src } : src;
128
- } else if (light || dark) {
129
- result = {
130
- ...(alt ? { alt } : {}),
131
- ...(dark ? { dark } : {}),
132
- ...(light ? { light } : {}),
133
- };
134
- }
135
- if (result) {
136
- warnings.push(
137
- "Mapped the Starlight logo; move the referenced file under public/ (Blume serves static assets from there)."
138
- );
139
- }
140
- return result;
141
- };
142
-
143
- const GITHUB_REPO = /github\.com\/(?<owner>[^/]+)\/(?<repo>[^/?#]+)/u;
144
- const EDIT_LINK =
145
- /github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/edit\/(?<branch>[^/?#]+)/u;
146
-
147
- interface GithubConfig {
148
- branch?: string;
149
- owner: string;
150
- repo: string;
151
- }
152
-
153
- const githubFromUrl = (url: string): GithubConfig | undefined => {
154
- const groups = GITHUB_REPO.exec(url)?.groups;
155
- if (!groups?.owner || !groups.repo) {
156
- return undefined;
157
- }
158
- return { owner: groups.owner, repo: groups.repo.replace(/\.git$/u, "") };
159
- };
160
-
161
- interface SocialMapping {
162
- github?: GithubConfig;
163
- socials: Record<string, string>;
164
- }
165
-
166
- /** Normalize a Starlight social icon key into a Blume `footer.socials` key. */
167
- const socialKey = (icon: string): string =>
168
- icon.toLowerCase().replace(/\.com$/u, "");
169
-
170
- const mapSocial = (value: LiteralValue | undefined): SocialMapping => {
171
- const result: SocialMapping = { socials: {} };
172
- const record = (icon: string, href: string): void => {
173
- const key = socialKey(icon);
174
- result.socials[key] = href;
175
- if (key === "github" && !result.github) {
176
- result.github = githubFromUrl(href);
177
- }
178
- };
179
-
180
- const array = asArray(value);
181
- if (array) {
182
- for (const entry of array) {
183
- if (!isObject(entry)) {
184
- continue;
185
- }
186
- const icon = asString(entry.icon);
187
- const href = asString(entry.href);
188
- if (icon && href) {
189
- record(icon, href);
190
- }
191
- }
192
- return result;
193
- }
194
- // Older Starlight: `social: { github: "https://…", discord: "https://…" }`.
195
- if (isObject(value)) {
196
- for (const [icon, href] of Object.entries(value)) {
197
- const url = asString(href);
198
- if (url) {
199
- record(icon, url);
200
- }
201
- }
202
- }
203
- return result;
204
- };
205
-
206
- const mapEditLink = (
207
- value: LiteralValue | undefined
208
- ): GithubConfig | undefined => {
209
- const editLink = isObject(value) ? value : undefined;
210
- const baseUrl = asString(editLink?.baseUrl);
211
- if (!baseUrl) {
212
- return undefined;
213
- }
214
- const groups = EDIT_LINK.exec(baseUrl)?.groups;
215
- if (!groups?.owner || !groups.repo || !groups.branch) {
216
- return undefined;
217
- }
218
- return {
219
- branch: groups.branch,
220
- owner: groups.owner,
221
- repo: groups.repo.replace(/\.git$/u, ""),
222
- };
223
- };
224
-
225
- /** Flatten a Starlight badge (`string` or `{ text, variant }`) to a string. */
226
- const badgeText = (value: LiteralValue | undefined): string | undefined =>
227
- asString(value) ?? asString(isObject(value) ? value.text : undefined);
228
-
229
- const mapSidebarItem = (
230
- value: LiteralValue,
231
- warnings: string[]
232
- ): SidebarItemConfig | null => {
233
- if (typeof value === "string") {
234
- return `/${value.replace(/^\/+/u, "")}`;
235
- }
236
- if (!isObject(value)) {
237
- return null;
238
- }
239
- const label = asString(value.label);
240
- const badge = badgeText(value.badge);
241
- const collapsed =
242
- typeof value.collapsed === "boolean" ? value.collapsed : undefined;
243
-
244
- const autogenerate = isObject(value.autogenerate)
245
- ? asString(value.autogenerate.directory)
246
- : undefined;
247
- if (autogenerate && label) {
248
- return {
249
- label,
250
- ...(badge ? { badge } : {}),
251
- ...(collapsed === undefined ? {} : { collapsed }),
252
- root: autogenerate,
253
- };
254
- }
255
-
256
- const items = asArray(value.items);
257
- if (items && label) {
258
- const mapped = items
259
- .map((item) => mapSidebarItem(item, warnings))
260
- .filter((item): item is SidebarItemConfig => item !== null);
261
- return {
262
- ...(badge ? { badge } : {}),
263
- ...(collapsed === undefined ? {} : { collapsed }),
264
- items: mapped,
265
- label,
266
- };
267
- }
268
-
269
- const link = asString(value.link);
270
- if (link) {
271
- return label
272
- ? { ...(badge ? { badge } : {}), href: link, label }
273
- : { href: link, label: link };
274
- }
275
-
276
- const slug = asString(value.slug);
277
- if (slug) {
278
- const href = `/${slug.replace(/^\/+/u, "")}`;
279
- return label ? { ...(badge ? { badge } : {}), href, label } : href;
280
- }
281
-
282
- warnings.push("Skipped a sidebar entry that uses an unsupported shape.");
283
- return null;
284
- };
285
-
286
- const mapSidebar = (
287
- value: LiteralValue | undefined,
288
- warnings: string[]
289
- ): SidebarItemConfig[] | undefined => {
290
- const array = asArray(value);
291
- if (!array) {
292
- return undefined;
293
- }
294
- const items = array
295
- .map((item) => mapSidebarItem(item, warnings))
296
- .filter((item): item is SidebarItemConfig => item !== null);
297
- return items.length > 0 ? items : undefined;
298
- };
299
-
300
- const mapCodeTheme = (
301
- value: LiteralValue | undefined,
302
- warnings: string[]
303
- ): { dark: string; light: string } | undefined => {
304
- const expressive = isObject(value) ? value : undefined;
305
- if (value === false) {
306
- warnings.push(
307
- "expressiveCode is disabled in Starlight; Blume always renders code blocks."
308
- );
309
- return undefined;
310
- }
311
- if (!expressive) {
312
- return undefined;
313
- }
314
- if (expressive.styleOverrides !== undefined) {
315
- warnings.push(
316
- "Dropped expressiveCode.styleOverrides; restyle code blocks via theme.css."
317
- );
318
- }
319
- const themes = asArray(expressive.themes)?.filter(
320
- (theme): theme is string => typeof theme === "string"
321
- );
322
- const [first, second] = themes ?? [];
323
- if (!first) {
324
- return undefined;
325
- }
326
- const dark = themes?.find((theme) => /dark/u.test(theme)) ?? first;
327
- const light =
328
- themes?.find((theme) => /light/u.test(theme)) ?? second ?? first;
329
- return { dark, light };
330
- };
331
-
332
- const mapHead = (
333
- value: LiteralValue | undefined,
334
- warnings: string[]
335
- ): Record<string, string> | undefined => {
336
- const array = asArray(value);
337
- if (!array) {
338
- return undefined;
339
- }
340
- const metatags: Record<string, string> = {};
341
- for (const entry of array) {
342
- if (!isObject(entry)) {
343
- continue;
344
- }
345
- const tag = asString(entry.tag);
346
- const attrs = isObject(entry.attrs) ? entry.attrs : undefined;
347
- const content = asString(attrs?.content);
348
- const key = asString(attrs?.name) ?? asString(attrs?.property);
349
- if (tag === "meta" && key && content) {
350
- metatags[key] = content;
351
- } else {
352
- warnings.push(
353
- `Dropped a <${tag ?? "?"}> head entry; re-add scripts/links via a custom layout.`
354
- );
355
- }
356
- }
357
- return Object.keys(metatags).length > 0 ? metatags : undefined;
358
- };
359
-
360
- /** Warn about Starlight options that carry over to Blume only by hand. */
361
- const warnDroppedOptions = (
362
- options: StarlightOptions,
363
- warnings: string[]
364
- ): void => {
365
- if (options.customCss !== undefined) {
366
- warnings.push(
367
- "Dropped customCss; move your custom styles into a top-level theme.css."
368
- );
369
- }
370
- if (isObject(options.components)) {
371
- warnings.push(
372
- "Dropped Starlight component overrides; re-implement them via `blume eject`."
373
- );
374
- }
375
- if (asArray(options.plugins)?.length) {
376
- warnings.push(
377
- "Dropped Starlight plugins; they have no Blume equivalent and need manual review."
378
- );
379
- }
380
- if (options.routeMiddleware !== undefined) {
381
- warnings.push(
382
- "Dropped routeMiddleware; re-implement any route data in Blume directly."
383
- );
384
- }
385
- };
386
-
387
- /**
388
- * Map the statically-read Starlight options onto a `BlumeConfig`. Content stays
389
- * under `src/content/docs`, so the content root is pinned there. Unsupported or
390
- * non-literal options are dropped with a warning rather than failing.
391
- */
392
- export const mapStarlightConfig = (
393
- options: StarlightOptions,
394
- warnings: string[]
395
- ): BlumeConfig => {
396
- const config: BlumeConfig = {
397
- content: { root: "src/content/docs" },
398
- title: asString(options.title) ?? "Documentation",
399
- };
400
- if (options.title !== undefined && asString(options.title) === undefined) {
401
- warnings.push(
402
- "Starlight title is per-locale or computed; set config.title manually."
403
- );
404
- }
405
-
406
- const description =
407
- asString(options.description) ?? asString(options.tagline);
408
- if (description) {
409
- config.description = description;
410
- }
411
-
412
- const logo = mapLogo(options.logo, warnings);
413
- if (logo) {
414
- config.logo = logo;
415
- }
416
-
417
- const favicon = asString(options.favicon);
418
- if (favicon) {
419
- config.favicon = favicon;
420
- if (!favicon.startsWith("/") && !/^https?:/u.test(favicon)) {
421
- warnings.push(
422
- `favicon "${favicon}" should live under public/ and be referenced from the site root.`
423
- );
424
- }
425
- }
426
-
427
- const social = mapSocial(options.social);
428
- const github = mapEditLink(options.editLink) ?? social.github;
429
- if (github) {
430
- config.github = github;
431
- }
432
-
433
- const sidebar = mapSidebar(options.sidebar, warnings);
434
- if (sidebar) {
435
- config.navigation = { sidebar };
436
- }
437
-
438
- const theme = mapCodeTheme(options.expressiveCode, warnings);
439
- if (theme) {
440
- config.markdown = { codeBlocks: { theme } };
441
- }
442
-
443
- const metatags = mapHead(options.head, warnings);
444
- if (metatags) {
445
- config.seo = { metatags };
446
- }
447
-
448
- if (options.lastUpdated === true) {
449
- config.lastModified = true;
450
- }
451
-
452
- warnDroppedOptions(options, warnings);
453
-
454
- return config;
455
- };
@@ -1,78 +0,0 @@
1
- import { renameTag, rewriteCallouts } from "../shared.ts";
2
-
3
- /**
4
- * Source-to-source rewrites that turn Starlight-only MDX into idiomatic Blume
5
- * markup. Runs once at migration time — no Starlight-aware plugins remain in the
6
- * Blume runtime.
7
- */
8
-
9
- const STARLIGHT_IMPORT =
10
- /^import\s+[\s\S]*?\s+from\s+["']@astrojs\/starlight(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
11
-
12
- /**
13
- * Drop `import … from "@astrojs/starlight/components"` (and other subpaths).
14
- * Blume injects its components globally, so these imports would fail to resolve
15
- * once the Starlight packages are gone.
16
- */
17
- export const stripStarlightImports = (source: string): string => {
18
- const stripped = source.replace(STARLIGHT_IMPORT, "");
19
- // Collapse the blank gap a removed import block leaves behind.
20
- return stripped === source ? source : stripped.replaceAll(/\n{3,}/gu, "\n\n");
21
- };
22
-
23
- /** Starlight `<Aside type="X">` values mapped to Blume directive names. */
24
- const ASIDE_TYPE_DIRECTIVES: Record<string, string> = {
25
- caution: "caution",
26
- danger: "danger",
27
- note: "note",
28
- tip: "tip",
29
- };
30
-
31
- /**
32
- * Convert Starlight `<Aside type="note|tip|caution|danger" title="…">`
33
- * components into Blume `:::` directives. A bare `<Aside>` becomes `:::note`.
34
- * (Plain `:::`-syntax asides already pass through unchanged — Blume's directive
35
- * set covers all four types, with `caution` aliased to `warning`.)
36
- */
37
- export const rewriteStarlightAsides = (source: string): string =>
38
- rewriteCallouts(source, {
39
- defaultDirective: "note",
40
- tagDirectives: {},
41
- tags: ["Aside"],
42
- typeDirectives: ASIDE_TYPE_DIRECTIVES,
43
- });
44
-
45
- /**
46
- * Rename Starlight container components to their Blume equivalents, then move
47
- * each `<Tab>`'s `label` onto the `title` prop Blume reads. `LinkCard` folds into
48
- * `Card` (which renders an `href`); its `description` has no Card prop and is
49
- * dropped. `CardGrid`'s `stagger` layout flag has no equivalent and is left on
50
- * the tag harmlessly (Blume ignores unknown attributes).
51
- */
52
- export const rewriteStarlightComponents = (source: string): string => {
53
- let out = renameTag(source, "CardGrid", "CardGroup");
54
- out = renameTag(out, "LinkCard", "Card");
55
- out = renameTag(out, "TabItem", "Tab");
56
- // Starlight selects a tab label with `label=`; Blume's `<Tab>` uses `title=`.
57
- out = out.replaceAll(
58
- /(?<tab><Tab\b[^>]*?)\blabel(?<eq>\s*=)/gu,
59
- "$<tab>title$<eq>"
60
- );
61
- return out;
62
- };
63
-
64
- /** Starlight components with no drop-in Blume equivalent — flagged for review. */
65
- const UNSUPPORTED_COMPONENTS = ["Code", "FileTree", "LinkButton", "Steps"];
66
-
67
- /** Names of Starlight components in `source` that need manual attention. */
68
- export const unsupportedStarlightComponents = (source: string): string[] =>
69
- UNSUPPORTED_COMPONENTS.filter((name) =>
70
- new RegExp(`<${name}\\b`, "u").test(source)
71
- );
72
-
73
- const ASSET_ALIAS =
74
- /(?:!\[[^\]]*\]\(|\bsrc\s*=\s*["'])(?<path>[~@]\/[^"')\s]+)/u;
75
-
76
- /** True when the source references images via Starlight's `~/`/`@/` aliases. */
77
- export const hasAliasedAssets = (source: string): boolean =>
78
- ASSET_ALIAS.test(source);
@@ -1,111 +0,0 @@
1
- import { z } from "zod";
2
-
3
- import { stripUnknownPageMeta } from "../shared.ts";
4
-
5
- /**
6
- * One-time translation of Starlight page frontmatter into Blume's shape. Runs at
7
- * migration time only — Blume's runtime page schema stays Starlight-free and
8
- * strict. `sidebar.badge` flattens to a string, `pagefind:false` becomes
9
- * `search.exclude`, `lastUpdated` (a date) becomes `lastModified`, and
10
- * `prev`/`next:false` become `hideFooterPagination`. Starlight-only keys
11
- * (`template`, `hero`, `banner`, `tableOfContents`, `editUrl`, `head`) are then
12
- * dropped by the strict-schema pass and reported.
13
- */
14
-
15
- const starlightBadgeSchema = z.union([
16
- z.string(),
17
- z.object({ text: z.string() }).passthrough(),
18
- ]);
19
-
20
- const starlightSidebarSchema = z
21
- .object({
22
- badge: starlightBadgeSchema.optional(),
23
- hidden: z.boolean().optional(),
24
- label: z.string().optional(),
25
- order: z.number().optional(),
26
- })
27
- .passthrough();
28
-
29
- const starlightPageMetaInputSchema = z
30
- .object({
31
- lastUpdated: z.union([z.date(), z.string(), z.boolean()]).optional(),
32
- next: z.unknown().optional(),
33
- pagefind: z.boolean().optional(),
34
- prev: z.unknown().optional(),
35
- sidebar: starlightSidebarSchema.optional(),
36
- })
37
- .passthrough();
38
-
39
- type StarlightPageMeta = z.infer<typeof starlightPageMetaInputSchema>;
40
-
41
- const normalizedSidebar = (
42
- sidebar: NonNullable<StarlightPageMeta["sidebar"]>
43
- ): Record<string, unknown> => {
44
- const out: Record<string, unknown> = {};
45
- if (sidebar.label !== undefined) {
46
- out.label = sidebar.label;
47
- }
48
- if (sidebar.order !== undefined) {
49
- out.order = sidebar.order;
50
- }
51
- if (sidebar.hidden !== undefined) {
52
- out.hidden = sidebar.hidden;
53
- }
54
- const badge =
55
- typeof sidebar.badge === "string" ? sidebar.badge : sidebar.badge?.text;
56
- if (badge !== undefined) {
57
- out.badge = badge;
58
- }
59
- return out;
60
- };
61
-
62
- /**
63
- * Map Starlight page frontmatter onto Blume's frontmatter shape, returning the
64
- * cleaned data plus any unknown keys dropped to satisfy the strict page schema.
65
- */
66
- export const normalizeStarlightPageMeta = (
67
- value: unknown
68
- ): { data: Record<string, unknown>; removed: string[] } => {
69
- const parsed = starlightPageMetaInputSchema.safeParse(value);
70
- if (!parsed.success || typeof value !== "object" || value === null) {
71
- return stripUnknownPageMeta((value ?? {}) as Record<string, unknown>);
72
- }
73
-
74
- const meta = parsed.data;
75
- const data: Record<string, unknown> = {
76
- ...(value as Record<string, unknown>),
77
- };
78
-
79
- if (meta.sidebar) {
80
- const sidebar = normalizedSidebar(meta.sidebar);
81
- if (Object.keys(sidebar).length > 0) {
82
- data.sidebar = sidebar;
83
- } else {
84
- delete data.sidebar;
85
- }
86
- }
87
-
88
- if (meta.pagefind === false) {
89
- const existing =
90
- typeof data.search === "object" && data.search !== null
91
- ? (data.search as Record<string, unknown>)
92
- : {};
93
- data.search = { ...existing, exclude: true };
94
- }
95
- delete data.pagefind;
96
-
97
- if (meta.lastUpdated instanceof Date) {
98
- data.lastModified = meta.lastUpdated.toISOString();
99
- } else if (typeof meta.lastUpdated === "string") {
100
- data.lastModified = meta.lastUpdated;
101
- }
102
- delete data.lastUpdated;
103
-
104
- if (meta.prev === false || meta.next === false) {
105
- data.hideFooterPagination = true;
106
- }
107
- delete data.prev;
108
- delete data.next;
109
-
110
- return stripUnknownPageMeta(data);
111
- };
@@ -1,54 +0,0 @@
1
- import type { BlumeConfig } from "../../core/schema.ts";
2
- import type { LiteralValue } from "../shared.ts";
3
- import { asLiteralString, isLiteralObject } from "../shared.ts";
4
-
5
- /**
6
- * Map Starlight's `defaultLocale` + `locales` onto Blume's `i18n` config.
7
- * Starlight keys locales by URL segment (`root` for the un-prefixed default) and
8
- * carries the BCP-47 tag in `lang`; Blume keys by `code`. The `root` locale maps
9
- * to the default with `hideDefaultLocalePrefix`, and locale directories under
10
- * `src/content/docs` line up with Blume's `"dir"` parser.
11
- */
12
-
13
- type I18nConfig = NonNullable<BlumeConfig["i18n"]>;
14
-
15
- export const starlightI18n = (
16
- options: Record<string, LiteralValue>
17
- ): I18nConfig | undefined => {
18
- const locales = isLiteralObject(options.locales)
19
- ? options.locales
20
- : undefined;
21
- if (!locales) {
22
- return undefined;
23
- }
24
-
25
- const entries: { code: string; label: string }[] = [];
26
- let rootCode: string | undefined;
27
- for (const [key, value] of Object.entries(locales)) {
28
- const entry = isLiteralObject(value) ? value : undefined;
29
- const lang = asLiteralString(entry?.lang);
30
- const label = asLiteralString(entry?.label) ?? key;
31
- const code = key === "root" ? (lang ?? "en") : (lang ?? key);
32
- if (key === "root") {
33
- rootCode = code;
34
- }
35
- entries.push({ code, label });
36
- }
37
- if (entries.length === 0) {
38
- return undefined;
39
- }
40
-
41
- const requested = asLiteralString(options.defaultLocale);
42
- const fallback = rootCode ?? entries[0]?.code ?? "en";
43
- const preferred =
44
- requested === undefined || requested === "root" ? fallback : requested;
45
- const codes = new Set(entries.map((locale) => locale.code));
46
- const defaultLocale = codes.has(preferred) ? preferred : fallback;
47
-
48
- return {
49
- defaultLocale,
50
- hideDefaultLocalePrefix: true,
51
- locales: entries,
52
- parser: "dir",
53
- };
54
- };