blume 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +12696 -0
  3. package/dist/cli/index.js.map +145 -0
  4. package/dist/types/core/bridge.d.ts +24 -0
  5. package/dist/types/core/config.d.ts +35 -0
  6. package/dist/types/core/data.d.ts +129 -0
  7. package/dist/types/core/define-components.d.ts +27 -0
  8. package/dist/types/core/define-meta.d.ts +16 -0
  9. package/dist/types/core/deployment-env.d.ts +9 -0
  10. package/dist/types/core/diagnostics.d.ts +17 -0
  11. package/dist/types/core/i18n-ui.d.ts +500 -0
  12. package/dist/types/core/load-module.d.ts +7 -0
  13. package/dist/types/core/package-root.d.ts +17 -0
  14. package/dist/types/core/project.d.ts +9 -0
  15. package/dist/types/core/schema.d.ts +3451 -0
  16. package/dist/types/core/sources/types.d.ts +107 -0
  17. package/dist/types/core/types.d.ts +245 -0
  18. package/dist/types/core/ui-packs/ar.d.ts +3 -0
  19. package/dist/types/core/ui-packs/bg.d.ts +3 -0
  20. package/dist/types/core/ui-packs/bn.d.ts +3 -0
  21. package/dist/types/core/ui-packs/ca.d.ts +3 -0
  22. package/dist/types/core/ui-packs/cs.d.ts +3 -0
  23. package/dist/types/core/ui-packs/da.d.ts +3 -0
  24. package/dist/types/core/ui-packs/de.d.ts +3 -0
  25. package/dist/types/core/ui-packs/el.d.ts +3 -0
  26. package/dist/types/core/ui-packs/es.d.ts +3 -0
  27. package/dist/types/core/ui-packs/fa.d.ts +3 -0
  28. package/dist/types/core/ui-packs/fi.d.ts +3 -0
  29. package/dist/types/core/ui-packs/fr.d.ts +3 -0
  30. package/dist/types/core/ui-packs/he.d.ts +3 -0
  31. package/dist/types/core/ui-packs/hi.d.ts +3 -0
  32. package/dist/types/core/ui-packs/hr.d.ts +3 -0
  33. package/dist/types/core/ui-packs/hu.d.ts +3 -0
  34. package/dist/types/core/ui-packs/id.d.ts +3 -0
  35. package/dist/types/core/ui-packs/index.d.ts +13 -0
  36. package/dist/types/core/ui-packs/it.d.ts +3 -0
  37. package/dist/types/core/ui-packs/ja.d.ts +3 -0
  38. package/dist/types/core/ui-packs/ko.d.ts +3 -0
  39. package/dist/types/core/ui-packs/nl.d.ts +3 -0
  40. package/dist/types/core/ui-packs/no.d.ts +3 -0
  41. package/dist/types/core/ui-packs/pl.d.ts +3 -0
  42. package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
  43. package/dist/types/core/ui-packs/pt.d.ts +3 -0
  44. package/dist/types/core/ui-packs/ro.d.ts +3 -0
  45. package/dist/types/core/ui-packs/ru.d.ts +3 -0
  46. package/dist/types/core/ui-packs/sk.d.ts +3 -0
  47. package/dist/types/core/ui-packs/sr.d.ts +3 -0
  48. package/dist/types/core/ui-packs/sv.d.ts +3 -0
  49. package/dist/types/core/ui-packs/th.d.ts +3 -0
  50. package/dist/types/core/ui-packs/tr.d.ts +3 -0
  51. package/dist/types/core/ui-packs/uk.d.ts +3 -0
  52. package/dist/types/core/ui-packs/vi.d.ts +3 -0
  53. package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
  54. package/dist/types/core/ui-packs/zh.d.ts +3 -0
  55. package/dist/types/core/version.d.ts +8 -0
  56. package/dist/types/index.d.ts +10 -0
  57. package/dist/types/migrate/mintlify/config.d.ts +2 -0
  58. package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
  59. package/dist/types/theme/fonts.d.ts +163 -0
  60. package/docs/01-quickstart.mdx +99 -0
  61. package/docs/02-deployment.mdx +129 -0
  62. package/docs/advanced/api-reference.mdx +114 -0
  63. package/docs/advanced/blog.mdx +121 -0
  64. package/docs/advanced/changelog.mdx +113 -0
  65. package/docs/advanced/custom-pages.mdx +268 -0
  66. package/docs/advanced/meta.ts +7 -0
  67. package/docs/changelog/v0-1-0.mdx +12 -0
  68. package/docs/changelog/v0-2-0.mdx +16 -0
  69. package/docs/configuration/ai.mdx +228 -0
  70. package/docs/configuration/analytics.mdx +98 -0
  71. package/docs/configuration/customization.mdx +91 -0
  72. package/docs/configuration/export.mdx +70 -0
  73. package/docs/configuration/index.mdx +290 -0
  74. package/docs/configuration/meta.ts +15 -0
  75. package/docs/configuration/search.mdx +172 -0
  76. package/docs/configuration/seo.mdx +196 -0
  77. package/docs/configuration/theming.mdx +178 -0
  78. package/docs/content/components.mdx +651 -0
  79. package/docs/content/i18n.mdx +205 -0
  80. package/docs/content/index.mdx +161 -0
  81. package/docs/content/islands.mdx +94 -0
  82. package/docs/content/meta.mdx +119 -0
  83. package/docs/content/meta.ts +15 -0
  84. package/docs/content/navigation.mdx +168 -0
  85. package/docs/content/sources.mdx +216 -0
  86. package/docs/content/syntax.mdx +445 -0
  87. package/docs/index.mdx +112 -0
  88. package/docs/reference/cli.mdx +43 -0
  89. package/docs/reference/frontmatter.mdx +74 -0
  90. package/docs/reference/meta.ts +7 -0
  91. package/package.json +150 -6
  92. package/src/ai/ask.ts +93 -0
  93. package/src/ai/llms.ts +64 -0
  94. package/src/ai/markdown.ts +31 -0
  95. package/src/ai/mcp/data.ts +74 -0
  96. package/src/ai/mcp/discovery.ts +49 -0
  97. package/src/ai/mcp/server.ts +225 -0
  98. package/src/ai/mcp/tools.ts +47 -0
  99. package/src/assets/icon.png +0 -0
  100. package/src/astro/examples.ts +104 -0
  101. package/src/astro/generate.ts +1006 -0
  102. package/src/astro/index.ts +4 -0
  103. package/src/astro/integration.ts +74 -0
  104. package/src/astro/islands.ts +131 -0
  105. package/src/astro/markdown-negotiation.ts +68 -0
  106. package/src/astro/pages.ts +87 -0
  107. package/src/astro/templates.ts +1326 -0
  108. package/src/cli/commands/add.ts +81 -0
  109. package/src/cli/commands/build.ts +103 -0
  110. package/src/cli/commands/dev.ts +108 -0
  111. package/src/cli/commands/doctor.ts +74 -0
  112. package/src/cli/commands/eject.ts +57 -0
  113. package/src/cli/commands/init.ts +98 -0
  114. package/src/cli/commands/migrate.ts +39 -0
  115. package/src/cli/commands/preview.ts +39 -0
  116. package/src/cli/commands/sync.ts +52 -0
  117. package/src/cli/commands/validate.ts +61 -0
  118. package/src/cli/index.ts +35 -0
  119. package/src/cli/log.ts +37 -0
  120. package/src/cli/prepare.ts +80 -0
  121. package/src/components/Icon.astro +99 -0
  122. package/src/components/content/Accordion.astro +8 -0
  123. package/src/components/content/AccordionItem.astro +121 -0
  124. package/src/components/content/AutoTypeTable.astro +51 -0
  125. package/src/components/content/Badge.astro +124 -0
  126. package/src/components/content/Callout.astro +73 -0
  127. package/src/components/content/Card.astro +104 -0
  128. package/src/components/content/CardGroup.astro +14 -0
  129. package/src/components/content/CodeBlock.astro +28 -0
  130. package/src/components/content/CodeGroup.astro +13 -0
  131. package/src/components/content/Color.astro +15 -0
  132. package/src/components/content/ColorItem.astro +87 -0
  133. package/src/components/content/ColorRow.astro +10 -0
  134. package/src/components/content/Column.astro +6 -0
  135. package/src/components/content/Columns.astro +9 -0
  136. package/src/components/content/Component.astro +65 -0
  137. package/src/components/content/Diff.astro +44 -0
  138. package/src/components/content/Expandable.astro +11 -0
  139. package/src/components/content/FileTree.astro +8 -0
  140. package/src/components/content/Frame.astro +70 -0
  141. package/src/components/content/GithubInfo.astro +110 -0
  142. package/src/components/content/Math.astro +24 -0
  143. package/src/components/content/Panel.astro +20 -0
  144. package/src/components/content/Prompt.astro +129 -0
  145. package/src/components/content/Step.astro +34 -0
  146. package/src/components/content/Steps.astro +20 -0
  147. package/src/components/content/Tab.astro +46 -0
  148. package/src/components/content/Tabs.astro +273 -0
  149. package/src/components/content/Tile.astro +42 -0
  150. package/src/components/content/Tooltip.astro +68 -0
  151. package/src/components/content/Tree.astro +300 -0
  152. package/src/components/content/TreeFile.astro +15 -0
  153. package/src/components/content/TreeFolder.astro +62 -0
  154. package/src/components/content/TypeTable.astro +106 -0
  155. package/src/components/content/Update.astro +66 -0
  156. package/src/components/content/Visibility.astro +12 -0
  157. package/src/components/content/Warning.astro +9 -0
  158. package/src/components/content/auto-type-table.ts +141 -0
  159. package/src/components/content/diff.ts +95 -0
  160. package/src/components/content/github-info.ts +79 -0
  161. package/src/components/content/mermaid-element.ts +68 -0
  162. package/src/components/github-mark.ts +9 -0
  163. package/src/components/index.ts +14 -0
  164. package/src/components/islands/AskAI.astro +12 -0
  165. package/src/components/islands/ask-ai.tsx +156 -0
  166. package/src/components/layout/Analytics.astro +63 -0
  167. package/src/components/layout/Banner.astro +50 -0
  168. package/src/components/layout/Breadcrumbs.astro +31 -0
  169. package/src/components/layout/Favicon.astro +22 -0
  170. package/src/components/layout/Fonts.astro +14 -0
  171. package/src/components/layout/Header.astro +188 -0
  172. package/src/components/layout/LanguageSwitcher.astro +56 -0
  173. package/src/components/layout/NavTree.astro +462 -0
  174. package/src/components/layout/PageActions.astro +438 -0
  175. package/src/components/layout/PageFeedback.astro +58 -0
  176. package/src/components/layout/PageLayout.astro +173 -0
  177. package/src/components/layout/Pagination.astro +56 -0
  178. package/src/components/layout/ReferenceLayout.astro +107 -0
  179. package/src/components/layout/RootLayout.astro +537 -0
  180. package/src/components/layout/Search.astro +608 -0
  181. package/src/components/layout/TableOfContents.astro +68 -0
  182. package/src/components/layout/analytics-client.ts +38 -0
  183. package/src/components/layout/head-scripts.ts +19 -0
  184. package/src/components/layout/nav-utils.ts +87 -0
  185. package/src/components/layout/overrides.ts +32 -0
  186. package/src/components/layout/search/algolia.ts +43 -0
  187. package/src/components/layout/search/endpoint.ts +22 -0
  188. package/src/components/layout/search/flexsearch.ts +52 -0
  189. package/src/components/layout/search/orama-cloud.ts +41 -0
  190. package/src/components/layout/search/orama.ts +26 -0
  191. package/src/components/layout/search/pagefind.ts +43 -0
  192. package/src/components/layout/search/types.ts +163 -0
  193. package/src/components/layout/search/typesense.ts +60 -0
  194. package/src/components/layout/toc-element.ts +108 -0
  195. package/src/core/bridge.ts +92 -0
  196. package/src/core/config.ts +112 -0
  197. package/src/core/content.ts +50 -0
  198. package/src/core/data.ts +128 -0
  199. package/src/core/define-components.ts +34 -0
  200. package/src/core/define-meta.ts +20 -0
  201. package/src/core/deployment-env.ts +73 -0
  202. package/src/core/diagnostics.ts +104 -0
  203. package/src/core/frontmatter.ts +43 -0
  204. package/src/core/graph.ts +128 -0
  205. package/src/core/i18n-ui.ts +171 -0
  206. package/src/core/i18n.ts +169 -0
  207. package/src/core/last-modified.ts +88 -0
  208. package/src/core/links.ts +352 -0
  209. package/src/core/load-module.ts +15 -0
  210. package/src/core/manifest.ts +126 -0
  211. package/src/core/meta.ts +97 -0
  212. package/src/core/navigation.ts +392 -0
  213. package/src/core/package-root.ts +37 -0
  214. package/src/core/project-graph.ts +153 -0
  215. package/src/core/project.ts +56 -0
  216. package/src/core/schema.ts +1057 -0
  217. package/src/core/server-features.ts +23 -0
  218. package/src/core/sources/assets.ts +77 -0
  219. package/src/core/sources/cache.ts +122 -0
  220. package/src/core/sources/filesystem.ts +99 -0
  221. package/src/core/sources/mdx-remote.ts +215 -0
  222. package/src/core/sources/mintlify.ts +161 -0
  223. package/src/core/sources/normalize.ts +243 -0
  224. package/src/core/sources/notion.ts +440 -0
  225. package/src/core/sources/portable-text.ts +143 -0
  226. package/src/core/sources/read.ts +36 -0
  227. package/src/core/sources/resolve.ts +158 -0
  228. package/src/core/sources/sanity.ts +218 -0
  229. package/src/core/sources/types.ts +105 -0
  230. package/src/core/tsconfig-aliases.ts +201 -0
  231. package/src/core/types.ts +261 -0
  232. package/src/core/ui-packs/ar.ts +47 -0
  233. package/src/core/ui-packs/bg.ts +47 -0
  234. package/src/core/ui-packs/bn.ts +47 -0
  235. package/src/core/ui-packs/ca.ts +47 -0
  236. package/src/core/ui-packs/cs.ts +47 -0
  237. package/src/core/ui-packs/da.ts +47 -0
  238. package/src/core/ui-packs/de.ts +47 -0
  239. package/src/core/ui-packs/el.ts +47 -0
  240. package/src/core/ui-packs/es.ts +47 -0
  241. package/src/core/ui-packs/fa.ts +47 -0
  242. package/src/core/ui-packs/fi.ts +47 -0
  243. package/src/core/ui-packs/fr.ts +47 -0
  244. package/src/core/ui-packs/he.ts +47 -0
  245. package/src/core/ui-packs/hi.ts +47 -0
  246. package/src/core/ui-packs/hr.ts +47 -0
  247. package/src/core/ui-packs/hu.ts +47 -0
  248. package/src/core/ui-packs/id.ts +47 -0
  249. package/src/core/ui-packs/index.ts +87 -0
  250. package/src/core/ui-packs/it.ts +47 -0
  251. package/src/core/ui-packs/ja.ts +47 -0
  252. package/src/core/ui-packs/ko.ts +47 -0
  253. package/src/core/ui-packs/nl.ts +47 -0
  254. package/src/core/ui-packs/no.ts +47 -0
  255. package/src/core/ui-packs/pl.ts +47 -0
  256. package/src/core/ui-packs/pt-br.ts +47 -0
  257. package/src/core/ui-packs/pt.ts +47 -0
  258. package/src/core/ui-packs/ro.ts +47 -0
  259. package/src/core/ui-packs/ru.ts +47 -0
  260. package/src/core/ui-packs/sk.ts +47 -0
  261. package/src/core/ui-packs/sr.ts +47 -0
  262. package/src/core/ui-packs/sv.ts +47 -0
  263. package/src/core/ui-packs/th.ts +47 -0
  264. package/src/core/ui-packs/tr.ts +47 -0
  265. package/src/core/ui-packs/uk.ts +47 -0
  266. package/src/core/ui-packs/vi.ts +47 -0
  267. package/src/core/ui-packs/zh-tw.ts +47 -0
  268. package/src/core/ui-packs/zh.ts +47 -0
  269. package/src/core/version.ts +23 -0
  270. package/src/deploy/robots.ts +20 -0
  271. package/src/deploy/rss.ts +128 -0
  272. package/src/deploy/sitemap.ts +28 -0
  273. package/src/index.ts +39 -0
  274. package/src/markdown/code-title.ts +71 -0
  275. package/src/markdown/directives.ts +83 -0
  276. package/src/markdown/heading-anchors.ts +137 -0
  277. package/src/markdown/index.ts +228 -0
  278. package/src/markdown/inline-code.ts +108 -0
  279. package/src/markdown/language-icon.ts +172 -0
  280. package/src/markdown/math.ts +32 -0
  281. package/src/markdown/mdast.ts +48 -0
  282. package/src/markdown/mermaid.ts +37 -0
  283. package/src/markdown/package-commands.ts +159 -0
  284. package/src/markdown/package-install.ts +40 -0
  285. package/src/migrate/fumadocs/config.ts +155 -0
  286. package/src/migrate/fumadocs/content.ts +365 -0
  287. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  288. package/src/migrate/fumadocs/groups.ts +230 -0
  289. package/src/migrate/fumadocs/index.ts +337 -0
  290. package/src/migrate/fumadocs/meta.ts +244 -0
  291. package/src/migrate/migrate.ts +53 -0
  292. package/src/migrate/mintlify/config.ts +1040 -0
  293. package/src/migrate/mintlify/content.ts +98 -0
  294. package/src/migrate/mintlify/frontmatter.ts +126 -0
  295. package/src/migrate/mintlify/i18n.ts +51 -0
  296. package/src/migrate/mintlify/icons.ts +128 -0
  297. package/src/migrate/mintlify/index.ts +266 -0
  298. package/src/migrate/mintlify/snippets.ts +306 -0
  299. package/src/migrate/mintlify/transform.ts +80 -0
  300. package/src/migrate/nextra/content.ts +46 -0
  301. package/src/migrate/nextra/frontmatter.ts +40 -0
  302. package/src/migrate/nextra/index.ts +374 -0
  303. package/src/migrate/nextra/meta.ts +266 -0
  304. package/src/migrate/shared.ts +720 -0
  305. package/src/migrate/starlight/config.ts +459 -0
  306. package/src/migrate/starlight/content.ts +78 -0
  307. package/src/migrate/starlight/frontmatter.ts +111 -0
  308. package/src/migrate/starlight/i18n.ts +54 -0
  309. package/src/migrate/starlight/index.ts +131 -0
  310. package/src/og/card.ts +92 -0
  311. package/src/og/index.ts +2 -0
  312. package/src/openapi/scalar.ts +246 -0
  313. package/src/registry/eject.ts +310 -0
  314. package/src/registry/registry.ts +100 -0
  315. package/src/registry/rewrite-imports.ts +39 -0
  316. package/src/runtime/index.ts +14 -0
  317. package/src/search/build.ts +23 -0
  318. package/src/search/documents.ts +164 -0
  319. package/src/search/orama-index.ts +66 -0
  320. package/src/search/providers.ts +91 -0
  321. package/src/search/sync/algolia.ts +30 -0
  322. package/src/search/sync/index.ts +50 -0
  323. package/src/search/sync/orama-cloud.ts +40 -0
  324. package/src/search/sync/typesense.ts +65 -0
  325. package/src/seo/jsonld.ts +113 -0
  326. package/src/theme/entry.ts +637 -0
  327. package/src/theme/fonts.ts +198 -0
  328. package/src/theme/icons.ts +184 -0
  329. package/src/theme/palette.ts +143 -0
  330. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,365 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile as readFileFromDisk } from "node:fs/promises";
