blume 1.5.2 → 1.6.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
@@ -12,7 +12,9 @@ import { codeToHtml } from "shiki";
12
12
  import { baseLinksPlugin } from "./base-links.ts";
13
13
  import { codeTitleTransformer } from "./code-title.ts";
14
14
  import { directiveToCalloutPlugin } from "./directives.ts";
15
+ import { MARKDOWN_FEATURES, MDX_FEATURES } from "./features.ts";
15
16
  import { headingAnchorPlugin } from "./heading-anchors.ts";
17
+ import { includePlugin } from "./include.ts";
16
18
  import { inlineCodeHighlightPlugin } from "./inline-code.ts";
17
19
  import { languageIconTransformer } from "./language-icon.ts";
18
20
  import { mathPlugin } from "./math.ts";
@@ -21,6 +23,7 @@ import { packageInstallPlugin } from "./package-install.ts";
21
23
  import { tableWrapPlugin } from "./table-wrap.ts";
22
24
  import { DEFAULT_CODE_THEMES } from "./themes.ts";
23
25
  import type { CodeThemes } from "./themes.ts";
26
+ import { ts2jsPlugin } from "./ts2js.ts";
24
27
 
25
28
  export type { CodeTheme, CodeThemes } from "./themes.ts";
26
29
 
@@ -38,8 +41,10 @@ export {
38
41
  } from "./code-title.ts";
39
42
  export { calloutTypeFor } from "./directives.ts";
40
43
  export { headingAnchorPlugin } from "./heading-anchors.ts";
44
+ export { includePlugin } from "./include.ts";
41
45
  export { mermaidPlugin } from "./mermaid.ts";
42
46
  export { packageInstallPlugin } from "./package-install.ts";
47
+ export { ts2jsPlugin } from "./ts2js.ts";
43
48
  export { blumeTwoslashTransformer } from "./twoslash.ts";
44
49
 
45
50
  /** Element type of Satteri's `mdastPlugins`, sourced from the (alpha) core. */
@@ -77,21 +82,18 @@ const asShikiTransformer = (transformer: { name: string }): ShikiTransformer =>
77
82
  /**
78
83
  * Hast plugins enabled by config. Inline `` `code`{:lang} `` highlighting is
79
84
  * always on: it only fires on an explicit trailing `{:lang}` marker, so plain
80
- * inline code is untouched and there's nothing to opt out of. Self-linking
81
- * heading anchors (`<h2>`–`<h6>` wrapped in an `<a>` to their own id) are on
82
- * unless `markdown.headingAnchors` is `false`. Inline code runs first so the
83
- * anchor wrap re-refs already-highlighted code.
85
+ * inline code is untouched and there's nothing to opt out of. The heading
86
+ * plugin also always runs — it owns heading ids and the trailing markers
87
+ * (`[#custom-id]`, `[!toc]`, `[toc]`), which must parse regardless of config —
88
+ * while `markdown.headingAnchors: false` only turns off the self-linking
89
+ * anchor wrap on `<h2>`–`<h6>`. Inline code runs first so the anchor wrap
90
+ * re-refs already-highlighted code.
84
91
  */
85
- const blumeHastPlugins = (options: BlumeMarkdownOptions): HastPlugin[] => {
86
- const plugins: HastPlugin[] = [
87
- asHastPlugin(inlineCodeHighlightPlugin(options.codeThemes)),
88
- asHastPlugin(tableWrapPlugin()),
89
- ];
90
- if (options.headingAnchors !== false) {
91
- plugins.push(asHastPlugin(headingAnchorPlugin()));
92
- }
93
- return plugins;
94
- };
92
+ const blumeHastPlugins = (options: BlumeMarkdownOptions): HastPlugin[] => [
93
+ asHastPlugin(inlineCodeHighlightPlugin(options.codeThemes)),
94
+ asHastPlugin(tableWrapPlugin()),
95
+ asHastPlugin(headingAnchorPlugin({ wrap: options.headingAnchors !== false })),
96
+ ];
95
97
 
96
98
  /**
97
99
  * Shiki transformers enabled by default for every code block. The four upstream
@@ -242,13 +244,6 @@ export const highlightCode = async (
242
244
  }
243
245
  };
244
246
 
245
- /**
246
- * Sätteri Markdown features Blume enables beyond Astro's defaults. GFM,
247
- * frontmatter, and smart punctuation are already on; this adds superscript
248
- * (`^text^`) and subscript (`~text~`), which render to native `<sup>`/`<sub>`.
249
- */
250
- const FEATURES = { subscript: true, superscript: true };
251
-
252
247
  /** Options shared by both processors. */
