blume 0.0.0 → 0.1.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/bin/blume.mjs +18 -0
- package/dist/cli/index.js +11989 -0
- package/dist/cli/index.js.map +141 -0
- package/docs/01-quickstart.mdx +99 -0
- package/docs/02-deployment.mdx +129 -0
- package/docs/advanced/api-reference.mdx +114 -0
- package/docs/advanced/blog.mdx +121 -0
- package/docs/advanced/changelog.mdx +113 -0
- package/docs/advanced/custom-pages.mdx +187 -0
- package/docs/advanced/meta.ts +7 -0
- package/docs/changelog/v0-1-0.mdx +12 -0
- package/docs/changelog/v0-2-0.mdx +16 -0
- package/docs/configuration/ai.mdx +228 -0
- package/docs/configuration/analytics.mdx +98 -0
- package/docs/configuration/customization.mdx +91 -0
- package/docs/configuration/export.mdx +70 -0
- package/docs/configuration/index.mdx +271 -0
- package/docs/configuration/meta.ts +15 -0
- package/docs/configuration/search.mdx +172 -0
- package/docs/configuration/seo.mdx +196 -0
- package/docs/configuration/theming.mdx +178 -0
- package/docs/content/components.mdx +565 -0
- package/docs/content/i18n.mdx +205 -0
- package/docs/content/index.mdx +161 -0
- package/docs/content/islands.mdx +94 -0
- package/docs/content/meta.mdx +119 -0
- package/docs/content/meta.ts +15 -0
- package/docs/content/navigation.mdx +168 -0
- package/docs/content/sources.mdx +216 -0
- package/docs/content/syntax.mdx +445 -0
- package/docs/index.mdx +112 -0
- package/docs/reference/cli.mdx +43 -0
- package/docs/reference/frontmatter.mdx +74 -0
- package/docs/reference/meta.ts +7 -0
- package/package.json +140 -6
- package/src/ai/ask.ts +93 -0
- package/src/ai/llms.ts +65 -0
- package/src/ai/markdown.ts +31 -0
- package/src/ai/mcp/data.ts +74 -0
- package/src/ai/mcp/discovery.ts +49 -0
- package/src/ai/mcp/server.ts +225 -0
- package/src/ai/mcp/tools.ts +47 -0
- package/src/assets/icon.png +0 -0
- package/src/astro/generate.ts +878 -0
- package/src/astro/index.ts +4 -0
- package/src/astro/integration.ts +74 -0
- package/src/astro/islands.ts +131 -0
- package/src/astro/markdown-negotiation.ts +68 -0
- package/src/astro/pages.ts +28 -0
- package/src/astro/templates.ts +1199 -0
- package/src/cli/commands/add.ts +81 -0
- package/src/cli/commands/build.ts +103 -0
- package/src/cli/commands/dev.ts +108 -0
- package/src/cli/commands/doctor.ts +74 -0
- package/src/cli/commands/eject.ts +57 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +39 -0
- package/src/cli/commands/preview.ts +39 -0
- package/src/cli/commands/sync.ts +52 -0
- package/src/cli/commands/validate.ts +60 -0
- package/src/cli/index.ts +35 -0
- package/src/cli/log.ts +37 -0
- package/src/cli/prepare.ts +80 -0
- package/src/components/Icon.astro +99 -0
- package/src/components/content/Accordion.astro +8 -0
- package/src/components/content/AccordionItem.astro +121 -0
- package/src/components/content/AutoTypeTable.astro +51 -0
- package/src/components/content/Badge.astro +124 -0
- package/src/components/content/Callout.astro +73 -0
- package/src/components/content/Card.astro +104 -0
- package/src/components/content/CardGroup.astro +14 -0
- package/src/components/content/CodeGroup.astro +13 -0
- package/src/components/content/Color.astro +15 -0
- package/src/components/content/ColorItem.astro +87 -0
- package/src/components/content/ColorRow.astro +10 -0
- package/src/components/content/Column.astro +6 -0
- package/src/components/content/Columns.astro +9 -0
- package/src/components/content/Expandable.astro +11 -0
- package/src/components/content/FileTree.astro +8 -0
- package/src/components/content/Frame.astro +70 -0
- package/src/components/content/GithubInfo.astro +110 -0
- package/src/components/content/Math.astro +24 -0
- package/src/components/content/Panel.astro +20 -0
- package/src/components/content/Prompt.astro +129 -0
- package/src/components/content/Step.astro +34 -0
- package/src/components/content/Steps.astro +20 -0
- package/src/components/content/Tab.astro +40 -0
- package/src/components/content/Tabs.astro +273 -0
- package/src/components/content/Tile.astro +42 -0
- package/src/components/content/Tooltip.astro +68 -0
- package/src/components/content/Tree.astro +300 -0
- package/src/components/content/TreeFile.astro +15 -0
- package/src/components/content/TreeFolder.astro +62 -0
- package/src/components/content/TypeTable.astro +106 -0
- package/src/components/content/Update.astro +66 -0
- package/src/components/content/Visibility.astro +12 -0
- package/src/components/content/Warning.astro +9 -0
- package/src/components/content/auto-type-table.ts +141 -0
- package/src/components/content/github-info.ts +79 -0
- package/src/components/content/mermaid-element.ts +68 -0
- package/src/components/github-mark.ts +9 -0
- package/src/components/index.ts +14 -0
- package/src/components/islands/AskAI.astro +12 -0
- package/src/components/islands/ask-ai.tsx +156 -0
- package/src/components/layout/Analytics.astro +63 -0
- package/src/components/layout/Banner.astro +50 -0
- package/src/components/layout/Breadcrumbs.astro +31 -0
- package/src/components/layout/Favicon.astro +15 -0
- package/src/components/layout/Fonts.astro +14 -0
- package/src/components/layout/Header.astro +188 -0
- package/src/components/layout/LanguageSwitcher.astro +56 -0
- package/src/components/layout/NavTree.astro +462 -0
- package/src/components/layout/PageActions.astro +438 -0
- package/src/components/layout/PageFeedback.astro +58 -0
- package/src/components/layout/Pagination.astro +56 -0
- package/src/components/layout/ReferenceLayout.astro +102 -0
- package/src/components/layout/RootLayout.astro +533 -0
- package/src/components/layout/Search.astro +608 -0
- package/src/components/layout/TableOfContents.astro +68 -0
- package/src/components/layout/analytics-client.ts +38 -0
- package/src/components/layout/nav-utils.ts +87 -0
- package/src/components/layout/overrides.ts +32 -0
- package/src/components/layout/search/algolia.ts +43 -0
- package/src/components/layout/search/endpoint.ts +22 -0
- package/src/components/layout/search/flexsearch.ts +52 -0
- package/src/components/layout/search/orama-cloud.ts +41 -0
- package/src/components/layout/search/orama.ts +26 -0
- package/src/components/layout/search/pagefind.ts +43 -0
- package/src/components/layout/search/types.ts +163 -0
- package/src/components/layout/search/typesense.ts +60 -0
- package/src/components/layout/toc-element.ts +108 -0
- package/src/core/bridge.ts +92 -0
- package/src/core/config.ts +112 -0
- package/src/core/content.ts +50 -0
- package/src/core/define-components.ts +34 -0
- package/src/core/define-meta.ts +20 -0
- package/src/core/deployment-env.ts +73 -0
- package/src/core/diagnostics.ts +104 -0
- package/src/core/graph.ts +128 -0
- package/src/core/i18n-ui.ts +171 -0
- package/src/core/i18n.ts +169 -0
- package/src/core/last-modified.ts +88 -0
- package/src/core/links.ts +336 -0
- package/src/core/load-module.ts +15 -0
- package/src/core/manifest.ts +126 -0
- package/src/core/meta.ts +97 -0
- package/src/core/navigation.ts +392 -0
- package/src/core/package-root.ts +37 -0
- package/src/core/project-graph.ts +153 -0
- package/src/core/project.ts +56 -0
- package/src/core/schema.ts +1057 -0
- package/src/core/server-features.ts +23 -0
- package/src/core/sources/assets.ts +77 -0
- package/src/core/sources/cache.ts +122 -0
- package/src/core/sources/filesystem.ts +99 -0
- package/src/core/sources/mdx-remote.ts +216 -0
- package/src/core/sources/mintlify.ts +161 -0
- package/src/core/sources/normalize.ts +227 -0
- package/src/core/sources/notion.ts +440 -0
- package/src/core/sources/portable-text.ts +143 -0
- package/src/core/sources/read.ts +36 -0
- package/src/core/sources/resolve.ts +158 -0
- package/src/core/sources/sanity.ts +218 -0
- package/src/core/sources/types.ts +105 -0
- package/src/core/types.ts +261 -0
- package/src/core/ui-packs/ar.ts +47 -0
- package/src/core/ui-packs/bg.ts +47 -0
- package/src/core/ui-packs/bn.ts +47 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +47 -0
- package/src/core/ui-packs/da.ts +47 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +47 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +47 -0
- package/src/core/ui-packs/fi.ts +47 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +47 -0
- package/src/core/ui-packs/hi.ts +47 -0
- package/src/core/ui-packs/hr.ts +47 -0
- package/src/core/ui-packs/hu.ts +47 -0
- package/src/core/ui-packs/id.ts +47 -0
- package/src/core/ui-packs/index.ts +87 -0
- package/src/core/ui-packs/it.ts +47 -0
- package/src/core/ui-packs/ja.ts +47 -0
- package/src/core/ui-packs/ko.ts +47 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +47 -0
- package/src/core/ui-packs/pl.ts +47 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +47 -0
- package/src/core/ui-packs/ro.ts +47 -0
- package/src/core/ui-packs/ru.ts +47 -0
- package/src/core/ui-packs/sk.ts +47 -0
- package/src/core/ui-packs/sr.ts +47 -0
- package/src/core/ui-packs/sv.ts +47 -0
- package/src/core/ui-packs/th.ts +47 -0
- package/src/core/ui-packs/tr.ts +47 -0
- package/src/core/ui-packs/uk.ts +47 -0
- package/src/core/ui-packs/vi.ts +47 -0
- package/src/core/ui-packs/zh-tw.ts +47 -0
- package/src/core/ui-packs/zh.ts +47 -0
- package/src/core/version.ts +23 -0
- package/src/deploy/robots.ts +20 -0
- package/src/deploy/rss.ts +128 -0
- package/src/deploy/sitemap.ts +28 -0
- package/src/index.ts +27 -0
- package/src/markdown/code-title.ts +71 -0
- package/src/markdown/directives.ts +83 -0
- package/src/markdown/heading-anchors.ts +137 -0
- package/src/markdown/index.ts +159 -0
- package/src/markdown/inline-code.ts +108 -0
- package/src/markdown/language-icon.ts +172 -0
- package/src/markdown/math.ts +32 -0
- package/src/markdown/mdast.ts +48 -0
- package/src/markdown/mermaid.ts +37 -0
- package/src/markdown/package-commands.ts +159 -0
- package/src/markdown/package-install.ts +40 -0
- package/src/migrate/fumadocs/config.ts +106 -0
- package/src/migrate/fumadocs/content.ts +365 -0
- package/src/migrate/fumadocs/frontmatter.ts +18 -0
- package/src/migrate/fumadocs/index.ts +252 -0
- package/src/migrate/fumadocs/meta.ts +114 -0
- package/src/migrate/migrate.ts +53 -0
- package/src/migrate/mintlify/config.ts +1040 -0
- package/src/migrate/mintlify/content.ts +98 -0
- package/src/migrate/mintlify/frontmatter.ts +126 -0
- package/src/migrate/mintlify/i18n.ts +51 -0
- package/src/migrate/mintlify/icons.ts +128 -0
- package/src/migrate/mintlify/index.ts +266 -0
- package/src/migrate/mintlify/snippets.ts +305 -0
- package/src/migrate/mintlify/transform.ts +81 -0
- package/src/migrate/nextra/content.ts +46 -0
- package/src/migrate/nextra/frontmatter.ts +40 -0
- package/src/migrate/nextra/index.ts +374 -0
- package/src/migrate/nextra/meta.ts +266 -0
- package/src/migrate/shared.ts +623 -0
- package/src/migrate/starlight/config.ts +459 -0
- package/src/migrate/starlight/content.ts +78 -0
- package/src/migrate/starlight/frontmatter.ts +111 -0
- package/src/migrate/starlight/i18n.ts +54 -0
- package/src/migrate/starlight/index.ts +131 -0
- package/src/og/card.ts +92 -0
- package/src/og/index.ts +2 -0
- package/src/openapi/scalar.ts +246 -0
- package/src/registry/eject.ts +263 -0
- package/src/registry/registry.ts +100 -0
- package/src/registry/rewrite-imports.ts +39 -0
- package/src/runtime/index.ts +14 -0
- package/src/search/build.ts +23 -0
- package/src/search/documents.ts +165 -0
- package/src/search/orama-index.ts +66 -0
- package/src/search/providers.ts +91 -0
- package/src/search/sync/algolia.ts +30 -0
- package/src/search/sync/index.ts +50 -0
- package/src/search/sync/orama-cloud.ts +40 -0
- package/src/search/sync/typesense.ts +65 -0
- package/src/seo/jsonld.ts +113 -0
- package/src/theme/entry.ts +608 -0
- package/src/theme/fonts.ts +198 -0
- package/src/theme/icons.ts +184 -0
- package/src/theme/palette.ts +143 -0
- package/src/theme/twoslash.ts +81 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** Supported package managers, in the order tabs are displayed. */
|
|
2
|
+
export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"] as const;
|
|
3
|
+
|
|
4
|
+
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
5
|
+
|
|
6
|
+
/** Words that mark the input as an explicit command rather than a bare list. */
|
|
7
|
+
const MANAGER_PREFIXES = new Set(["bun", "bunx", "npm", "npx", "pnpm", "yarn"]);
|
|
8
|
+
|
|
9
|
+
const WHITESPACE = /\s+/u;
|
|
10
|
+
const WHITESPACE_RUN = /\s+/gu;
|
|
11
|
+
const GLOBAL_FLAGS = new Set(["-g", "--global"]);
|
|
12
|
+
|
|
13
|
+
type Operation = "add" | "create" | "exec" | "install" | "remove" | "run";
|
|
14
|
+
|
|
15
|
+
interface Intent {
|
|
16
|
+
args: string[];
|
|
17
|
+
operation: Operation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Map a package-manager subcommand to a normalized operation. */
|
|
21
|
+
const normalizeVerb = (verb: string): Operation | null => {
|
|
22
|
+
switch (verb) {
|
|
23
|
+
case "add":
|
|
24
|
+
case "i":
|
|
25
|
+
case "in":
|
|
26
|
+
case "install": {
|
|
27
|
+
return "add";
|
|
28
|
+
}
|
|
29
|
+
case "create":
|
|
30
|
+
case "init": {
|
|
31
|
+
return "create";
|
|
32
|
+
}
|
|
33
|
+
case "dlx":
|
|
34
|
+
case "exec":
|
|
35
|
+
case "x": {
|
|
36
|
+
return "exec";
|
|
37
|
+
}
|
|
38
|
+
case "remove":
|
|
39
|
+
case "rm":
|
|
40
|
+
case "un":
|
|
41
|
+
case "uninstall": {
|
|
42
|
+
return "remove";
|
|
43
|
+
}
|
|
44
|
+
case "run": {
|
|
45
|
+
return "run";
|
|
46
|
+
}
|
|
47
|
+
default: {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Normalize npm-style flags to forms every manager understands. */
|
|
54
|
+
const normalizeFlags = (args: string[]): string[] =>
|
|
55
|
+
args.flatMap((arg) => {
|
|
56
|
+
if (arg === "--save-dev") {
|
|
57
|
+
return ["-D"];
|
|
58
|
+
}
|
|
59
|
+
if (arg === "--save" || arg === "-S") {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return [arg];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse an input command (a bare package list like `react react-dom` or an
|
|
67
|
+
* explicit command like `npm i -D typescript` / `npx astro add`) into a
|
|
68
|
+
* manager-agnostic intent.
|
|
69
|
+
*/
|
|
70
|
+
const parseIntent = (input: string): Intent => {
|
|
71
|
+
const tokens = input.trim().split(WHITESPACE).filter(Boolean);
|
|
72
|
+
if (tokens.length === 0) {
|
|
73
|
+
return { args: [], operation: "install" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [first, ...rest] = tokens;
|
|
77
|
+
if (first === undefined) {
|
|
78
|
+
return { args: [], operation: "install" };
|
|
79
|
+
}
|
|
80
|
+
if (!MANAGER_PREFIXES.has(first)) {
|
|
81
|
+
return { args: normalizeFlags(tokens), operation: "add" };
|
|
82
|
+
}
|
|
83
|
+
if (first === "npx" || first === "bunx") {
|
|
84
|
+
return { args: rest, operation: "exec" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const [verb, ...verbArgs] = rest;
|
|
88
|
+
if (!verb) {
|
|
89
|
+
return { args: [], operation: "install" };
|
|
90
|
+
}
|
|
91
|
+
const operation = normalizeVerb(verb);
|
|
92
|
+
if (operation === null) {
|
|
93
|
+
// Unknown subcommand (e.g. `npm test`); run it as a script.
|
|
94
|
+
return { args: rest, operation: "run" };
|
|
95
|
+
}
|
|
96
|
+
if (operation === "add" && verbArgs.length === 0) {
|
|
97
|
+
return { args: [], operation: "install" };
|
|
98
|
+
}
|
|
99
|
+
return { args: normalizeFlags(verbArgs), operation };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Render one manager's command for the given intent. */
|
|
103
|
+
const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
104
|
+
const args = intent.args.join(" ");
|
|
105
|
+
switch (intent.operation) {
|
|
106
|
+
case "add": {
|
|
107
|
+
if (manager === "npm") {
|
|
108
|
+
return `npm install ${args}`;
|
|
109
|
+
}
|
|
110
|
+
if (manager === "yarn" && intent.args.some((a) => GLOBAL_FLAGS.has(a))) {
|
|
111
|
+
const pkgs = intent.args.filter((a) => !GLOBAL_FLAGS.has(a)).join(" ");
|
|
112
|
+
return `yarn global add ${pkgs}`;
|
|
113
|
+
}
|
|
114
|
+
return `${manager} add ${args}`;
|
|
115
|
+
}
|
|
116
|
+
case "create": {
|
|
117
|
+
return `${manager} create ${args}`;
|
|
118
|
+
}
|
|
119
|
+
case "exec": {
|
|
120
|
+
if (manager === "npm") {
|
|
121
|
+
return `npx ${args}`;
|
|
122
|
+
}
|
|
123
|
+
if (manager === "bun") {
|
|
124
|
+
return `bunx ${args}`;
|
|
125
|
+
}
|
|
126
|
+
return `${manager} dlx ${args}`;
|
|
127
|
+
}
|
|
128
|
+
case "remove": {
|
|
129
|
+
return manager === "npm"
|
|
130
|
+
? `npm uninstall ${args}`
|
|
131
|
+
: `${manager} remove ${args}`;
|
|
132
|
+
}
|
|
133
|
+
case "run": {
|
|
134
|
+
return `${manager} run ${args}`;
|
|
135
|
+
}
|
|
136
|
+
default: {
|
|
137
|
+
return `${manager} install`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert an install command into the equivalent for every supported package
|
|
144
|
+
* manager. Accepts a bare package list (`react`) or a full command
|
|
145
|
+
* (`npm i -D typescript`, `npx astro add react`).
|
|
146
|
+
*/
|
|
147
|
+
export const toPackageCommands = (
|
|
148
|
+
input: string
|
|
149
|
+
): Record<PackageManager, string> => {
|
|
150
|
+
const intent = parseIntent(input);
|
|
151
|
+
const normalize = (command: string): string =>
|
|
152
|
+
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
153
|
+
return {
|
|
154
|
+
bun: normalize(buildCommand("bun", intent)),
|
|
155
|
+
npm: normalize(buildCommand("npm", intent)),
|
|
156
|
+
pnpm: normalize(buildCommand("pnpm", intent)),
|
|
157
|
+
yarn: normalize(buildCommand("yarn", intent)),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { codeBlock, jsxAttribute, jsxFlowElement } from "./mdast.ts";
|
|
2
|
+
import type { MdastNode, MdastVisitorContext } from "./mdast.ts";
|
|
3
|
+
import { PACKAGE_MANAGERS, toPackageCommands } from "./package-commands.ts";
|
|
4
|
+
|
|
5
|
+
interface CodeNode extends MdastNode {
|
|
6
|
+
lang?: string | null;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Build an `<Tab title="...">` node wrapping a single highlighted command. */
|
|
11
|
+
const tabNode = (manager: string, command: string) =>
|
|
12
|
+
jsxFlowElement(
|
|
13
|
+
"Tab",
|
|
14
|
+
[jsxAttribute("title", manager)],
|
|
15
|
+
[codeBlock("bash", command)]
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Satteri MDAST plugin that turns a ` ```package-install ` code block into a
|
|
20
|
+
* `<Tabs>` group with one `<Tab>` per package manager, each holding the
|
|
21
|
+
* converted install command as a normal (highlighted) code block. Runs at the
|
|
22
|
+
* MDAST stage so the generated code blocks are highlighted like any other.
|
|
23
|
+
*/
|
|
24
|
+
export const packageInstallPlugin = () => ({
|
|
25
|
+
code(node: CodeNode, ctx: MdastVisitorContext) {
|
|
26
|
+
if (node.lang !== "package-install") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const commands = toPackageCommands(node.value);
|
|
30
|
+
ctx.replaceNode(
|
|
31
|
+
node,
|
|
32
|
+
jsxFlowElement(
|
|
33
|
+
"Tabs",
|
|
34
|
+
[],
|
|
35
|
+
PACKAGE_MANAGERS.map((manager) => tabNode(manager, commands[manager]))
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
name: "blume-package-install",
|
|
40
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { join } from "pathe";
|
|
5
|
+
|
|
6
|
+
import type { BlumeConfig } from "../../core/schema.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a `BlumeConfig` for a Fumadocs project. Fumadocs is code-first — its
|
|
10
|
+
* navigation comes from the folder tree plus `meta.json` files, not a JSON
|
|
11
|
+
* config — so there is little to translate. We take the title from
|
|
12
|
+
* `package.json` and scrape the `loader({ baseUrl })` route prefix from the
|
|
13
|
+
* source loader so migrated routes keep serving under `/<baseUrl>`. The source
|
|
14
|
+
* files are TypeScript, so we read by pattern rather than executing them.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Loader files that may hold `loader({ baseUrl: "/docs" })`. */
|
|
18
|
+
const SOURCE_FILES = [
|
|
19
|
+
"lib/source.ts",
|
|
20
|
+
"app/source.ts",
|
|
21
|
+
"src/lib/source.ts",
|
|
22
|
+
"src/app/source.ts",
|
|
23
|
+
"source.ts",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const BASE_URL = /baseUrl\s*:\s*['"`](?<base>[^'"`]+)['"`]/u;
|
|
27
|
+
|
|
28
|
+
/** Title-case a package name (drop any scope) into a readable doc title. */
|
|
29
|
+
const prettifyTitle = (name: string): string => {
|
|
30
|
+
const base = name.includes("/")
|
|
31
|
+
? name.slice(name.lastIndexOf("/") + 1)
|
|
32
|
+
: name;
|
|
33
|
+
const words = base.split(/[-_\s]+/u).filter(Boolean);
|
|
34
|
+
if (words.length === 0) {
|
|
35
|
+
return "Documentation";
|
|
36
|
+
}
|
|
37
|
+
return words
|
|
38
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
39
|
+
.join(" ");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const readTitle = async (root: string): Promise<string> => {
|
|
43
|
+
const packageJson = join(root, "package.json");
|
|
44
|
+
if (!existsSync(packageJson)) {
|
|
45
|
+
return "Documentation";
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(await readFile(packageJson, "utf-8")) as {
|
|
49
|
+
name?: unknown;
|
|
50
|
+
};
|
|
51
|
+
return typeof parsed.name === "string" && parsed.name.trim()
|
|
52
|
+
? prettifyTitle(parsed.name)
|
|
53
|
+
: "Documentation";
|
|
54
|
+
} catch {
|
|
55
|
+
return "Documentation";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const scrapeBaseUrl = async (root: string): Promise<string | null> => {
|
|
60
|
+
for (const candidate of SOURCE_FILES) {
|
|
61
|
+
const file = join(root, candidate);
|
|
62
|
+
if (!existsSync(file)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential probing of candidates
|
|
66
|
+
const base = BASE_URL.exec(await readFile(file, "utf-8"))?.groups?.base;
|
|
67
|
+
if (base) {
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface FumadocsConfigResult {
|
|
75
|
+
config: BlumeConfig;
|
|
76
|
+
warnings: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolve the Blume config and route prefix for a Fumadocs project. */
|
|
80
|
+
export const loadFumadocsConfig = async (
|
|
81
|
+
root: string
|
|
82
|
+
): Promise<FumadocsConfigResult> => {
|
|
83
|
+
const title = await readTitle(root);
|
|
84
|
+
const baseUrl = await scrapeBaseUrl(root);
|
|
85
|
+
// Default to the conventional Fumadocs `/docs` base when none is declared.
|
|
86
|
+
const prefix = (baseUrl ?? "docs").replaceAll(/^\/+|\/+$/gu, "");
|
|
87
|
+
const warnings: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (prefix) {
|
|
90
|
+
warnings.push(
|
|
91
|
+
`Docs are served under /${prefix} (set content.sources prefix); change it to "" to serve from the site root.`
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
config: {
|
|
95
|
+
content: {
|
|
96
|
+
sources: [{ prefix, root: "docs", type: "filesystem" }],
|
|
97
|
+
},
|
|
98
|
+
title,
|
|
99
|
+
},
|
|
100
|
+
warnings,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
warnings.push("Docs are served from the site root.");
|
|
105
|
+
return { config: { title }, warnings };
|
|
106
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile as readFileFromDisk } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
import { dirname, resolve } from "pathe";
|
|
6
|
+
|
|
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('"', """);
|
|
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
|
+
};
|