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,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
+ });
@@ -0,0 +1,228 @@
1
+ import { satteri } from "@astrojs/markdown-satteri";
2
+ import {
3
+ transformerMetaHighlight,
4
+ transformerNotationDiff,
5
+ transformerNotationFocus,
6
+ transformerNotationHighlight,
7
+ transformerNotationWordHighlight,
8
+ } from "@shikijs/transformers";
9
+ import { codeToHtml } from "shiki";
10
+
11
+ import { codeTitleTransformer } from "./code-title.ts";
12
+ import { directiveToCalloutPlugin } from "./directives.ts";
13
+ import { headingAnchorPlugin } from "./heading-anchors.ts";
14
+ import { inlineCodeHighlightPlugin } from "./inline-code.ts";
15
+ import { languageIconTransformer } from "./language-icon.ts";
16
+ import { mathPlugin } from "./math.ts";
17
+ import { mermaidPlugin } from "./mermaid.ts";
18
+ import { packageInstallPlugin } from "./package-install.ts";
19
+
20
+ /** A Shiki transformer, derived from the upstream factories' return type. */
21
+ type ShikiTransformer = ReturnType<typeof transformerNotationDiff>;
22
+
23
+ export {
24
+ PACKAGE_MANAGERS,
25
+ type PackageManager,
26
+ toPackageCommands,
27
+ } from "./package-commands.ts";
28
+ export {
29
+ type CodeTitleTransformer,
30
+ codeTitleTransformer,
31
+ } from "./code-title.ts";
32
+ export { calloutTypeFor } from "./directives.ts";
33
+ export { headingAnchorPlugin } from "./heading-anchors.ts";
34
+ export { mermaidPlugin } from "./mermaid.ts";
35
+ export { packageInstallPlugin } from "./package-install.ts";
36
+
37
+ /** Element type of Satteri's `mdastPlugins`, sourced from the (alpha) core. */
38
+ type MdastPlugin = NonNullable<
39
+ NonNullable<Parameters<typeof satteri>[0]>["mdastPlugins"]
40
+ >[number];
41
+
42
+ /** Element type of Satteri's `hastPlugins`. */
43
+ type HastPlugin = NonNullable<
44
+ NonNullable<Parameters<typeof satteri>[0]>["hastPlugins"]
45
+ >[number];
46
+
47
+ /**
48
+ * Hast plugins enabled by config. Inline `` `code`{:lang} `` highlighting is
49
+ * opt-in; self-linking heading anchors (`<h2>`–`<h6>` wrapped in an `<a>` to
50
+ * their own id) are on unless `markdown.headingAnchors` is `false`. Inline code
51
+ * runs first so the anchor wrap re-refs already-highlighted code.
52
+ */
53
+ const blumeHastPlugins = (options: BlumeMarkdownOptions): HastPlugin[] => {
54
+ const plugins: HastPlugin[] = [];
55
+ if (options.inline) {
56
+ plugins.push(inlineCodeHighlightPlugin() as unknown as HastPlugin);
57
+ }
58
+ if (options.headingAnchors !== false) {
59
+ plugins.push(headingAnchorPlugin() as unknown as HastPlugin);
60
+ }
61
+ return plugins;
62
+ };
63
+
64
+ /**
65
+ * Shiki transformers enabled by default for every code block. The four upstream
66
+ * notation transformers read GitHub-style comments and strip them from the
67
+ * output: `// [!code highlight]`, `// [!code ++]` / `// [!code --]`,
68
+ * `// [!code word:x]`, and `// [!code focus]`. The v3 match algorithm scopes a
69
+ * notation to the line it sits on (or the next, for a trailing comment).
70
+ * `transformerMetaHighlight` adds numeric range highlighting from the fence meta
71
+ * (` ```ts {1,3-5} `), reusing the same `highlighted` class. Blume's own
72
+ * {@link languageIconTransformer} prepends a brand icon, and
73
+ * {@link codeTitleTransformer} runs last to promote fence-meta (title / line
74
+ * numbers) to `<pre>` attributes. The theme styles the classes these emit
75
+ * (`highlighted`, `diff add/remove`, `highlighted-word`, `focused`,
76
+ * `blume-lang-icon`).
77
+ */
78
+ export interface BlumeShikiOptions {
79
+ /** Prepend a brand language icon to the header (`markdown.code.icons`). */
80
+ icons?: boolean;
81
+ }
82
+
83
+ export const blumeShikiTransformers = (
84
+ options: BlumeShikiOptions = {}
85
+ ): ShikiTransformer[] => {
86
+ const transformers: ShikiTransformer[] = [
87
+ transformerNotationHighlight({ matchAlgorithm: "v3" }),
88
+ transformerNotationDiff({ matchAlgorithm: "v3" }),
89
+ transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
90
+ transformerNotationFocus({ matchAlgorithm: "v3" }),
91
+ transformerMetaHighlight(),
92
+ ];
93
+ if (options.icons !== false) {
94
+ transformers.push(languageIconTransformer() as unknown as ShikiTransformer);
95
+ }
96
+ // The fence-meta reader (title / line numbers) always runs last.
97
+ transformers.push(codeTitleTransformer() as unknown as ShikiTransformer);
98
+ return transformers;
99
+ };
100
+
101
+ /**
102
+ * The light/dark Shiki themes Blume highlights with. Kept in lockstep with the
103
+ * generated Astro config's `shikiConfig.themes` so code highlighted outside the
104
+ * Markdown pipeline (via {@link highlightCode}) matches fenced code exactly.
105
+ */
106
+ const CODE_THEMES = { dark: "github-dark", light: "github-light" } as const;
107
+
108
+ const escapeHtml = (value: string): string =>
109
+ value
110
+ .replaceAll("&", "&amp;")
111
+ .replaceAll("<", "&lt;")
112
+ .replaceAll(">", "&gt;");
113
+
114
+ /**
115
+ * Tag the highlighted `<pre>` with `astro-code` (plus any extra classes) so the
116
+ * theme's code-block styles apply — `codeToHtml`'s bare output is `pre.shiki`,
117
+ * which the theme doesn't style.
118
+ */
119
+ const astroCodeClassTransformer = (extra?: string): ShikiTransformer =>
120
+ ({
121
+ name: "blume:astro-code-class",
122
+ pre(node: { properties: Record<string, unknown> }) {
123
+ const existing =
124
+ typeof node.properties.class === "string" ? node.properties.class : "";
125
+ node.properties.class = `astro-code ${extra ?? ""} ${existing}`
126
+ .replaceAll(/\s+/gu, " ")
127
+ .trim();
128
+ },
129
+ }) as unknown as ShikiTransformer;
130
+
131
+ export interface HighlightCodeOptions extends BlumeShikiOptions {
132
+ /** Extra `<pre>` class names, e.g. `blume-source` for a height-capped pane. */
133
+ className?: string;
134
+ }
135
+
136
+ /**
137
+ * Highlight a code string with the same Shiki themes and transformers as Blume's
138
+ * Markdown code fences, returning ready-to-render HTML. Use it to show themed
139
+ * code *outside* the Markdown pipeline (custom pages, components): the output
140
+ * carries the `astro-code` class and dual (light/dark) color variables, so the
141
+ * theme styles it — including the light/dark swap — with no extra CSS. The
142
+ * theme's code-block rules are scoped to `.prose`, so render the result inside a
143
+ * `.prose` container (the shipped `<CodeBlock>` does this). An unknown language
144
+ * falls back to an escaped plain block.
145
+ */
146
+ export const highlightCode = async (
147
+ code: string,
148
+ lang: string,
149
+ options: HighlightCodeOptions = {}
150
+ ): Promise<string> => {
151
+ try {
152
+ return await codeToHtml(code, {
153
+ defaultColor: false,
154
+ lang,
155
+ themes: CODE_THEMES,
156
+ transformers: [
157
+ ...blumeShikiTransformers({ icons: options.icons }),
158
+ astroCodeClassTransformer(options.className),
159
+ ],
160
+ });
161
+ } catch {
162
+ const className = `astro-code ${options.className ?? ""}`
163
+ .replaceAll(/\s+/gu, " ")
164
+ .trim();
165
+ return `<pre class="${className}"><code>${escapeHtml(code)}</code></pre>`;
166
+ }
167
+ };
168
+
169
+ /**
170
+ * Sätteri Markdown features Blume enables beyond Astro's defaults. GFM,
171
+ * frontmatter, and smart punctuation are already on; this adds superscript
172
+ * (`^text^`) and subscript (`~text~`), which render to native `<sup>`/`<sub>`.
173
+ */
174
+ const FEATURES = { subscript: true, superscript: true };
175
+
176
+ /** Options shared by both processors. */
177
+ export interface BlumeMarkdownOptions {
178
+ /**
179
+ * Wrap `<h2>`–`<h6>` in self-linking anchors (`markdown.headingAnchors`).
180
+ * On unless explicitly `false`.
181
+ */
182
+ headingAnchors?: boolean;
183
+ /** Highlight inline `` `code`{:lang} `` snippets (`markdown.code.inline`). */
184
+ inline?: boolean;
185
+ }
186
+
187
+ /** Sätteri processor for plain `.md`, with Blume's curated feature set. */
188
+ export const blumeMarkdownProcessor = (options: BlumeMarkdownOptions = {}) =>
189
+ satteri({
190
+ features: { ...FEATURES },
191
+ hastPlugins: blumeHastPlugins(options),
192
+ });
193
+
194
+ export interface BlumeMdxOptions extends BlumeMarkdownOptions {
195
+ /** Enable KaTeX math parsing and rendering. */
196
+ math?: boolean;
197
+ }
198
+
199
+ /**
200
+ * Sätteri MDX processor: Blume's feature set plus the MDAST plugins that target
201
+ * components — `package-install` → package-manager tabs, `:::note` →
202
+ * `<Callout>`, and ` ```mermaid ` → a `<blume-mermaid>` element. Used as the
203
+ * `processor` for `@astrojs/mdx` so these apply to
204
+ * `.mdx` only (plain `.md` uses {@link blumeMarkdownProcessor}). Math is opt-in
205
+ * via config since `$` is common in prose and code.
206
+ *
207
+ * The plugins are modeled with minimal structural types; bridge them to
208
+ * Satteri's full `MdastPlugin` type at this single boundary.
209
+ */
210
+ export const blumeMdxProcessor = (options: BlumeMdxOptions = {}) => {
211
+ const plugins: unknown[] = [
212
+ packageInstallPlugin(),
213
+ directiveToCalloutPlugin(),
214
+ mermaidPlugin(),
215
+ ];
216
+ if (options.math) {
217
+ plugins.push(mathPlugin());
218
+ }
219
+ return satteri({
220
+ features: {
221
+ ...FEATURES,
222
+ directive: true,
223
+ ...(options.math ? { math: true } : {}),
224
+ },
225
+ hastPlugins: blumeHastPlugins(options),
226
+ mdastPlugins: plugins as unknown as MdastPlugin[],
227
+ });
228
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Inline syntax highlighting for `` `code{:lang}` ``. A Satteri hast plugin runs
3
+ * after Markdown is turned into hast and looks at each inline `<code>` for a
4
+ * trailing `{:lang}` marker. The marker sits *inside* the backticks (Shiki's
5
+ * "tailing-curly-colon" convention) so it survives MDX, where a `{…}` after a
6
+ * code span would be parsed as a JSX expression. When found, it strips the
7
+ * marker and replaces the code's text with Shiki tokens (dual github-light/dark
8
+ * via CSS variables, like fenced blocks).
9
+ *
10
+ * Shiki is imported lazily, so it loads only on pages that actually use inline
11
+ * highlighting — and never when `markdown.code.inline` is off (the plugin is not
12
+ * added to the pipeline at all).
13
+ */
14
+
15
+ /** A minimal hast node (avoids a hast type dependency). */
16
+ interface HastNode {
17
+ children?: HastNode[];
18
+ properties?: Record<string, unknown>;
19
+ tagName?: string;
20
+ type: string;
21
+ value?: string;
22
+ }
23
+
24
+ /** The slice of Satteri's hast visitor context this plugin reads. */
25
+ interface HastContext {
26
+ parent: (node: HastNode) => { tagName?: string } | undefined;
27
+ textContent: (node: HastNode) => string;
28
+ }
29
+
30
+ /** A Satteri hast plugin, typed structurally to avoid a Satteri dep. */
31
+ export interface InlineCodePlugin {
32
+ name: string;
33
+ element: {
34
+ filter: string[];
35
+ visit: (node: HastNode, ctx: HastContext) => Promise<HastNode | undefined>;
36
+ };
37
+ }
38
+
39
+ /** Parsed `{:lang}` marker and the code with it removed. */
40
+ export interface InlineLang {
41
+ code: string;
42
+ lang: string;
43
+ }
44
+
45
+ const INLINE_LANG = /\{:(?<lang>[\w+-]+)\}$/u;
46
+
47
+ /** Parse a trailing `{:lang}` marker from an inline `<code>`'s own text. */
48
+ export const parseInlineLang = (text: string): InlineLang | null => {
49
+ const match = text.match(INLINE_LANG);
50
+ if (!match?.groups?.lang || match.index === undefined) {
51
+ return null;
52
+ }
53
+ const code = text.slice(0, match.index);
54
+ return code ? { code, lang: match.groups.lang } : null;
55
+ };
56
+
57
+ /** Loosely-typed Shiki entry: just the inline-highlight call this plugin makes. */
58
+ type InlineHighlighter = (
59
+ code: string,
60
+ options: {
61
+ defaultColor: false;
62
+ lang: string;
63
+ structure: "inline";
64
+ themes: { dark: string; light: string };
65
+ }
66
+ ) => Promise<{ children: HastNode[] }>;
67
+
68
+ // `import()` caches the module, so this dedupes Shiki across calls on its own.
69
+ const loadHighlighter = async (): Promise<InlineHighlighter> => {
70
+ const mod = await import("shiki");
71
+ return mod.codeToHast as unknown as InlineHighlighter;
72
+ };
73
+
74
+ /** Build the plugin. Highlights inline `` `code{:lang}` `` snippets. */
75
+ export const inlineCodeHighlightPlugin = (): InlineCodePlugin => ({
76
+ element: {
77
+ filter: ["code"],
78
+ async visit(node, ctx) {
79
+ // Only inline code: a <code> inside <pre> is a fenced block — skip it.
80
+ if (ctx.parent(node)?.tagName === "pre") {
81
+ return;
82
+ }
83
+ const parsed = parseInlineLang(ctx.textContent(node));
84
+ if (!parsed) {
85
+ return;
86
+ }
87
+ try {
88
+ const codeToHast = await loadHighlighter();
89
+ const root = await codeToHast(parsed.code, {
90
+ defaultColor: false,
91
+ lang: parsed.lang,
92
+ structure: "inline",
93
+ themes: { dark: "github-dark", light: "github-light" },
94
+ });
95
+ return {
96
+ children: root.children,
97
+ properties: { className: ["blume-inline-code"] },
98
+ tagName: "code",
99
+ type: "element",
100
+ };
101
+ } catch {
102
+ // Unknown language or load failure: leave the code unhighlighted
103
+ // (falling through returns undefined, so the node is left as-is).
104
+ }
105
+ },
106
+ },
107
+ name: "blume:inline-code",
108
+ });