3
+
4
+ import { dirname, resolve } from "pathe";
5
+
6
+ import matter from "../../core/frontmatter.ts";
7
+ import { findOpenTagEnd, renameTag, rewriteCallouts } from "../shared.ts";
8
+
9
+ /**
10
+ * Source-to-source rewrites that turn Fumadocs-only MDX into idiomatic Blume
11
+ * markup. Runs once at migration time — no Fumadocs-aware plugins remain in the
12
+ * Blume runtime.
13
+ */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Imports
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const FUMADOCS_IMPORT =
20
+ /^import\s+[\s\S]*?\s+from\s+["']fumadocs-(?:ui|core|mdx)(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
21
+
22
+ /**
23
+ * Drop `import … from "fumadocs-ui/…"` (and `-core`/`-mdx`) statements. Blume
24
+ * injects its components globally, so these imports would fail to resolve once
25
+ * the Fumadocs packages are gone.
26
+ */
27
+ export const stripFumadocsImports = (source: string): string => {
28
+ const stripped = source.replace(FUMADOCS_IMPORT, "");
29
+ // Collapse the blank gap a removed import block leaves behind.
30
+ return stripped === source ? source : stripped.replaceAll(/\n{3,}/gu, "\n\n");
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Callouts
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Fumadocs `<Callout type="X">` values mapped to Blume directive names. */
38
+ const CALLOUT_TYPE_DIRECTIVES: Record<string, string> = {
39
+ error: "danger",
40
+ info: "info",
41
+ note: "note",
42
+ success: "success",
43
+ tip: "tip",
44
+ warn: "warning",
45
+ warning: "warning",
46
+ };
47
+
48
+ /**
49
+ * Convert Fumadocs `<Callout type="info|warn|error|…" title="…">` components
50
+ * into Blume `:::` directives. A bare `<Callout>` becomes `:::note`; the `icon`
51
+ * prop is dropped.
52
+ */
53
+ export const rewriteFumadocsCallouts = (source: string): string =>
54
+ rewriteCallouts(source, {
55
+ defaultDirective: "note",
56
+ tagDirectives: {},
57
+ tags: ["Callout"],
58
+ typeDirectives: CALLOUT_TYPE_DIRECTIVES,
59
+ });
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Container component renames
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Rename Fumadocs container components to their Blume equivalents. The
67
+ * lookahead in `renameTag` means a longer tag (`Accordions`, `Files`) is never
68
+ * matched by the rule for its shorter prefix (`Accordion`, `File`), so the
69
+ * item-level renames run first and the container renames second:
70
+ *
71
+ * - `<Cards>` -> `<CardGroup>` (items stay `<Card>`).
72
+ * - `<Accordions>`/`<Accordion>` -> `<Accordion>`/`<AccordionItem>`.
73
+ * - `<Files>`/`<Folder>`/`<File>` -> `<FileTree>`/`<TreeFolder>`/`<TreeFile>`.
74
+ */
75
+ export const rewriteFumadocsContainers = (source: string): string => {
76
+ let out = renameTag(source, "Cards", "CardGroup");
77
+ out = renameTag(out, "Accordion", "AccordionItem");
78
+ out = renameTag(out, "Accordions", "Accordion");
79
+ out = renameTag(out, "File", "TreeFile");
80
+ out = renameTag(out, "Folder", "TreeFolder");
81
+ out = renameTag(out, "Files", "FileTree");
82
+ return out;
83
+ };
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Tabs (items + value -> per-Tab title)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ const TABS_TAG = /<Tabs(?=[\s/>])/u;
90
+ const TAB_TAG = /<Tab(?=[\s/>])/u;
91
+ const ITEM_LITERAL = /'(?<s>[^']*)'|"(?<d>[^"]*)"|`(?<t>[^`]*)`/gu;
92
+
93
+ const escapeAttribute = (value: string): string =>
94
+ value.replaceAll('"', "&quot;");
95
+
96
+ /** Find the close brace matching the `{` at `open`, or -1 if unterminated. */
97
+ const matchBrace = (source: string, open: number): number => {
98
+ let depth = 0;
99
+ for (let index = open; index < source.length; index += 1) {
100
+ if (source[index] === "{") {
101
+ depth += 1;
102
+ } else if (source[index] === "}") {
103
+ depth -= 1;
104
+ if (depth === 0) {
105
+ return index;
106
+ }
107
+ }
108
+ }
109
+ return -1;
110
+ };
111
+
112
+ /** Span of an `items={…}` expression within a tag's attributes, or null. */
113
+ const itemsSpan = (attrs: string): { end: number; start: number } | null => {
114
+ const match = /\bitems\s*=\s*/u.exec(attrs);
115
+ if (!match) {
116
+ return null;
117
+ }
118
+ const braceStart = attrs.indexOf("{", match.index + match[0].length);
119
+ if (braceStart === -1) {
120
+ return null;
121
+ }
122
+ const braceEnd = matchBrace(attrs, braceStart);
123
+ return braceEnd === -1 ? null : { end: braceEnd, start: match.index };
124
+ };
125
+
126
+ /** Read the string literals from a Fumadocs `<Tabs items={[…]}>` attribute. */
127
+ const parseItems = (attrs: string): string[] => {
128
+ const span = itemsSpan(attrs);
129
+ if (!span) {
130
+ return [];
131
+ }
132
+ const inner = attrs.slice(span.start, span.end + 1);
133
+ const items: string[] = [];
134
+ for (const match of inner.matchAll(ITEM_LITERAL)) {
135
+ items.push(match.groups?.s ?? match.groups?.d ?? match.groups?.t ?? "");
136
+ }
137
+ return items;
138
+ };
139
+
140
+ /** Rebuild a `<Tabs …>` open tag with the `items={…}` attribute removed. */
141
+ const buildTabsOpen = (attrs: string, selfClosing: boolean): string => {
142
+ const span = itemsSpan(attrs);
143
+ const withoutItems = span
144
+ ? attrs.slice(0, span.start) + attrs.slice(span.end + 1)
145
+ : attrs;
146
+ const body = withoutItems.replace(/\/\s*$/u, "").trim();
147
+ const space = body ? ` ${body}` : "";
148
+ return selfClosing ? `<Tabs${space} />` : `<Tabs${space}>`;
149
+ };
150
+
151
+ /** Give a single `<Tab …>` open tag a `title`, preferring its own `value`. */
152
+ const titleizeTab = (tag: string, fallback: string | undefined): string => {
153
+ if (/\btitle\s*=/u.test(tag)) {
154
+ return tag;
155
+ }
156
+ if (/\bvalue\s*=/u.test(tag)) {
157
+ return tag.replace(/\bvalue(?<eq>\s*=)/u, "title$<eq>");
158
+ }
159
+ if (fallback === undefined) {
160
+ return tag;
161
+ }
162
+ return tag.replace(/^<Tab/u, `<Tab title="${escapeAttribute(fallback)}"`);
163
+ };
164
+
165
+ /**
166
+ * Find the `</name>` that closes the tag opened at `from` (an index just past
167
+ * the open tag's `>`), honoring nesting. Returns its start index, or -1.
168
+ */
169
+ const findMatchingClose = (
170
+ source: string,
171
+ from: number,
172
+ name: string
173
+ ): number => {
174
+ const token = new RegExp(`<${name}(?=[\\s/>])|</${name}>`, "gu");
175
+ token.lastIndex = from;
176
+ let depth = 1;
177
+ for (let match = token.exec(source); match; match = token.exec(source)) {
178
+ if (match[0].startsWith("</")) {
179
+ depth -= 1;
180
+ if (depth === 0) {
181
+ return match.index;
182
+ }
183
+ } else {
184
+ depth += 1;
185
+ }
186
+ }
187
+ return -1;
188
+ };
189
+
190
+ /** Title the direct `<Tab>` children of a Tabs block, skipping nested Tabs. */
191
+ const titleizeTabChildren = (inner: string, items: string[]): string => {
192
+ let output = "";
193
+ let cursor = 0;
194
+ let position = 0;
195
+
196
+ while (cursor < inner.length) {
197
+ const rest = inner.slice(cursor);
198
+ const tab = TAB_TAG.exec(rest);
199
+ if (!tab) {
200
+ output += rest;
201
+ break;
202
+ }
203
+ const nested = TABS_TAG.exec(rest);
204
+ if (nested && nested.index < tab.index) {
205
+ const start = cursor + nested.index;
206
+ const openEnd = findOpenTagEnd(inner, start + "<Tabs".length);
207
+ const close =
208
+ openEnd === -1 ? -1 : findMatchingClose(inner, openEnd + 1, "Tabs");
209
+ if (close === -1) {
210
+ output += inner.slice(cursor);
211
+ break;
212
+ }
213
+ const end = close + "</Tabs>".length;
214
+ output += inner.slice(cursor, end);
215
+ cursor = end;
216
+ continue;
217
+ }
218
+ const start = cursor + tab.index;
219
+ const openEnd = findOpenTagEnd(inner, start + "<Tab".length);
220
+ if (openEnd === -1) {
221
+ output += inner.slice(cursor, start + 1);
222
+ cursor = start + 1;
223
+ continue;
224
+ }
225
+ const tagText = inner.slice(start, openEnd + 1);
226
+ output +=
227
+ inner.slice(cursor, start) + titleizeTab(tagText, items[position]);
228
+ position += 1;
229
+ cursor = openEnd + 1;
230
+ }
231
+
232
+ return output;
233
+ };
234
+
235
+ /**
236
+ * Reshape Fumadocs tabs to Blume's API. Fumadocs declares tab labels on the
237
+ * parent (`<Tabs items={['npm','pnpm']}>`) and selects with `<Tab value="npm">`;
238
+ * Blume's `<Tab>` carries its own `title`. Each tab is titled from its `value`
239
+ * (or the positional `items` entry) and the `items` prop is stripped. Nested
240
+ * tab groups are handled recursively with their own `items`.
241
+ */
242
+ export const rewriteFumadocsTabs = (source: string): string => {
243
+ let output = "";
244
+ let cursor = 0;
245
+
246
+ while (cursor < source.length) {
247
+ const match = TABS_TAG.exec(source.slice(cursor));
248
+ if (!match) {
249
+ output += source.slice(cursor);
250
+ break;
251
+ }
252
+ const start = cursor + match.index;
253
+ const openEnd = findOpenTagEnd(source, start + "<Tabs".length);
254
+ if (openEnd === -1) {
255
+ output += source.slice(cursor, start + 1);
256
+ cursor = start + 1;
257
+ continue;
258
+ }
259
+ const attrs = source.slice(start + "<Tabs".length, openEnd);
260
+ const selfClosing = attrs.trimEnd().endsWith("/");
261
+ output += source.slice(cursor, start);
262
+
263
+ if (selfClosing) {
264
+ output += buildTabsOpen(attrs, true);
265
+ cursor = openEnd + 1;
266
+ continue;
267
+ }
268
+
269
+ const close = findMatchingClose(source, openEnd + 1, "Tabs");
270
+ if (close === -1) {
271
+ output += source.slice(start, openEnd + 1);
272
+ cursor = openEnd + 1;
273
+ continue;
274
+ }
275
+ const inner = rewriteFumadocsTabs(source.slice(openEnd + 1, close));
276
+ output += buildTabsOpen(attrs, false);
277
+ output += titleizeTabChildren(inner, parseItems(attrs));
278
+ output += "</Tabs>";
279
+ cursor = close + "</Tabs>".length;
280
+ }
281
+
282
+ return output;
283
+ };
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Includes
287
+ // ---------------------------------------------------------------------------
288
+
289
+ const INCLUDE = /<include\b[^>]*>(?<path>[\s\S]*?)<\/include>/gu;
290
+
291
+ interface IncludeOptions {
292
+ filePath: string;
293
+ readFile?: (file: string) => Promise<string>;
294
+ seen?: Set<string>;
295
+ }
296
+
297
+ /**
298
+ * Inline Fumadocs `<include>./partial.mdx</include>` references — source-level
299
+ * includes with no Blume runtime equivalent. The referenced file's frontmatter
300
+ * is stripped, nested includes are resolved (with a cycle guard), and the path
301
+ * is resolved relative to the including file. Missing or circular targets are
302
+ * left untouched and reported.
303
+ */
304
+ export const inlineFumadocsIncludes = async (
305
+ source: string,
306
+ options: IncludeOptions
307
+ ): Promise<{ content: string; warnings: string[] }> => {
308
+ const matches = [...source.matchAll(INCLUDE)];
309
+ if (matches.length === 0) {
310
+ return { content: source, warnings: [] };
311
+ }
312
+
313
+ const read = options.readFile ?? ((file) => readFileFromDisk(file, "utf-8"));
314
+ const seen = options.seen ?? new Set<string>();
315
+ const warnings: string[] = [];
316
+ let content = source;
317
+
318
+ for (const match of matches) {
319
+ const rawPath = match.groups?.path?.trim() ?? "";
320
+ if (!rawPath) {
321
+ continue;
322
+ }
323
+ const target = resolve(dirname(options.filePath), rawPath);
324
+ if (seen.has(target)) {
325
+ warnings.push(`Circular <include> "${rawPath}" — left as-is.`);
326
+ continue;
327
+ }
328
+ if (!existsSync(target)) {
329
+ warnings.push(`<include> target "${rawPath}" not found — left as-is.`);
330
+ continue;
331
+ }
332
+ seen.add(target);
333
+ // oxlint-disable-next-line no-await-in-loop -- sequential include reads
334
+ const raw = await read(target);
335
+ // oxlint-disable-next-line no-await-in-loop -- sequential include reads
336
+ const nested = await inlineFumadocsIncludes(matter(raw).content.trim(), {
337
+ ...options,
338
+ filePath: target,
339
+ seen,
340
+ });
341
+ seen.delete(target);
342
+ warnings.push(...nested.warnings);
343
+ content = content.replace(match[0], () => nested.content);
344
+ }
345
+
346
+ return { content, warnings };
347
+ };
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Unsupported components
351
+ // ---------------------------------------------------------------------------
352
+
353
+ /** Fumadocs components with no drop-in Blume equivalent — flagged for review. */
354
+ const UNSUPPORTED_COMPONENTS = [
355
+ "Banner",
356
+ "DynamicCodeBlock",
357
+ "ImageZoom",
358
+ "InlineTOC",
359
+ ];
360
+
361
+ /** Names of Fumadocs components in `source` that need manual attention. */
362
+ export const unsupportedFumadocsComponents = (source: string): string[] =>
363
+ UNSUPPORTED_COMPONENTS.filter((name) =>
364
+ new RegExp(`<${name}\\b`, "u").test(source)
365
+ );
@@ -0,0 +1,18 @@
1
+ import { stripUnknownPageMeta } from "../shared.ts";
2
+
3
+ /**
4
+ * Normalize Fumadocs page frontmatter for Blume. Fumadocs' core fields
5
+ * (`title`, `description`, `icon`) already validate against Blume's page schema
6
+ * and pass through untouched; Fumadocs-only keys with no equivalent — most
7
+ * notably `full` (full-width, no-TOC layout) — are dropped by the strict-schema
8
+ * strip and reported so the author can re-apply them by hand.
9
+ */
10
+ export const normalizeFumadocsPageMeta = (
11
+ value: unknown
12
+ ): { data: Record<string, unknown>; removed: string[] } => {
13
+ const data =
14
+ value && typeof value === "object" && !Array.isArray(value)
15
+ ? { ...(value as Record<string, unknown>) }
16
+ : {};
17
+ return stripUnknownPageMeta(data);
18
+ };
@@ -0,0 +1,230 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { mkdir, rename, writeFile } from "node:fs/promises";
3
+
4
+ import { basename, join } from "pathe";
5
+
6
+ import { renderMetaModule } from "./meta.ts";
7
+ import type {
8
+ FumadocsPageItem,
9
+ FumadocsPagesStructure,
10
+ FumadocsSection,
11
+ } from "./meta.ts";
12
+
13
+ /**
14
+ * Rebuild Fumadocs `---Section---` separators as Blume group folders.
15
+ *
16
+ * Fumadocs draws a non-collapsible section heading from a `"---Label---"` entry
17
+ * in a folder's `pages` array — exactly what a Blume `(Label)/` group folder
18
+ * renders by default (`display: "flat"`). Blume has no flat-file "separator"
19
+ * primitive, so the only faithful, non-overriding home for a section is a group
20
+ * folder: it is route-transparent (the `(Label)` segment is stripped from URLs,
21
+ * see `core/sources/normalize.ts`) and composes with per-folder `meta.ts`,
22
+ * unlike a global `navigation.sidebar` override (which would disable every other
23
+ * folder's meta site-wide).
24
+ *
25
+ * The reshape runs against the already-migrated `docs/` tree, so each `pages`
26
+ * entry is resolved to a real page file or folder before it is moved. A section
27
+ * that is a single folder is left in place (wrapping it would double the
28
+ * heading); links have no file to move and are dropped with a warning.
29
+ */
30
+
31
+ const PAGE_EXTS = [".mdx", ".md"];
32
+ const WORD_SPLIT = /[-_]/u;
33
+ const PATH_SEP = /[/\\]/u;
34
+
35
+ interface ResolvedEntry {
36
+ kind: "file" | "folder";
37
+ path: string;
38
+ }
39
+
40
+ const isDirectory = (path: string): boolean => {
41
+ try {
42
+ return statSync(path).isDirectory();
43
+ } catch {
44
+ return false;
45
+ }
46
+ };
47
+
48
+ /** Resolve a `pages` name to its on-disk page file or folder under `docsDir`. */
49
+ const resolveEntry = (docsDir: string, name: string): ResolvedEntry | null => {
50
+ for (const ext of PAGE_EXTS) {
51
+ const file = join(docsDir, `${name}${ext}`);
52
+ if (existsSync(file)) {
53
+ return { kind: "file", path: file };
54
+ }
55
+ }
56
+ const folder = join(docsDir, name);
57
+ return isDirectory(folder) ? { kind: "folder", path: folder } : null;
58
+ };
59
+
60
+ const humanize = (name: string): string =>
61
+ name
62
+ .split(WORD_SPLIT)
63
+ .filter(Boolean)
64
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
65
+ .join(" ");
66
+
67
+ const linkWarning = (item: { href: string; text: string }): string =>
68
+ `Dropped sidebar link "${item.text}" (${item.href}) — add it to navbar.links manually.`;
69
+
70
+ const extractWarning = (name: string): string =>
71
+ `Sidebar extract "...${name}" became a normal "${name}" group (its pages are not flattened into the parent).`;
72
+
73
+ export interface GroupReshapeResult {
74
+ /** Ordering keys for the parent folder's `meta.ts` `pages`. */
75
+ order: string[];
76
+ warnings: string[];
77
+ }
78
+
79
+ /** Record an in-place item's ordering key (lead items, or an ungroupable run). */
80
+ const addUngroupedItem = (
81
+ item: FumadocsPageItem,
82
+ order: string[],
83
+ warnings: string[]
84
+ ): void => {
85
+ if (item.kind === "link") {
86
+ warnings.push(linkWarning(item));
87
+ return;
88
+ }
89
+ if (item.kind === "extract") {
90
+ warnings.push(extractWarning(item.name));
91
+ }
92
+ order.push(item.name);
93
+ };
94
+
95
+ /** A section of one folder keeps its place without a wrapping group folder. */
96
+ const reshapeSingleFolderSection = (
97
+ section: FumadocsSection,
98
+ only: FumadocsPageItem & { kind: "extract" | "ref" },
99
+ order: string[],
100
+ warnings: string[]
101
+ ): void => {
102
+ order.push(only.name);
103
+ if (humanize(only.name).toLowerCase() !== section.label.toLowerCase()) {
104
+ warnings.push(
105
+ `Section "${section.label}" wraps folder "${only.name}"; set that folder's meta title to "${section.label}" to make the heading match.`
106
+ );
107
+ }
108
+ if (only.kind === "extract") {
109
+ warnings.push(extractWarning(only.name));
110
+ }
111
+ };
112
+
113
+ /** Move one section item into the group folder; return its ordering key. */
114
+ const moveItemIntoGroup = async (
115
+ item: FumadocsPageItem,
116
+ docsDir: string,
117
+ groupDir: string,
118
+ label: string,
119
+ warnings: string[]
120
+ ): Promise<string | null> => {
121
+ if (item.kind === "link") {
122
+ warnings.push(linkWarning(item));
123
+ return null;
124
+ }
125
+ const resolved = resolveEntry(docsDir, item.name);
126
+ if (!resolved) {
127
+ warnings.push(
128
+ `Sidebar entry "${item.name}" in section "${label}" matched no page or folder; skipped.`
129
+ );
130
+ return null;
131
+ }
132
+ const dest = join(groupDir, basename(resolved.path));
133
+ if (existsSync(dest)) {
134
+ warnings.push(
135
+ `Skipped moving "${item.name}" into section "${label}" (target already exists).`
136
+ );
137
+ return null;
138
+ }
139
+ await mkdir(groupDir, { recursive: true });
140
+ await rename(resolved.path, dest);
141
+ if (item.kind === "extract") {
142
+ warnings.push(extractWarning(item.name));
143
+ }
144
+ return item.name;
145
+ };
146
+
147
+ const reshapeSection = async (
148
+ section: FumadocsSection,
149
+ docsDir: string,
150
+ order: string[],
151
+ warnings: string[]
152
+ ): Promise<void> => {
153
+ const movable = section.items.filter((item) => item.kind !== "link");
154
+
155
+ // A lone folder already renders as its own group; wrapping it in a `(Label)/`
156
+ // folder would stack two headings, so leave it in place.
157
+ if (movable.length === 1) {
158
+ const [only] = movable as [FumadocsPageItem & { kind: "extract" | "ref" }];
159
+ if (resolveEntry(docsDir, only.name)?.kind === "folder") {
160
+ reshapeSingleFolderSection(section, only, order, warnings);
161
+ return;
162
+ }
163
+ }
164
+
165
+ if (PATH_SEP.test(section.label)) {
166
+ warnings.push(
167
+ `Section "${section.label}" has a slash in its name and can't become a group folder; its pages stay ungrouped.`
168
+ );
169
+ for (const item of section.items) {
170
+ addUngroupedItem(item, order, warnings);
171
+ }
172
+ return;
173
+ }
174
+
175
+ const groupDir = join(docsDir, `(${section.label})`);
176
+ const sectionKeys: string[] = [];
177
+ for (const item of section.items) {
178
+ // oxlint-disable-next-line no-await-in-loop -- sequential moves into one group
179
+ const key = await moveItemIntoGroup(
180
+ item,
181
+ docsDir,
182
+ groupDir,
183
+ section.label,
184
+ warnings
185
+ );
186
+ if (key) {
187
+ sectionKeys.push(key);
188
+ }
189
+ }
190
+
191
+ // Nothing movable (e.g. a link-only section) — drop the empty heading.
192
+ if (sectionKeys.length === 0) {
193
+ return;
194
+ }
195
+
196
+ // The group folder's key in the nav is its label, so order it by label.
197
+ order.push(section.label);
198
+ // Preserve the authored order of the section's pages.
199
+ if (sectionKeys.length > 1) {
200
+ await writeFile(
201
+ join(groupDir, "meta.ts"),
202
+ renderMetaModule({ pages: sectionKeys }),
203
+ "utf-8"
204
+ );
205
+ }
206
+ };
207
+
208
+ /**
209
+ * Reshape a parsed `pages` structure into group folders under `docsDir`,
210
+ * returning the parent folder's `meta.ts` page ordering and any warnings. Items
211
+ * are moved within the already-migrated `docs/` tree, so this must run after the
212
+ * pages have been moved out of `content/docs`.
213
+ */
214
+ export const reshapeFumadocsGroups = async (
215
+ structure: FumadocsPagesStructure,
216
+ docsDir: string
217
+ ): Promise<GroupReshapeResult> => {
218
+ const order: string[] = [];
219
+ const warnings: string[] = [];
220
+
221
+ for (const item of structure.lead) {
222
+ addUngroupedItem(item, order, warnings);
223
+ }
224
+ for (const section of structure.sections) {
225
+ // oxlint-disable-next-line no-await-in-loop -- sections share `docsDir` state
226
+ await reshapeSection(section, docsDir, order, warnings);
227
+ }
228
+
229
+ return { order, warnings };
230
+ };