253
248
  export interface BlumeMarkdownOptions {
254
249
  /**
@@ -273,12 +268,17 @@ export interface BlumeMarkdownOptions {
273
268
  * `basePath` link isn't double-prefixed (see `withComposedBasePath`).
274
269
  */
275
270
  deployBase?: string;
271
+ /**
272
+ * The docs content root, bounding `<include>` target resolution (and
273
+ * anchoring `/`-leading include paths). When unset, relative includes still
274
+ * resolve from the including file.
275
+ */
276
+ contentRoot?: string;
276
277
  }
277
278
 
278
279
  /**
279
- * MDAST plugins that apply to both `.md` and `.mdx`. Currently just the
280
- * base-path link rewrite, added only when a `basePath` or `deployBase` is
281
- * configured.
280
+ * MDAST plugins that apply to both `.md` and `.mdx`: the base-path link
281
+ * rewrite (added only when a `basePath` or `deployBase` is configured).
282
282
  */
283
283
  const blumeSharedMdastPlugins = (
284
284
  options: BlumeMarkdownOptions
@@ -291,20 +291,32 @@ const blumeSharedMdastPlugins = (
291
291
  ]
292
292
  : [];
293
293
 
294
+ /**
295
+ * The `<include>` splice. Always first: its mutations apply before the next
296
+ * plugin runs, so spliced content flows through the rest of the chain
297
+ * (callouts, mermaid, math, base links) like inline content.
298
+ */
299
+ const blumeIncludePlugin = (options: BlumeMarkdownOptions): MdastPlugin =>
300
+ asMdastPlugin(includePlugin({ contentRoot: options.contentRoot }));
301
+
294
302
  /** Sätteri processor for plain `.md`, with Blume's curated feature set. */
295
303
  export const blumeMarkdownProcessor = (options: BlumeMarkdownOptions = {}) =>
296
304
  satteri({
297
- features: { ...FEATURES },
305
+ features: { ...MARKDOWN_FEATURES },
298
306
  hastPlugins: blumeHastPlugins(options),
299
- mdastPlugins: blumeSharedMdastPlugins(options),
307
+ mdastPlugins: [
308
+ blumeIncludePlugin(options),
309
+ ...blumeSharedMdastPlugins(options),
310
+ ],
300
311
  });
301
312
 
302
313
  export type BlumeMdxOptions = BlumeMarkdownOptions;
303
314
 
304
315
  /**
305
316
  * Sätteri MDX processor: Blume's feature set plus the MDAST plugins that target
306
- * components — `package-install` → package-manager tabs, `:::note` →
307
- * `<Callout>`, ` ```mermaid ` → a `<blume-mermaid>` element, and block math
317
+ * components — `package-install` → package-manager tabs, ` ```ts ts2js ` →
318
+ * TypeScript/JavaScript tabs, `:::note` → `<Callout>`, ` ```mermaid ` → a
319
+ * `<blume-mermaid>` element, and block math
308
320
  * (`$$…$$`) → the `<Math>` component. Used as the `processor` for
309
321
  * `@astrojs/mdx` so these apply to `.mdx` only (plain `.md` uses
310
322
  * {@link blumeMarkdownProcessor}).
@@ -319,15 +331,12 @@ export type BlumeMdxOptions = BlumeMarkdownOptions;
319
331
  */
320
332
  export const blumeMdxProcessor = (options: BlumeMdxOptions = {}) =>
321
333
  satteri({
322
- features: {
323
- ...FEATURES,
324
- directive: true,
325
- // Block-only: `$$…$$` parses, a bare `$` stays literal text.
326
- math: { singleDollarTextMath: false },
327
- },
334
+ features: { ...MDX_FEATURES },
328
335
  hastPlugins: blumeHastPlugins(options),
329
336
  mdastPlugins: [
337
+ blumeIncludePlugin(options),
330
338
  asMdastPlugin(packageInstallPlugin()),
339
+ asMdastPlugin(ts2jsPlugin()),
331
340
  asMdastPlugin(directiveToCalloutPlugin()),
332
341
  asMdastPlugin(mermaidPlugin()),
333
342
  asMdastPlugin(mathPlugin()),
@@ -15,7 +15,7 @@ import {
15
15
  siAstro,
16
16
  siC,
17
17
  siCplusplus,
18
- siCss3,
18
+ siCss,
19
19
  siDart,
20
20
  siDocker,
21
21
  siGnubash,
@@ -66,7 +66,7 @@ const LANGUAGE_ICONS: LanguageIcons = {
66
66
  "c++": siCplusplus,
67
67
  cjs: siJavascript,
68
68
  cpp: siCplusplus,
69
- css: siCss3,
69
+ css: siCss,
70
70
  cts: siTypescript,
71
71
  dart: siDart,
72
72
  docker: siDocker,
@@ -53,10 +53,14 @@ export const jsxTextElement = (
53
53
  children: MdastValue[] = []
54
54
  ) => ({ attributes, children, name, type: "mdxJsxTextElement" });
55
55
 
56
- /** Build a fenced code block node. */
57
- export const codeBlock = (lang: string, value: string) => ({
56
+ /** Build a fenced code block node, optionally with a fence-meta string. */
57
+ export const codeBlock = (
58
+ lang: string,
59
+ value: string,
60
+ meta: string | null = null
61
+ ) => ({
58
62
  lang,
59
- meta: null,
63
+ meta,
60
64
  type: "code",
61
65
  value,
62
66
  });
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Satteri MDAST plugin that turns a TypeScript fence carrying the `ts2js`
3
+ * meta keyword (```ts ts2js) into a `<Tabs>` group with the original
4
+ * TypeScript and an auto-generated JavaScript variant, so authors maintain
5
+ * one snippet and readers pick their dialect. The pair carries its own
6
+ * `syncKey`, so every ts2js pair on a page switches together without
7
+ * dragging along authored tab groups that happen to contain a tab titled
8
+ * "JavaScript".
9
+ *
10
+ * Types are stripped with Sucrase rather than the TypeScript compiler:
11
+ * `ts.transpileModule` re-prints the file (collapsed blank lines, four-space
12
+ * indentation, a `"use strict"` prologue on import-less snippets), while
13
+ * Sucrase erases type syntax token-by-token and leaves the author's
14
+ * formatting — and, crucially for {@link tidy}, the line structure — intact.
15
+ */
16
+
17
+ import { createRequire } from "node:module";
18
+
19
+ import type { transform } from "sucrase";
20
+
21
+ import { isLineRange, metaTokens } from "./fence-meta.ts";
22
+ import { codeBlock, jsxAttribute, jsxFlowElement } from "./mdast.ts";
23
+ import type { MdastNode, MdastVisitorContext } from "./mdast.ts";
24
+
25
+ interface CodeNode extends MdastNode {
26
+ lang?: string | null;
27
+ meta?: string | null;
28
+ value: string;
29
+ }
30
+
31
+ /** Re-join filtered meta tokens, or `null` when nothing survives. */
32
+ const joinMeta = (tokens: string[]): string | null =>
33
+ tokens.length > 0 ? tokens.join(" ") : null;
34
+
35
+ const require = createRequire(import.meta.url);
36
+
37
+ interface Sucrase {
38
+ transform: typeof transform;
39
+ }
40
+
41
+ /** How the plugin obtains Sucrase; injectable so tests can fail the load. */
42
+ export type SucraseLoader = () => Sucrase;
43
+
44
+ // SAFETY: this resolves Blume's own `sucrase` dependency, whose CJS entry
45
+ // exports the `transform` function the interface describes.
46
+ const defaultLoader: SucraseLoader = () => require("sucrase") as Sucrase;
47
+
48
+ /** Triggering fence languages, mapped to their generated tab's language. */
49
+ const JS_LANG = new Map([
50
+ ["ts", "js"],
51
+ ["tsx", "jsx"],
52
+ ["typescript", "js"],
53
+ ]);
54
+
55
+ /**
56
+ * Shiki's twoslash transformer triggers on this pattern over the *raw* fence
57
+ * meta (`RE_TWOSLASH` under `explicitTrigger`), so the keyword matches even
58
+ * inside a quoted attribute (`title="the twoslash guide"`). Mirror it
59
+ * exactly: any fence Shiki will twoslash-render is left alone, because hover
60
+ * data can't carry over to the generated JavaScript.
61
+ */
62
+ const TWOSLASH = /\btwoslash\b/u;
63
+
64
+ /** A line holding nothing but a Shiki notation comment (`// [!code …]`). */
65
+ const NOTATION_COMMENT = /^\s*\/\/\s*\[!code[^\]]*\]\s*$/u;
66
+
67
+ /** A line holding nothing but a `//` comment. */
68
+ const COMMENT_ONLY = /^\s*\/\//u;
69
+
70
+ const isBlank = (text: string): boolean => text.trim().length === 0;
71
+
72
+ interface TidyLine {
73
+ /** The line is blank because erasure removed its code. */
74
+ erased: boolean;
75
+ text: string;
76
+ }
77
+
78
+ /**
79
+ * Clean up Sucrase's erasure artifacts by comparing output to source line by
80
+ * line — erasure never adds or removes lines, so the two align (CRLF input
81
+ * included; the source's line endings are preserved). Untouched lines pass
82
+ * through verbatim, which keeps multi-line template literal interiors —
83
+ * whose blank runs and trailing spaces are string *content* — byte-exact.
84
+ * Lines erasure changed are cleaned:
85
+ *
86
+ * - the stray space a removed trailing type operator leaves before `;` (and
87
+ * any trailing whitespace) is dropped;
88
+ * - a comment stranded by its erased code — including a trailing Shiki
89
+ * notation marker (`// [!code highlight]`), which would otherwise
90
+ * re-anchor to the next line — is removed along with the code it
91
+ * annotated, as is an own-line notation marker whose target line was
92
+ * erased;
93
+ * - blank runs erasure created collapse: a run that is entirely erasure
94
+ * closes up, a run mixing erased and authored blanks keeps a single blank
95
+ * line, and either is dropped at the snippet's edges.
96
+ */
97
+ const tidy = (js: string, source: string): string => {
98
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
99
+ const srcLines = source.split(/\r?\n/u);
100
+ const srcLine = (index: number): string => srcLines[index] ?? "";
101
+ const lines = js.split(/\r?\n/u).map((line, index): TidyLine => {
102
+ const src = srcLine(index);
103
+ if (line === src) {
104
+ return { erased: false, text: line };
105
+ }
106
+ if (COMMENT_ONLY.test(line) && !COMMENT_ONLY.test(src)) {
107
+ return { erased: true, text: "" };
108
+ }
109
+ const text = line.replace(/[ \t]+;$/u, ";").replace(/[ \t]+$/u, "");
110
+ return { erased: text.length === 0, text };
111
+ });
112
+ // An own-line notation marker anchors to the line below it; if erasure
113
+ // blanked that target, the marker would highlight whatever ends up there
114
+ // instead, so it goes too.
115
+ for (const [index, line] of lines.entries()) {
116
+ const next = lines[index + 1];
117
+ if (
118
+ next &&
119
+ NOTATION_COMMENT.test(line.text) &&
120
+ isBlank(next.text) &&
121
+ !isBlank(srcLine(index + 1))
122
+ ) {
123
+ line.erased = true;
124
+ line.text = "";
125
+ }
126
+ }
127
+ const out: string[] = [];
128
+ let blankRun: TidyLine[] = [];
129
+ let sawCode = false;
130
+ const flushBlanks = (atEnd: boolean) => {
131
+ if (blankRun.some((line) => line.erased)) {
132
+ if (sawCode && !atEnd && blankRun.some((line) => !line.erased)) {
133
+ out.push("");
134
+ }
135
+ } else {
136
+ out.push(...blankRun.map((line) => line.text));
137
+ }
138
+ blankRun = [];
139
+ };
140
+ for (const line of lines) {
141
+ if (isBlank(line.text)) {
142
+ blankRun.push(line);
143
+ } else {
144
+ flushBlanks(false);
145
+ out.push(line.text);
146
+ sawCode = true;
147
+ }
148
+ }
149
+ flushBlanks(true);
150
+ return out.join(eol);
151
+ };
152
+
153
+ /** Build the `<Tab>` holding one dialect's code fence. */
154
+ const tabNode = (
155
+ title: string,
156
+ lang: string,
157
+ value: string,
158
+ meta: string | null
159
+ ) =>
160
+ jsxFlowElement(
161
+ "Tab",
162
+ [jsxAttribute("title", title)],
163
+ [codeBlock(lang, value, meta)]
164
+ );
165
+
166
+ /**
167
+ * The `ts2js` fence plugin. The generated JavaScript tab drops `{1,3-5}` line
168
+ * ranges from its meta (line numbers shift once types are gone) but keeps the
169
+ * rest (`title="..."`, `lineNumbers`); the TypeScript tab keeps everything.
170
+ * The keyword itself is stripped from both, which also guarantees the emitted
171
+ * fences can never re-trigger the plugin.
172
+ *
173
+ * The `loadSucrase` parameter exists for tests; production callers use the
174
+ * default, which resolves Blume's own dependency.
175
+ */
176
+ export const ts2jsPlugin = (loadSucrase: SucraseLoader = defaultLoader) => {
177
+ // Resolved lazily on the first triggered fence, so importing this module
178
+ // (at Astro config load) never pays Sucrase's parse cost for ts2js-free
179
+ // sites. `null` records a failed load: every fence degrades to its
180
+ // authored form instead of failing the build, and the load is never
181
+ // retried.
182
+ let sucrase: Sucrase | null | undefined;
183
+
184
+ const stripTypes = (code: string, lang: string): string | undefined => {
185
+ if (sucrase === undefined) {
186
+ try {
187
+ sucrase = loadSucrase();
188
+ } catch {
189
+ sucrase = null;
190
+ }
191
+ }
192
+ if (sucrase === null) {
193
+ return undefined;
194
+ }
195
+ try {
196
+ return tidy(
197
+ sucrase.transform(code, {
198
+ // Keep modern syntax (no downleveling) and the authored JSX.
199
+ disableESTransforms: true,
200
+ jsxRuntime: "preserve",
201
+ // An import kept only for the reader's context must survive:
202
+ // without this, Sucrase drops any import with no value-position
203
+ // reference, and the JavaScript tab would show code whose bindings
204
+ // are undefined when copied. Type-only imports (`import type`,
205
+ // `{ type T }`) are still elided.
206
+ keepUnusedImports: true,
207
+ // The `jsx` transform only for `tsx`: with it enabled, a `ts`
208
+ // angle-bracket assertion (`<number>value`) would parse as JSX and
209
+ // fail.
210
+ transforms: lang === "tsx" ? ["typescript", "jsx"] : ["typescript"],
211
+ }).code,
212
+ code
213
+ );
214
+ } catch {
215
+ // Sucrase throws on code it can't parse (pseudocode, deliberate
216
+ // fragments). Render the fence as authored instead of failing the
217
+ // build.
218
+ return undefined;
219
+ }
220
+ };
221
+
222
+ return {
223
+ code(node: CodeNode, ctx: MdastVisitorContext) {
224
+ const lang = node.lang ?? "";
225
+ const jsLang = JS_LANG.get(lang);
226
+ if (!jsLang) {
227
+ return;
228
+ }
229
+ const tokens = metaTokens(node.meta);
230
+ if (!tokens.includes("ts2js") || TWOSLASH.test(node.meta ?? "")) {
231
+ return;
232
+ }
233
+ const js = stripTypes(node.value, lang);
234
+ // Empty output means a types-only snippet; a blank JavaScript tab helps
235
+ // nobody, so keep the TypeScript fence as-is.
236
+ if (!js) {
237
+ return;
238
+ }
239
+ const kept = tokens.filter((token) => token !== "ts2js");
240
+ const jsMeta = kept.filter((token) => !isLineRange(token));
241
+ ctx.replaceNode(
242
+ node,
243
+ jsxFlowElement(
244
+ "Tabs",
245
+ [
246
+ // hash off: picking a dialect must not rewrite the page hash
247
+ // (clobbering the heading anchor the reader arrived with).
248
+ jsxAttribute("hash", "false"),
249
+ // Scope syncing to generated pairs: without a key, picking
250
+ // "JavaScript" here would also yank any authored group that has a
251
+ // same-titled tab (an SDK-language switcher, say) — one-way,
252
+ // since "TypeScript" wouldn't match to flip it back.
253
+ jsxAttribute("syncKey", "ts2js"),
254
+ ],
255
+ [
256
+ tabNode("TypeScript", lang, node.value, joinMeta(kept)),
257
+ tabNode("JavaScript", jsLang, js, joinMeta(jsMeta)),
258
+ ]
259
+ )
260
+ );
261
+ },
262
+ name: "blume-ts2js",
263
+ };
264
+ };
@@ -157,8 +157,11 @@ const inlineComponentRefs = (document: AsyncApiDocument): void => {
157
157
  const resolved = isObject(table)
158
158
  ? table[unescapePointer(groups.name ?? "")]
159
159
  : undefined;
160
+ // A shallow copy per alias: trait merging mutates its node in place
161
+ // (fields written, `traits` deleted), and two entries pointing at the
162
+ // same component — or the component table itself — must not share it.
160
163
  if (isObject(resolved) && !isString(resolved.$ref)) {
161
- map[id] = resolved;
164
+ map[id] = { ...resolved };
162
165
  }
163
166
  }
164
167
  }