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,47 @@
|
|
|
1
|
+
import type { UIStringsOverride } from "../i18n-ui.ts";
|
|
2
|
+
|
|
3
|
+
/** Chinese — Traditional (zh-TW) UI strings. */
|
|
4
|
+
export const zhTW: UIStringsOverride = {
|
|
5
|
+
actions: {
|
|
6
|
+
addToCursor: "新增至 Cursor",
|
|
7
|
+
addToVscode: "新增至 VS Code",
|
|
8
|
+
askAI: "向 AI 詢問此頁面",
|
|
9
|
+
connectMcp: "連線至 MCP",
|
|
10
|
+
copied: "已複製!",
|
|
11
|
+
copyClaudeCode: "複製 Claude Code 指令",
|
|
12
|
+
copyMarkdown: "複製為 Markdown",
|
|
13
|
+
copyServerUrl: "複製伺服器 URL",
|
|
14
|
+
edit: "在 GitHub 上編輯",
|
|
15
|
+
openInChat: "在聊天中開啟",
|
|
16
|
+
scrollToTop: "回到頂部",
|
|
17
|
+
},
|
|
18
|
+
ask: {
|
|
19
|
+
empty: "針對文件提出問題。",
|
|
20
|
+
error: "抱歉,發生了一些問題。",
|
|
21
|
+
label: "提出問題",
|
|
22
|
+
placeholder: "提出問題…",
|
|
23
|
+
send: "傳送",
|
|
24
|
+
title: "向 AI 提問",
|
|
25
|
+
},
|
|
26
|
+
feedback: {
|
|
27
|
+
no: "沒有幫助",
|
|
28
|
+
question: "這個頁面有幫助嗎?",
|
|
29
|
+
thanks: "感謝你的意見回饋!",
|
|
30
|
+
yes: "有幫助",
|
|
31
|
+
},
|
|
32
|
+
languageSwitcher: { label: "語言", untranslated: "未翻譯" },
|
|
33
|
+
page: {
|
|
34
|
+
lastUpdated: "最後更新於",
|
|
35
|
+
next: "下一頁",
|
|
36
|
+
previous: "上一頁",
|
|
37
|
+
skipToContent: "跳至內容",
|
|
38
|
+
},
|
|
39
|
+
search: {
|
|
40
|
+
button: "搜尋",
|
|
41
|
+
devOnly: "搜尋在正式版組建中可用。",
|
|
42
|
+
label: "搜尋文件",
|
|
43
|
+
noResults: "找不到結果。",
|
|
44
|
+
placeholder: "搜尋文件…",
|
|
45
|
+
},
|
|
46
|
+
toc: { title: "本頁內容" },
|
|
47
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { UIStringsOverride } from "../i18n-ui.ts";
|
|
2
|
+
|
|
3
|
+
/** Chinese — Simplified (zh) UI strings. */
|
|
4
|
+
export const zh: UIStringsOverride = {
|
|
5
|
+
actions: {
|
|
6
|
+
addToCursor: "添加到 Cursor",
|
|
7
|
+
addToVscode: "添加到 VS Code",
|
|
8
|
+
askAI: "向 AI 询问此页面",
|
|
9
|
+
connectMcp: "连接到 MCP",
|
|
10
|
+
copied: "已复制!",
|
|
11
|
+
copyClaudeCode: "复制 Claude Code 命令",
|
|
12
|
+
copyMarkdown: "复制为 Markdown",
|
|
13
|
+
copyServerUrl: "复制服务器 URL",
|
|
14
|
+
edit: "在 GitHub 上编辑",
|
|
15
|
+
openInChat: "在聊天中打开",
|
|
16
|
+
scrollToTop: "回到顶部",
|
|
17
|
+
},
|
|
18
|
+
ask: {
|
|
19
|
+
empty: "就文档提出问题。",
|
|
20
|
+
error: "抱歉,出了点问题。",
|
|
21
|
+
label: "提出问题",
|
|
22
|
+
placeholder: "提出问题…",
|
|
23
|
+
send: "发送",
|
|
24
|
+
title: "向 AI 提问",
|
|
25
|
+
},
|
|
26
|
+
feedback: {
|
|
27
|
+
no: "没有帮助",
|
|
28
|
+
question: "这个页面有帮助吗?",
|
|
29
|
+
thanks: "感谢你的反馈!",
|
|
30
|
+
yes: "有帮助",
|
|
31
|
+
},
|
|
32
|
+
languageSwitcher: { label: "语言", untranslated: "未翻译" },
|
|
33
|
+
page: {
|
|
34
|
+
lastUpdated: "最后更新于",
|
|
35
|
+
next: "下一页",
|
|
36
|
+
previous: "上一页",
|
|
37
|
+
skipToContent: "跳到内容",
|
|
38
|
+
},
|
|
39
|
+
search: {
|
|
40
|
+
button: "搜索",
|
|
41
|
+
devOnly: "搜索在生产构建中可用。",
|
|
42
|
+
label: "搜索文档",
|
|
43
|
+
noResults: "未找到结果。",
|
|
44
|
+
placeholder: "搜索文档…",
|
|
45
|
+
},
|
|
46
|
+
toc: { title: "本页内容" },
|
|
47
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { join } from "pathe";
|
|
4
|
+
|
|
5
|
+
import { packageRoot } from "./package-root.ts";
|
|
6
|
+
|
|
7
|
+
let cached: string | undefined;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The installed Blume package version, read lazily from its `package.json`.
|
|
11
|
+
*
|
|
12
|
+
* Computed on demand (not at module load) so importing the `blume` barrel has no
|
|
13
|
+
* filesystem side effect, and anchored at the package root so it resolves the
|
|
14
|
+
* same whether running from source or the bundled CLI.
|
|
15
|
+
*/
|
|
16
|
+
export const getBlumeVersion = (): string => {
|
|
17
|
+
if (cached === undefined) {
|
|
18
|
+
const pkgPath = join(packageRoot(), "package.json");
|
|
19
|
+
cached = (JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string })
|
|
20
|
+
.version;
|
|
21
|
+
}
|
|
22
|
+
return cached;
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a robots.txt that allows all crawlers and points to the sitemap when
|
|
5
|
+
* one is available (a `site` is set and the sitemap is enabled). Returns null
|
|
6
|
+
* when robots generation is disabled.
|
|
7
|
+
*/
|
|
8
|
+
export const buildRobots = (project: BlumeProject): string | null => {
|
|
9
|
+
const { config } = project;
|
|
10
|
+
if (!config.seo.robots) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lines = ["User-agent: *", "Allow: /"];
|
|
15
|
+
const { site } = config.deployment;
|
|
16
|
+
if (site && config.seo.sitemap) {
|
|
17
|
+
lines.push("", `Sitemap: ${site.replace(/\/$/u, "")}/sitemap.xml`);
|
|
18
|
+
}
|
|
19
|
+
return `${lines.join("\n")}\n`;
|
|
20
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
|
+
import type { PageRecord } from "../core/types.ts";
|
|
3
|
+
|
|
4
|
+
/** A single feed entry derived from a content page. */
|
|
5
|
+
export interface RssItem {
|
|
6
|
+
title: string;
|
|
7
|
+
/** Absolute item URL. */
|
|
8
|
+
link: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Publish date, when the page declares one. */
|
|
11
|
+
date?: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A resolved RSS feed for one content type. */
|
|
15
|
+
export interface RssFeed {
|
|
16
|
+
/** Content type the feed covers, e.g. `blog`. */
|
|
17
|
+
type: string;
|
|
18
|
+
/** Feed URL path, e.g. `/blog/rss.xml`. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Channel title, e.g. `Blume — Blog`. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Absolute site link. */
|
|
23
|
+
link: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
items: RssItem[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const capitalize = (value: string): string =>
|
|
29
|
+
value.charAt(0).toUpperCase() + value.slice(1);
|
|
30
|
+
|
|
31
|
+
/** Publish date for a page: top-level `date`, else `changelog.date`. */
|
|
32
|
+
const pageDate = (page: PageRecord): Date | undefined => {
|
|
33
|
+
const raw = page.meta.date ?? page.meta.changelog?.date;
|
|
34
|
+
if (!raw) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const date = new Date(raw);
|
|
38
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build an RSS feed per configured content type that has publishable pages.
|
|
43
|
+
* Returns an empty list when RSS is disabled or no `deployment.site` is set —
|
|
44
|
+
* absolute URLs are required for a valid feed, mirroring `buildSitemap`.
|
|
45
|
+
*/
|
|
46
|
+
export const buildRssFeeds = (project: BlumeProject): RssFeed[] => {
|
|
47
|
+
const { config } = project;
|
|
48
|
+
const { rss } = config.seo;
|
|
49
|
+
const { site } = config.deployment;
|
|
50
|
+
if (!(rss.enabled && site)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const base = site.replace(/\/$/u, "");
|
|
54
|
+
|
|
55
|
+
const feeds: RssFeed[] = [];
|
|
56
|
+
for (const type of rss.types) {
|
|
57
|
+
const pages = project.graph.pages.filter(
|
|
58
|
+
(page) =>
|
|
59
|
+
page.contentType === type &&
|
|
60
|
+
!(page.meta.draft || page.meta.sidebar.hidden)
|
|
61
|
+
);
|
|
62
|
+
if (pages.length === 0) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const items: RssItem[] = pages
|
|
67
|
+
.map((page) => ({
|
|
68
|
+
date: pageDate(page),
|
|
69
|
+
description: page.description,
|
|
70
|
+
link: `${base}${page.route}`,
|
|
71
|
+
title: page.title,
|
|
72
|
+
}))
|
|
73
|
+
.toSorted((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0))
|
|
74
|
+
.slice(0, rss.limit);
|
|
75
|
+
|
|
76
|
+
feeds.push({
|
|
77
|
+
description: config.description,
|
|
78
|
+
items,
|
|
79
|
+
link: base,
|
|
80
|
+
path: `/${type}/rss.xml`,
|
|
81
|
+
title: `${config.title} — ${capitalize(type)}`,
|
|
82
|
+
type,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return feeds;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const escapeXml = (value: string): string =>
|
|
89
|
+
value
|
|
90
|
+
.replaceAll("&", "&")
|
|
91
|
+
.replaceAll("<", "<")
|
|
92
|
+
.replaceAll(">", ">")
|
|
93
|
+
.replaceAll('"', """)
|
|
94
|
+
.replaceAll("'", "'");
|
|
95
|
+
|
|
96
|
+
const renderItem = (item: RssItem): string => {
|
|
97
|
+
const parts = [
|
|
98
|
+
` <title>${escapeXml(item.title)}</title>`,
|
|
99
|
+
` <link>${escapeXml(item.link)}</link>`,
|
|
100
|
+
` <guid isPermaLink="true">${escapeXml(item.link)}</guid>`,
|
|
101
|
+
];
|
|
102
|
+
if (item.description) {
|
|
103
|
+
parts.push(` <description>${escapeXml(item.description)}</description>`);
|
|
104
|
+
}
|
|
105
|
+
if (item.date) {
|
|
106
|
+
parts.push(` <pubDate>${item.date.toUTCString()}</pubDate>`);
|
|
107
|
+
}
|
|
108
|
+
return ` <item>\n${parts.join("\n")}\n </item>`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Serialize a resolved feed into an RSS 2.0 XML document. */
|
|
112
|
+
export const renderRssFeed = (feed: RssFeed): string => {
|
|
113
|
+
const channel = [
|
|
114
|
+
` <title>${escapeXml(feed.title)}</title>`,
|
|
115
|
+
` <link>${escapeXml(feed.link)}</link>`,
|
|
116
|
+
` <description>${escapeXml(feed.description ?? feed.title)}</description>`,
|
|
117
|
+
` <atom:link href="${escapeXml(`${feed.link}${feed.path}`)}" rel="self" type="application/rss+xml" />`,
|
|
118
|
+
];
|
|
119
|
+
const items = feed.items.map(renderItem).join("\n");
|
|
120
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
121
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
122
|
+
<channel>
|
|
123
|
+
${channel.join("\n")}
|
|
124
|
+
${items}
|
|
125
|
+
</channel>
|
|
126
|
+
</rss>
|
|
127
|
+
`;
|
|
128
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a sitemap.xml from the route manifest. Returns null when the sitemap is
|
|
5
|
+
* disabled or no `site` is configured (absolute URLs are required for a valid
|
|
6
|
+
* sitemap). Drafts, hidden, and `noindex` pages are excluded.
|
|
7
|
+
*/
|
|
8
|
+
export const buildSitemap = (project: BlumeProject): string | null => {
|
|
9
|
+
const { site } = project.config.deployment;
|
|
10
|
+
if (!(site && project.config.seo.sitemap)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const base = site.replace(/\/$/u, "");
|
|
15
|
+
const urls = project.graph.pages
|
|
16
|
+
.filter(
|
|
17
|
+
(page) =>
|
|
18
|
+
!(page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex)
|
|
19
|
+
)
|
|
20
|
+
.map((page) => ` <url><loc>${base}${page.route}</loc></url>`)
|
|
21
|
+
.toSorted();
|
|
22
|
+
|
|
23
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
24
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
25
|
+
${urls.join("\n")}
|
|
26
|
+
</urlset>
|
|
27
|
+
`;
|
|
28
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { defineConfig } from "./core/config.ts";
|
|
2
|
+
export { defineComponents } from "./core/define-components.ts";
|
|
3
|
+
export type {
|
|
4
|
+
ComponentOverride,
|
|
5
|
+
ComponentOverrides,
|
|
6
|
+
IslandDescriptor,
|
|
7
|
+
} from "./core/define-components.ts";
|
|
8
|
+
export { defineMeta } from "./core/define-meta.ts";
|
|
9
|
+
export type {
|
|
10
|
+
FolderMetaDefinition,
|
|
11
|
+
FolderMetaFactory,
|
|
12
|
+
} from "./core/define-meta.ts";
|
|
13
|
+
export type {
|
|
14
|
+
BlumeConfig,
|
|
15
|
+
FolderMeta,
|
|
16
|
+
HydrationMode,
|
|
17
|
+
ResolvedConfig,
|
|
18
|
+
} from "./core/schema.ts";
|
|
19
|
+
export type {
|
|
20
|
+
Diagnostic,
|
|
21
|
+
Heading,
|
|
22
|
+
NavNode,
|
|
23
|
+
Navigation,
|
|
24
|
+
NavTab,
|
|
25
|
+
PageRecord,
|
|
26
|
+
} from "./core/types.ts";
|
|
27
|
+
export { getBlumeVersion } from "./core/version.ts";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-fence meta. A Shiki transformer reads the tokens after the language and
|
|
3
|
+
* promotes them to attributes on the rendered `<pre>`:
|
|
4
|
+
*
|
|
5
|
+
* - a title — the first bare token (```ts blume.config.ts) or `title="..."` —
|
|
6
|
+
* becomes `data-title`; the theme's code header shows it, falling back to the
|
|
7
|
+
* language label.
|
|
8
|
+
* - the `lineNumbers` keyword (```ts file.ts lineNumbers) becomes
|
|
9
|
+
* `data-line-numbers`; the theme renders a counter-driven line-number gutter.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The slice of Shiki's transformer `this` context Blume reads. */
|
|
13
|
+
interface CodeMetaContext {
|
|
14
|
+
options: { meta?: { __raw?: string } };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The `<pre>` hast node a Shiki `pre` hook receives. */
|
|
18
|
+
interface PreNode {
|
|
19
|
+
properties: Record<string, boolean | number | string | undefined>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A Shiki-compatible transformer, typed structurally to avoid a Shiki dep. */
|
|
23
|
+
export interface CodeTitleTransformer {
|
|
24
|
+
name: string;
|
|
25
|
+
pre: (this: CodeMetaContext, node: PreNode) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TITLE_ATTR = /title=(?<quote>["'])(?<title>[^"']*)\k<quote>/u;
|
|
29
|
+
const LINE_NUMBERS = /(?:^|\s)lineNumbers(?=\s|$)/u;
|
|
30
|
+
|
|
31
|
+
const parseTitle = (raw: string | undefined): string | undefined => {
|
|
32
|
+
if (!raw) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const explicit = raw.match(TITLE_ATTR);
|
|
36
|
+
if (explicit?.groups?.title) {
|
|
37
|
+
return explicit.groups.title;
|
|
38
|
+
}
|
|
39
|
+
// The first bare token is the title (```ts blume.config.ts), skipping Shiki
|
|
40
|
+
// line ranges (`{1,3-5}`), `key=value` attrs, and the reserved `lineNumbers`
|
|
41
|
+
// and `twoslash` keywords.
|
|
42
|
+
return raw
|
|
43
|
+
.trim()
|
|
44
|
+
.split(/\s+/u)
|
|
45
|
+
.find(
|
|
46
|
+
(token) =>
|
|
47
|
+
token.length > 0 &&
|
|
48
|
+
token !== "lineNumbers" &&
|
|
49
|
+
token !== "twoslash" &&
|
|
50
|
+
!token.startsWith("{") &&
|
|
51
|
+
!token.includes("=")
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const hasLineNumbers = (raw: string | undefined): boolean =>
|
|
56
|
+
Boolean(raw && LINE_NUMBERS.test(raw));
|
|
57
|
+
|
|
58
|
+
/** Build the transformer. Runs after Shiki's built-in `data-language` hook. */
|
|
59
|
+
export const codeTitleTransformer = (): CodeTitleTransformer => ({
|
|
60
|
+
name: "blume:code-meta",
|
|
61
|
+
pre(node) {
|
|
62
|
+
const raw = this.options.meta?.__raw;
|
|
63
|
+
const title = parseTitle(raw);
|
|
64
|
+
if (title) {
|
|
65
|
+
node.properties.dataTitle = title;
|
|
66
|
+
}
|
|
67
|
+
if (hasLineNumbers(raw)) {
|
|
68
|
+
node.properties.dataLineNumbers = true;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsxAttribute, jsxFlowElement } from "./mdast.ts";
|
|
2
|
+
import type { MdastNode, MdastVisitorContext } from "./mdast.ts";
|
|
3
|
+
|
|
4
|
+
interface DirectiveNode extends MdastNode {
|
|
5
|
+
attributes?: Record<string, string | null | undefined> | null;
|
|
6
|
+
children: MdastNode[];
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Directive names that map directly onto a Callout type. */
|
|
11
|
+
const CALLOUT_TYPES = new Set([
|
|
12
|
+
"danger",
|
|
13
|
+
"info",
|
|
14
|
+
"note",
|
|
15
|
+
"success",
|
|
16
|
+
"tip",
|
|
17
|
+
"warning",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/** Friendly aliases for the canonical Callout types. */
|
|
21
|
+
const ALIASES: Record<string, string> = {
|
|
22
|
+
caution: "warning",
|
|
23
|
+
error: "danger",
|
|
24
|
+
important: "note",
|
|
25
|
+
warn: "warning",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Resolve a directive name to a Callout type, or `null` if it is not one. */
|
|
29
|
+
export const calloutTypeFor = (name: string): string | null => {
|
|
30
|
+
const lower = name.toLowerCase();
|
|
31
|
+
if (CALLOUT_TYPES.has(lower)) {
|
|
32
|
+
return lower;
|
|
33
|
+
}
|
|
34
|
+
return ALIASES[lower] ?? null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface TextNode extends MdastNode {
|
|
38
|
+
value?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Concatenate the plain text of a node's immediate phrasing children. */
|
|
42
|
+
const textOf = (node: MdastNode): string =>
|
|
43
|
+
((node.children as TextNode[] | undefined) ?? [])
|
|
44
|
+
.map((child) => child.value ?? "")
|
|
45
|
+
.join("");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Satteri MDAST plugin mapping container directives (`:::note`, `:::warning`,
|
|
49
|
+
* `:::tip`, …) onto Blume's `<Callout>` component. The title comes from a
|
|
50
|
+
* `[label]` or a `{title="…"}` attribute; the body becomes the callout content.
|
|
51
|
+
* Directive names that are not callouts are left untouched.
|
|
52
|
+
*/
|
|
53
|
+
export const directiveToCalloutPlugin = () => ({
|
|
54
|
+
containerDirective(node: DirectiveNode, ctx: MdastVisitorContext) {
|
|
55
|
+
const type = calloutTypeFor(node.name);
|
|
56
|
+
if (type === null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const children = [...node.children];
|
|
61
|
+
let title = node.attributes?.title ?? undefined;
|
|
62
|
+
|
|
63
|
+
// A leading `:::name[Label]` parses to a paragraph flagged `directiveLabel`.
|
|
64
|
+
const labelIndex = children.findIndex(
|
|
65
|
+
(child) =>
|
|
66
|
+
child.type === "paragraph" &&
|
|
67
|
+
(child.data as { directiveLabel?: boolean } | undefined)?.directiveLabel
|
|
68
|
+
);
|
|
69
|
+
if (labelIndex !== -1) {
|
|
70
|
+
const [label] = children.splice(labelIndex, 1);
|
|
71
|
+
if (label) {
|
|
72
|
+
title ??= textOf(label) || undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const attributes = [jsxAttribute("type", type)];
|
|
77
|
+
if (title) {
|
|
78
|
+
attributes.push(jsxAttribute("title", title));
|
|
79
|
+
}
|
|
80
|
+
ctx.replaceNode(node, jsxFlowElement("Callout", attributes, children));
|
|
81
|
+
},
|
|
82
|
+
name: "blume-directive-callout",
|
|
83
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn every section heading into its own permalink. A Satteri hast plugin runs
|
|
3
|
+
* after Markdown is turned into hast and wraps each `<h2>`–`<h6>`'s content in an
|
|
4
|
+
* `<a href="#slug">`, so a reader can click the heading to copy, bookmark, or
|
|
5
|
+
* share a link straight to that section. `<h1>` (the page title) is slugged for
|
|
6
|
+
* parity but left unwrapped.
|
|
7
|
+
*
|
|
8
|
+
* Satteri's own `heading-ids` plugin (which assigns the `id` used by the table
|
|
9
|
+
* of contents) runs *after* every user hast plugin, and it reuses an `id` that
|
|
10
|
+
* is already present rather than re-slugging. So this plugin is the authoritative
|
|
11
|
+
* id setter: it slugs each heading with the same algorithm (a per-document
|
|
12
|
+
* `github-slugger`, the library Satteri and rehype-slug both use) and writes the
|
|
13
|
+
* `id`, which `heading-ids` then adopts — keeping the in-page anchor, the
|
|
14
|
+
* heading's `id`, and the TOC entry in lockstep. To match Satteri's duplicate
|
|
15
|
+
* disambiguation (`setup`, `setup-1`, …) exactly, it advances the slugger over
|
|
16
|
+
* `<h1>`–`<h6>` in document order even though only `<h2>`–`<h6>` get wrapped.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { satteriCollectHastText } from "@astrojs/markdown-satteri";
|
|
20
|
+
import GithubSlugger from "github-slugger";
|
|
21
|
+
|
|
22
|
+
/** A minimal hast node (avoids a hast type dependency). */
|
|
23
|
+
interface HastNode {
|
|
24
|
+
children?: HastNode[];
|
|
25
|
+
name?: string;
|
|
26
|
+
properties?: Record<string, unknown>;
|
|
27
|
+
tagName?: string;
|
|
28
|
+
type: string;
|
|
29
|
+
value?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The slice of Satteri's hast visitor context this plugin reads. */
|
|
33
|
+
interface HastContext {
|
|
34
|
+
data?: { astro?: { frontmatter?: Record<string, unknown> } };
|
|
35
|
+
setProperty: (node: HastNode, key: string, value: unknown) => void;
|
|
36
|
+
textContent: (node: HastNode) => string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A Satteri hast plugin, typed structurally to avoid a Satteri dep. */
|
|
40
|
+
export interface HeadingAnchorPlugin {
|
|
41
|
+
name: string;
|
|
42
|
+
element: {
|
|
43
|
+
filter: string[];
|
|
44
|
+
visit: (node: HastNode, ctx: HastContext) => HastNode | undefined;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Headings slugged for id parity with Satteri; only a subset gets wrapped. */
|
|
49
|
+
const HEADINGS = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
50
|
+
const WRAPPED = new Set(["h2", "h3", "h4", "h5", "h6"]);
|
|
51
|
+
|
|
52
|
+
/** True if the subtree already contains an `<a>`, so wrapping would nest links. */
|
|
53
|
+
const containsAnchor = (node: HastNode): boolean => {
|
|
54
|
+
for (const child of node.children ?? []) {
|
|
55
|
+
if ((child.tagName ?? child.name) === "a" || containsAnchor(child)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// One slugger per document render. The plugin instance is shared across every
|
|
63
|
+
// page, but slug disambiguation must reset per document; the render-scoped
|
|
64
|
+
// `astro` data object is a stable, unique key for one render (entries are
|
|
65
|
+
// dropped once the render is collected, so this never leaks).
|
|
66
|
+
const FALLBACK_SCOPE: object = {};
|
|
67
|
+
const sluggers = new WeakMap<object, GithubSlugger>();
|
|
68
|
+
|
|
69
|
+
const sluggerFor = (ctx: HastContext): GithubSlugger => {
|
|
70
|
+
const scope = ctx.data?.astro ?? ctx.data ?? FALLBACK_SCOPE;
|
|
71
|
+
const existing = sluggers.get(scope);
|
|
72
|
+
if (existing) {
|
|
73
|
+
return existing;
|
|
74
|
+
}
|
|
75
|
+
const slugger = new GithubSlugger();
|
|
76
|
+
sluggers.set(scope, slugger);
|
|
77
|
+
return slugger;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The slug for a heading, mirroring Satteri's `heading-ids` exactly. */
|
|
81
|
+
const slugFor = (
|
|
82
|
+
node: HastNode,
|
|
83
|
+
ctx: HastContext,
|
|
84
|
+
slugger: GithubSlugger
|
|
85
|
+
): string => {
|
|
86
|
+
const rawText = ctx.textContent(node);
|
|
87
|
+
// `frontmatter`-interpolated MDX headings (`## {frontmatter.title}`) need the
|
|
88
|
+
// resolved value; the helper is the same one `heading-ids` defers to.
|
|
89
|
+
const text = rawText.includes("frontmatter")
|
|
90
|
+
? satteriCollectHastText(
|
|
91
|
+
node as Parameters<typeof satteriCollectHastText>[0],
|
|
92
|
+
ctx.data?.astro?.frontmatter ?? {}
|
|
93
|
+
)
|
|
94
|
+
: rawText;
|
|
95
|
+
const existingId = node.properties?.id;
|
|
96
|
+
return typeof existingId === "string" ? existingId : slugger.slug(text);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Build the plugin. Wraps `<h2>`–`<h6>` in self-linking anchors. */
|
|
100
|
+
export const headingAnchorPlugin = (): HeadingAnchorPlugin => ({
|
|
101
|
+
element: {
|
|
102
|
+
filter: HEADINGS,
|
|
103
|
+
visit(node, ctx) {
|
|
104
|
+
const slug = slugFor(node, ctx, sluggerFor(ctx));
|
|
105
|
+
const wrap = node.tagName
|
|
106
|
+
? WRAPPED.has(node.tagName) && slug !== "" && !containsAnchor(node)
|
|
107
|
+
: false;
|
|
108
|
+
if (!wrap) {
|
|
109
|
+
// Unwrapped headings (h1, an empty slug, or one that already links) still
|
|
110
|
+
// need the id so `heading-ids` adopts it instead of re-slugging.
|
|
111
|
+
if (typeof node.properties?.id !== "string") {
|
|
112
|
+
ctx.setProperty(node, "id", slug);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Replacing the heading re-emits its original children as refs inside the
|
|
117
|
+
// new anchor (Satteri passes reused nodes through untouched).
|
|
118
|
+
return {
|
|
119
|
+
children: [
|
|
120
|
+
{
|
|
121
|
+
children: node.children ?? [],
|
|
122
|
+
properties: {
|
|
123
|
+
className: ["blume-heading-anchor"],
|
|
124
|
+
href: `#${slug}`,
|
|
125
|
+
},
|
|
126
|
+
tagName: "a",
|
|
127
|
+
type: "element",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
properties: { ...node.properties, id: slug },
|
|
131
|
+
tagName: node.tagName,
|
|
132
|
+
type: "element",
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
name: "blume:heading-anchors",
|
|
137
|
+
});
|