blume 1.4.2 → 1.5.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 (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/index.js +2260 -1100
  3. package/dist/cli/index.js.map +123 -117
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/base-path.d.ts +8 -0
  6. package/dist/types/core/config-input.d.ts +87 -27
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +16 -1
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +116 -15
  12. package/dist/types/core/sources/types.d.ts +11 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +14 -7
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/docs/advanced/api-reference.mdx +8 -6
  19. package/docs/configuration/ai.mdx +26 -8
  20. package/docs/configuration/search.mdx +2 -0
  21. package/docs/configuration/seo.mdx +1 -1
  22. package/docs/content/i18n.mdx +1 -1
  23. package/docs/content/meta.mdx +2 -1
  24. package/docs/content/meta.ts +1 -0
  25. package/docs/content/navigation.mdx +35 -1
  26. package/docs/content/sources.mdx +1 -1
  27. package/docs/content/versioning.mdx +106 -0
  28. package/docs/reference/cli.mdx +1 -0
  29. package/docs/reference/frontmatter.mdx +3 -0
  30. package/package.json +13 -1
  31. package/skills/blume-migrate/SKILL.md +2 -2
  32. package/skills/blume-migrate/references/docusaurus.md +1 -1
  33. package/skills/blume-migrate/references/fumadocs.md +1 -1
  34. package/skills/blume-migrate/references/mintlify.md +1 -1
  35. package/src/ai/agent-readability.ts +40 -12
  36. package/src/ai/api-catalog.ts +2 -2
  37. package/src/ai/ask-context.ts +49 -12
  38. package/src/ai/ask.ts +10 -1
  39. package/src/ai/component-markdown.ts +80 -43
  40. package/src/ai/llms.ts +42 -17
  41. package/src/ai/mcp/data.ts +48 -12
  42. package/src/ai/mcp/discovery.ts +52 -16
  43. package/src/ai/mcp/server.ts +280 -125
  44. package/src/ai/mcp/tools.ts +3 -3
  45. package/src/ai/skills.ts +32 -9
  46. package/src/ai/tar.ts +29 -70
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +13 -5
  50. package/src/astro/generate.ts +113 -63
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +23 -12
  53. package/src/astro/templates.ts +185 -41
  54. package/src/audit/agent.ts +16 -31
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +78 -25
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +15 -5
  64. package/src/audit/snapshot.ts +29 -6
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +21 -21
  68. package/src/cli/commands/build.ts +30 -16
  69. package/src/cli/commands/dev.ts +15 -15
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eject.ts +4 -4
  72. package/src/cli/commands/eval.ts +24 -30
  73. package/src/cli/commands/init.ts +9 -9
  74. package/src/cli/commands/mcp-stdio.ts +3 -0
  75. package/src/cli/commands/translate.ts +14 -3
  76. package/src/cli/commands/version.ts +85 -0
  77. package/src/cli/dev-lock.ts +31 -10
  78. package/src/cli/eject-scripts.ts +17 -2
  79. package/src/cli/env.ts +13 -30
  80. package/src/cli/index.ts +2 -0
  81. package/src/cli/init/questions.ts +1 -1
  82. package/src/cli/init/scaffold.ts +41 -13
  83. package/src/cli/internal-error.ts +1 -0
  84. package/src/cli/report-format.ts +22 -0
  85. package/src/components/content/AccordionItem.astro +2 -9
  86. package/src/components/content/ColorItem.astro +5 -13
  87. package/src/components/content/Component.astro +12 -8
  88. package/src/components/content/Frame.astro +2 -12
  89. package/src/components/content/Prompt.astro +12 -31
  90. package/src/components/content/Tab.astro +2 -9
  91. package/src/components/content/Tooltip.astro +1 -9
  92. package/src/components/content/Update.astro +2 -9
  93. package/src/components/content/auto-type-table.ts +3 -0
  94. package/src/components/content/diff.ts +9 -5
  95. package/src/components/content/github-info.ts +2 -0
  96. package/src/components/content/inline-markdown.ts +28 -0
  97. package/src/components/copy-feedback.ts +96 -0
  98. package/src/components/islands/ask-ai.tsx +111 -34
  99. package/src/components/islands/hooks.ts +5 -1
  100. package/src/components/islands/webmcp.ts +49 -12
  101. package/src/components/layout/Header.astro +25 -1
  102. package/src/components/layout/NavSelector.astro +11 -2
  103. package/src/components/layout/NavTree.astro +4 -2
  104. package/src/components/layout/PageActions.astro +20 -32
  105. package/src/components/layout/PageLayout.astro +8 -28
  106. package/src/components/layout/RootLayout.astro +24 -48
  107. package/src/components/layout/Search.astro +133 -22
  108. package/src/components/layout/VersionBanner.astro +39 -0
  109. package/src/components/layout/analytics-client.ts +8 -5
  110. package/src/components/layout/drawer-inert.ts +31 -0
  111. package/src/components/layout/hydration-hint.ts +1 -1
  112. package/src/components/layout/nav-utils.ts +1 -4
  113. package/src/components/layout/overrides.ts +25 -12
  114. package/src/components/layout/search/algolia.ts +18 -5
  115. package/src/components/layout/search/endpoint.ts +3 -0
  116. package/src/components/layout/search/flexsearch.ts +23 -7
  117. package/src/components/layout/search/orama-cloud.ts +1 -1
  118. package/src/components/layout/search/orama.ts +4 -1
  119. package/src/components/layout/search/pagefind.ts +8 -5
  120. package/src/components/layout/search/types.ts +45 -1
  121. package/src/components/layout/search/typesense.ts +19 -3
  122. package/src/components/openapi/ApiOverview.astro +32 -6
  123. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  124. package/src/components/openapi/Bindings.astro +89 -0
  125. package/src/components/openapi/MethodBadge.astro +3 -0
  126. package/src/components/openapi/Operation.astro +7 -2
  127. package/src/components/openapi/PanelTabs.astro +131 -0
  128. package/src/components/openapi/ParametersTable.astro +2 -0
  129. package/src/components/openapi/RequestPanel.astro +12 -119
  130. package/src/components/openapi/async-snippets.ts +174 -0
  131. package/src/components/openapi/async.ts +348 -0
  132. package/src/components/openapi/helpers.ts +52 -20
  133. package/src/components/openapi/panel.ts +11 -8
  134. package/src/components/openapi/security.ts +102 -29
  135. package/src/components/openapi/snippets.ts +11 -11
  136. package/src/components/raf-throttle.ts +21 -0
  137. package/src/components/slug.ts +14 -0
  138. package/src/core/base-path.ts +18 -1
  139. package/src/core/component-overrides.ts +28 -23
  140. package/src/core/config-input.ts +96 -27
  141. package/src/core/config.ts +20 -7
  142. package/src/core/content.ts +3 -1
  143. package/src/core/data.ts +16 -1
  144. package/src/core/define-components.ts +5 -0
  145. package/src/core/diagnostics.ts +46 -38
  146. package/src/core/frontmatter.ts +74 -4
  147. package/src/core/graph.ts +137 -53
  148. package/src/core/i18n-ui.ts +15 -0
  149. package/src/core/i18n.ts +16 -8
  150. package/src/core/load-module.ts +1 -0
  151. package/src/core/manifest.ts +92 -3
  152. package/src/core/meta.ts +44 -14
  153. package/src/core/nav-diagnostics.ts +3 -3
  154. package/src/core/navigation.ts +247 -67
  155. package/src/core/probe.ts +7 -19
  156. package/src/core/project-graph.ts +27 -4
  157. package/src/core/schema.ts +219 -67
  158. package/src/core/site-url.ts +27 -0
  159. package/src/core/sources/assets.ts +2 -0
  160. package/src/core/sources/cache.ts +16 -8
  161. package/src/core/sources/github-releases.ts +39 -11
  162. package/src/core/sources/mdx-remote.ts +4 -0
  163. package/src/core/sources/normalize.ts +93 -22
  164. package/src/core/sources/notion.ts +76 -22
  165. package/src/core/sources/portable-text.ts +48 -12
  166. package/src/core/sources/resolve.ts +1 -0
  167. package/src/core/sources/sanity.ts +68 -14
  168. package/src/core/sources/types.ts +17 -1
  169. package/src/core/sources/watch.ts +1 -1
  170. package/src/core/standard-schema.ts +9 -3
  171. package/src/core/text-width.ts +26 -0
  172. package/src/core/tsconfig-aliases.ts +9 -5
  173. package/src/core/types.ts +45 -2
  174. package/src/core/ui-packs/index.ts +9 -1
  175. package/src/core/version-cut.ts +301 -0
  176. package/src/core/version.ts +2 -0
  177. package/src/core/versions.ts +170 -0
  178. package/src/deploy/adapter-output.ts +5 -2
  179. package/src/deploy/cloudflare-negotiation.ts +40 -11
  180. package/src/deploy/robots.ts +2 -1
  181. package/src/deploy/rss.ts +2 -1
  182. package/src/deploy/sitemap.ts +89 -8
  183. package/src/deploy/vercel-negotiation.ts +11 -4
  184. package/src/eval/agents.ts +13 -10
  185. package/src/eval/report.ts +5 -18
  186. package/src/eval/run.ts +2 -2
  187. package/src/eval/schema.ts +1 -1
  188. package/src/markdown/base-links.ts +6 -6
  189. package/src/markdown/directives.ts +7 -1
  190. package/src/markdown/heading-anchors.ts +17 -6
  191. package/src/markdown/index.ts +73 -24
  192. package/src/markdown/inline-code.ts +14 -2
  193. package/src/markdown/language-icon.ts +6 -2
  194. package/src/markdown/mdast.ts +18 -4
  195. package/src/markdown/package-commands.ts +63 -58
  196. package/src/markdown/table-wrap.ts +4 -1
  197. package/src/markdown/twoslash.ts +2 -0
  198. package/src/og/card.ts +50 -33
  199. package/src/og/derive.ts +43 -27
  200. package/src/openapi/asyncapi.ts +366 -0
  201. package/src/openapi/model.ts +135 -66
  202. package/src/openapi/parse.ts +166 -33
  203. package/src/openapi/references.ts +47 -22
  204. package/src/openapi/render-mdx.ts +137 -59
  205. package/src/openapi/scalar.ts +8 -10
  206. package/src/openapi/source.ts +126 -29
  207. package/src/registry/eject.ts +7 -2
  208. package/src/search/documents.ts +103 -39
  209. package/src/search/facets.ts +7 -5
  210. package/src/search/orama-index.ts +117 -32
  211. package/src/search/popular.ts +10 -5
  212. package/src/search/providers.ts +2 -2
  213. package/src/search/sync/index.ts +2 -0
  214. package/src/search/sync/typesense.ts +4 -2
  215. package/src/seo/jsonld.ts +24 -6
  216. package/src/seo/x-handle.ts +8 -3
  217. package/src/theme/chrome-icons.ts +7 -2
  218. package/src/theme/fonts.ts +8 -4
  219. package/src/theme/icons.ts +4 -2
  220. package/src/theme/palette.ts +27 -15
  221. package/src/translate/ledger.ts +4 -2
  222. package/src/translate/meta.ts +15 -6
  223. package/src/translate/report.ts +10 -19
  224. package/src/translate/run.ts +29 -38
  225. package/src/translate/validate.ts +52 -17
  226. package/src/translate/work-list.ts +0 -0
  227. package/src/cli/coalesce.ts +0 -43
@@ -0,0 +1,89 @@
1
+ ---
2
+ import type { BindingGroup } from "./async.ts";
3
+ import type { SchemaLike } from "./helpers.ts";
4
+ import SchemaTable from "./SchemaTable.astro";
5
+
6
+ /**
7
+ * Protocol binding fields as a key/value list, one group per protocol
8
+ * (`kafka`, `ws`, `mqtt`, …). Schema-shaped values — the ws binding's `query`
9
+ * and `headers` — render as nested schema tables; everything else as code.
10
+ */
11
+ interface Props {
12
+ title: string;
13
+ groups: BindingGroup[];
14
+ schemas: Record<string, SchemaLike>;
15
+ expandAll?: boolean;
16
+ }
17
+
18
+ const { title, groups, schemas, expandAll = false } = Astro.props;
19
+
20
+ // A bare `type` key is not enough — binding sub-objects carry protocol enums
21
+ // there (the AMQP exchange's `type: "topic"`), so only a JSON-Schema type
22
+ // value counts.
23
+ const SCHEMA_TYPES = new Set([
24
+ "array",
25
+ "boolean",
26
+ "integer",
27
+ "null",
28
+ "number",
29
+ "object",
30
+ "string",
31
+ ]);
32
+
33
+ const isSchemaish = (value: unknown): value is SchemaLike => {
34
+ if (typeof value !== "object" || value === null) {
35
+ return false;
36
+ }
37
+ if ("properties" in value || "$ref" in value) {
38
+ return true;
39
+ }
40
+ const { type } = value as { type?: unknown };
41
+ return typeof type === "string" && SCHEMA_TYPES.has(type);
42
+ };
43
+ ---
44
+
45
+ {
46
+ groups.length > 0 && (
47
+ <section class="mt-6">
48
+ <div
49
+ aria-level="2"
50
+ class="mb-2 font-semibold text-foreground text-sm"
51
+ role="heading"
52
+ >
53
+ {title}
54
+ </div>
55
+ {groups.map((group) => (
56
+ <div class="not-prose mb-3 rounded-blume border border-border px-4 last:mb-0">
57
+ <div class="flex items-baseline gap-2 border-border py-3">
58
+ <code class="font-mono font-semibold text-foreground text-sm">
59
+ {group.protocol}
60
+ </code>
61
+ </div>
62
+ {group.rows.map((row) => (
63
+ <div class="border-border border-t py-3">
64
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
65
+ <code class="font-mono text-foreground text-sm">{row.name}</code>
66
+ {!isSchemaish(row.value) && (
67
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground text-xs">
68
+ {typeof row.value === "string"
69
+ ? row.value
70
+ : JSON.stringify(row.value)}
71
+ </code>
72
+ )}
73
+ </div>
74
+ {isSchemaish(row.value) && (
75
+ <div class="mt-2">
76
+ <SchemaTable
77
+ expandAll={expandAll}
78
+ schema={row.value}
79
+ schemas={schemas}
80
+ />
81
+ </div>
82
+ )}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ ))}
87
+ </section>
88
+ )
89
+ }
@@ -15,6 +15,9 @@ const COLORS: Record<string, string> = {
15
15
  PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
16
16
  POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
17
17
  PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
18
+ // AsyncAPI actions.
19
+ RECEIVE: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
20
+ SEND: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
18
21
  };
19
22
  const color = COLORS[upper] ?? "bg-muted text-muted-foreground";
20
23
  ---
@@ -14,6 +14,7 @@ import {
14
14
  type SecuritySchemeLike,
15
15
  } from "./security.ts";
16
16
  import { buildRequestSample, sampleLanguages } from "./snippets.ts";
17
+ import AsyncApiOperation from "./AsyncApiOperation.astro";
17
18
  import Authorization from "./Authorization.astro";
18
19
  import MethodBadge from "./MethodBadge.astro";
19
20
  import ParametersTable from "./ParametersTable.astro";
@@ -57,6 +58,9 @@ interface FullOperation {
57
58
  const { source, id } = Astro.props;
58
59
  const spec = specs[source];
59
60
  const ref = spec?.operations[id];
61
+ // The AsyncAPI front-end renders its own body; the lookups below are
62
+ // OpenAPI-shaped (paths, request/response) and resolve to nothing for it.
63
+ const isAsyncApi = spec?.kind === "asyncapi";
60
64
 
61
65
  const doc = (spec?.document ?? {}) as {
62
66
  paths?: Record<
@@ -113,9 +117,10 @@ const sample =
113
117
  : null;
114
118
  const languages = sampleLanguages(spec?.codeSamples ?? []);
115
119
  ---
116
-
117
120
  {
118
- !(spec && ref && operation) ? (
121
+ isAsyncApi ? (
122
+ <AsyncApiOperation id={id} source={source} />
123
+ ) : !(spec && ref && operation) ? (
119
124
  <div class="text-muted-foreground">This API operation could not be found.</div>
120
125
  ) : (
121
126
  <div class="not-prose">
@@ -0,0 +1,131 @@
1
+ ---
2
+ /**
3
+ * The tabbed code-panel chrome shared by the request/response rail (OpenAPI)
4
+ * and the sample/message rail (AsyncAPI): a heading, a tab per panel, an
5
+ * optional copy button, and one visible panel at a time. Panels carry either
6
+ * highlighted `html` or plain fallback `text`. Behavior comes from the
7
+ * `<blume-panel-tabs>` custom element in `panel.ts`.
8
+ */
9
+ interface Panel {
10
+ key: string;
11
+ label: string;
12
+ /** Highlighted code HTML; null/undefined renders the `text` fallback. */
13
+ html?: string | null;
14
+ /** Plain-text fallback when there is nothing to highlight. */
15
+ text?: string;
16
+ }
17
+
18
+ interface Props {
19
+ heading: string;
20
+ panels: Panel[];
21
+ /** Show a copy button that copies the active panel's text. */
22
+ copy?: boolean;
23
+ /** Render tab labels in a monospace face (status codes). */
24
+ mono?: boolean;
25
+ }
26
+
27
+ const { heading, panels, copy = false, mono = false } = Astro.props;
28
+
29
+ // A stable per-instance id prefix so tab/tabpanel pairs can reference each
30
+ // other without colliding across the multiple PanelTabs on one page (the
31
+ // heading is unique per rail group; panel keys are unique within a group).
32
+ const NON_ID = /[^a-z0-9]+/gu;
33
+ const idFor = (kind: "tab" | "panel", key: string): string =>
34
+ ["panel-tabs", heading, kind, key]
35
+ .map((part) => part.toLowerCase().replace(NON_ID, "-"))
36
+ .join("-");
37
+
38
+ // A `.prose` wrapper gives Shiki its scoped token colors; the global style at
39
+ // the foot of this file strips the standalone code block's own box (border,
40
+ // injected copy button, language label) so the code sits flush inside the one
41
+ // panel border.
42
+ const CODE_WRAP = "prose max-w-none text-xs";
43
+ const TAB_CLASS =
44
+ "-mb-px cursor-pointer border-transparent border-b-2 bg-transparent py-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
45
+ ---
46
+
47
+ {
48
+ panels.length > 0 && (
49
+ <div>
50
+ <div
51
+ aria-level="3"
52
+ class="mb-2 font-semibold text-foreground text-sm"
53
+ role="heading"
54
+ >
55
+ {heading}
56
+ </div>
57
+ <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
58
+ <div class="flex items-center justify-between gap-2 border-border border-b px-3">
59
+ <div class="flex flex-wrap gap-4" role="tablist">
60
+ {panels.map((panel, index) => (
61
+ <button
62
+ aria-controls={idFor("panel", panel.key)}
63
+ aria-selected={index === 0 ? "true" : "false"}
64
+ class:list={[TAB_CLASS, mono && "font-mono"]}
65
+ data-panel-tab={panel.key}
66
+ id={idFor("tab", panel.key)}
67
+ role="tab"
68
+ type="button"
69
+ >
70
+ {panel.label}
71
+ </button>
72
+ ))}
73
+ </div>
74
+ {copy && (
75
+ <button
76
+ aria-label={`Copy ${heading.toLowerCase()}`}
77
+ class="group shrink-0 cursor-pointer rounded px-1.5 py-1 text-muted-foreground text-xs hover:text-foreground"
78
+ data-panel-copy
79
+ type="button"
80
+ >
81
+ <span class="group-data-[copied]:hidden">Copy</span>
82
+ <span class="hidden group-data-[copied]:inline">Copied</span>
83
+ </button>
84
+ )}
85
+ </div>
86
+ {panels.map((panel, index) => (
87
+ <div
88
+ aria-labelledby={idFor("tab", panel.key)}
89
+ class:list={[index === 0 ? "" : "hidden"]}
90
+ data-panel={panel.key}
91
+ id={idFor("panel", panel.key)}
92
+ role="tabpanel"
93
+ >
94
+ {panel.html ? (
95
+ <div class={CODE_WRAP}>
96
+ <Fragment set:html={panel.html} />
97
+ </div>
98
+ ) : (
99
+ <div class="px-3 py-4 text-muted-foreground text-xs">
100
+ {panel.text ?? ""}
101
+ </div>
102
+ )}
103
+ </div>
104
+ ))}
105
+ </blume-panel-tabs>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ <script>
111
+ import "./panel.ts";
112
+ </script>
113
+
114
+ <style is:global>
115
+ /* Strip the standalone code block's own chrome inside a panel: the border,
116
+ margin, radius, and the copy button + language label the prose code theme
117
+ adds — the panel supplies a single border and its own copy button. */
118
+ blume-panel-tabs pre.astro-code {
119
+ margin: 0 !important;
120
+ border: 0 !important;
121
+ border-radius: 0 !important;
122
+ background: transparent !important;
123
+ padding: 0.75rem 1rem !important;
124
+ }
125
+ blume-panel-tabs pre.astro-code::before {
126
+ content: none !important;
127
+ }
128
+ blume-panel-tabs [data-blume-copy] {
129
+ display: none !important;
130
+ }
131
+ </style>
@@ -27,6 +27,8 @@ const SECTIONS: { in: string; title: string }[] = [
27
27
  { in: "query", title: "Query parameters" },
28
28
  { in: "header", title: "Header parameters" },
29
29
  { in: "cookie", title: "Cookie parameters" },
30
+ // AsyncAPI channel address parameters.
31
+ { in: "channel", title: "Channel parameters" },
30
32
  ];
31
33
 
32
34
  const groups = SECTIONS.map((section) => ({
@@ -3,6 +3,7 @@ import data from "blume:data";
3
3
 
4
4
  import { highlightCode } from "../../markdown/index.ts";
5
5
  import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
6
+ import PanelTabs from "./PanelTabs.astro";
6
7
  import type { RequestSample, SampleLanguage } from "./snippets.ts";
7
8
 
8
9
  interface MediaTypeLike {
@@ -24,27 +25,18 @@ interface Props {
24
25
 
25
26
  const { sample, languages, responses, schemas } = Astro.props;
26
27
 
27
- // A `.prose` wrapper gives Shiki its scoped token colors; the global style at
28
- // the foot of this file strips the standalone code block's own box (border,
29
- // injected copy button, language label) so the code sits flush inside the one
30
- // panel border.
31
- const CODE_WRAP = "prose max-w-none text-xs";
32
- const TAB_CLASS =
33
- "-mb-px cursor-pointer border-transparent border-b-2 bg-transparent py-2 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
34
- const HEADING = "mb-2 font-semibold text-foreground text-sm";
35
-
36
- const requestSamples = await Promise.all(
28
+ const requestPanels = await Promise.all(
37
29
  languages.map(async (language) => ({
38
30
  html: await highlightCode(language.build(sample), language.lang, {
39
31
  icons: false,
40
32
  themes: data.config.codeThemes,
41
33
  }),
42
- id: language.id,
34
+ key: language.id,
43
35
  label: language.label,
44
36
  }))
45
37
  );
46
38
 
47
- const responseEntries = await Promise.all(
39
+ const responsePanels = await Promise.all(
48
40
  Object.entries(responses).map(async ([status, response]) => {
49
41
  const media =
50
42
  Object.entries(response.content ?? {}).find(([type]) =>
@@ -60,116 +52,17 @@ const responseEntries = await Promise.all(
60
52
  icons: false,
61
53
  themes: data.config.codeThemes,
62
54
  });
63
- return { description: response.description ?? "", html, status };
55
+ return {
56
+ html,
57
+ key: status,
58
+ label: status,
59
+ text: response.description || "No example response.",
60
+ };
64
61
  })
65
62
  );
66
63
  ---
67
64
 
68
65
  <div class="not-prose flex flex-col gap-6">
69
- {
70
- requestSamples.length > 0 && (
71
- <div>
72
- <div aria-level="3" class={HEADING} role="heading">
73
- Request
74
- </div>
75
- <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
76
- <div class="flex items-center justify-between gap-2 border-border border-b px-3">
77
- <div class="flex gap-4" role="tablist">
78
- {requestSamples.map((entry, index) => (
79
- <button
80
- aria-selected={index === 0 ? "true" : "false"}
81
- class={TAB_CLASS}
82
- data-panel-tab={entry.id}
83
- role="tab"
84
- type="button"
85
- >
86
- {entry.label}
87
- </button>
88
- ))}
89
- </div>
90
- <button
91
- aria-label="Copy request"
92
- class="group shrink-0 cursor-pointer rounded px-1.5 py-1 text-muted-foreground text-xs hover:text-foreground"
93
- data-panel-copy
94
- type="button"
95
- >
96
- <span class="group-data-[copied]:hidden">Copy</span>
97
- <span class="hidden group-data-[copied]:inline">Copied</span>
98
- </button>
99
- </div>
100
- {requestSamples.map((entry, index) => (
101
- <div
102
- class:list={[index === 0 ? "" : "hidden", CODE_WRAP]}
103
- data-panel={entry.id}
104
- >
105
- <Fragment set:html={entry.html} />
106
- </div>
107
- ))}
108
- </blume-panel-tabs>
109
- </div>
110
- )
111
- }
112
- {
113
- responseEntries.length > 0 && (
114
- <div>
115
- <div aria-level="3" class={HEADING} role="heading">
116
- Response
117
- </div>
118
- <blume-panel-tabs class="block overflow-hidden rounded-blume border border-border bg-background">
119
- <div class="flex flex-wrap gap-4 border-border border-b px-3" role="tablist">
120
- {responseEntries.map((entry, index) => (
121
- <button
122
- aria-selected={index === 0 ? "true" : "false"}
123
- class={`${TAB_CLASS} font-mono`}
124
- data-panel-tab={entry.status}
125
- role="tab"
126
- type="button"
127
- >
128
- {entry.status}
129
- </button>
130
- ))}
131
- </div>
132
- {responseEntries.map((entry, index) => (
133
- <div
134
- class:list={[index === 0 ? "" : "hidden"]}
135
- data-panel={entry.status}
136
- >
137
- {entry.html ? (
138
- <div class={CODE_WRAP}>
139
- <Fragment set:html={entry.html} />
140
- </div>
141
- ) : (
142
- <div class="px-3 py-4 text-muted-foreground text-xs">
143
- {entry.description || "No example response."}
144
- </div>
145
- )}
146
- </div>
147
- ))}
148
- </blume-panel-tabs>
149
- </div>
150
- )
151
- }
66
+ <PanelTabs copy heading="Request" panels={requestPanels} />
67
+ <PanelTabs heading="Response" mono panels={responsePanels} />
152
68
  </div>
153
-
154
- <script>
155
- import "./panel.ts";
156
- </script>
157
-
158
- <style is:global>
159
- /* Strip the standalone code block's own chrome inside a panel: the border,
160
- margin, radius, and the copy button + language label the prose code theme
161
- adds — the panel supplies a single border and its own copy button. */
162
- blume-panel-tabs pre.astro-code {
163
- margin: 0 !important;
164
- border: 0 !important;
165
- border-radius: 0 !important;
166
- background: transparent !important;
167
- padding: 0.75rem 1rem !important;
168
- }
169
- blume-panel-tabs pre.astro-code::before {
170
- content: none !important;
171
- }
172
- blume-panel-tabs [data-blume-copy] {
173
- display: none !important;
174
- }
175
- </style>
@@ -0,0 +1,174 @@
1
+ import type {
2
+ AsyncApiAction,
3
+ AsyncApiServerObject,
4
+ } from "../../openapi/asyncapi.ts";
5
+ import { toJson } from "./helpers.ts";
6
+
7
+ /**
8
+ * Protocol-aware code samples for AsyncAPI operations — the async counterpart
9
+ * of `snippets.ts`. Samples are written from the reader's side of the wire:
10
+ * a `receive` operation means the application receives, so the sample shows
11
+ * how to *produce* a message; a `send` operation shows how to consume one.
12
+ * Protocols without a supported tool yield no samples at all — the message
13
+ * example panel already shows the payload, and fabricating a client for an
14
+ * unknown binding would be worse than nothing.
15
+ */
16
+
17
+ /** Everything a snippet builder needs about one operation. */
18
+ export interface MessageSample {
19
+ action: AsyncApiAction;
20
+ /** Channel address, `{param}` templates left intact. */
21
+ address: string;
22
+ /** Example payload value (undefined when none could be derived). */
23
+ payload?: unknown;
24
+ /** First server the channel is available on, if any. */
25
+ server?: AsyncApiServerObject;
26
+ }
27
+
28
+ /** `host[:port]` split apart; MQTT tooling wants them as separate flags. */
29
+ const hostParts = (server?: AsyncApiServerObject) => {
30
+ const raw = server?.host ?? "localhost";
31
+ const colon = raw.lastIndexOf(":");
32
+ if (colon > 0 && /^\d+$/u.test(raw.slice(colon + 1))) {
33
+ return { host: raw.slice(0, colon), port: raw.slice(colon + 1) };
34
+ }
35
+ return { host: raw, port: undefined };
36
+ };
37
+
38
+ /** POSIX single-quote escaping, matching `snippets.ts`. */
39
+ const shellQuote = (text: string): string =>
40
+ `'${text.replaceAll("'", String.raw`'\''`)}'`;
41
+
42
+ const payloadJson = (sample: MessageSample): string =>
43
+ toJson(sample.payload ?? {});
44
+
45
+ /** Compact single-line payload for shell `-m`/`echo` arguments. */
46
+ const payloadInline = (sample: MessageSample): string =>
47
+ JSON.stringify(sample.payload ?? {});
48
+
49
+ /** `wss://host/path` for a WebSocket channel; the address is the path. */
50
+ const wsUrl = (sample: MessageSample): string => {
51
+ const { server } = sample;
52
+ const scheme = server?.protocol === "ws" ? "ws" : "wss";
53
+ const host = server?.host ?? "localhost";
54
+ const base = `${server?.pathname ?? ""}/${sample.address}`.replaceAll(
55
+ /\/+/gu,
56
+ "/"
57
+ );
58
+ return `${scheme}://${host}${base === "/" ? "" : base}`;
59
+ };
60
+
61
+ const wscatSnippet = (sample: MessageSample): string => {
62
+ const connect = `wscat -c ${shellQuote(wsUrl(sample))}`;
63
+ return sample.action === "receive"
64
+ ? `${connect}\n> ${payloadInline(sample)}`
65
+ : `# Prints each message as it arrives\n${connect}`;
66
+ };
67
+
68
+ const webSocketSnippet = (sample: MessageSample): string => {
69
+ const open = `const socket = new WebSocket(${JSON.stringify(wsUrl(sample))});`;
70
+ if (sample.action === "receive") {
71
+ return [
72
+ open,
73
+ "",
74
+ 'socket.addEventListener("open", () => {',
75
+ ` socket.send(JSON.stringify(${payloadJson(sample).replaceAll("\n", "\n ")}));`,
76
+ "});",
77
+ ].join("\n");
78
+ }
79
+ return [
80
+ open,
81
+ "",
82
+ 'socket.addEventListener("message", (event) => {',
83
+ " console.log(JSON.parse(event.data));",
84
+ "});",
85
+ ].join("\n");
86
+ };
87
+
88
+ const kcatSnippet = (sample: MessageSample): string => {
89
+ const broker = sample.server?.host ?? "localhost:9092";
90
+ const base = `kcat -b ${shellQuote(broker)} -t ${shellQuote(sample.address)}`;
91
+ return sample.action === "receive"
92
+ ? `echo ${shellQuote(payloadInline(sample))} | ${base} -P`
93
+ : `${base} -C`;
94
+ };
95
+
96
+ const mosquittoSnippet = (sample: MessageSample): string => {
97
+ const { host, port } = hostParts(sample.server);
98
+ const target = `-h ${shellQuote(host)}${port ? ` -p ${port}` : ""} -t ${shellQuote(sample.address)}`;
99
+ return sample.action === "receive"
100
+ ? `mosquitto_pub ${target} -m ${shellQuote(payloadInline(sample))}`
101
+ : `mosquitto_sub ${target} -v`;
102
+ };
103
+
104
+ /** One renderable sample tool: tab id/label, Shiki language, builder. */
105
+ export interface AsyncSampleLanguage {
106
+ id: string;
107
+ label: string;
108
+ lang: string;
109
+ build: (sample: MessageSample) => string;
110
+ }
111
+
112
+ const TOOLS = {
113
+ js: { build: webSocketSnippet, id: "js", label: "JavaScript", lang: "js" },
114
+ kcat: { build: kcatSnippet, id: "kcat", label: "kcat", lang: "bash" },
115
+ mosquitto: {
116
+ build: mosquittoSnippet,
117
+ id: "mosquitto",
118
+ label: "mosquitto",
119
+ lang: "bash",
120
+ },
121
+ wscat: { build: wscatSnippet, id: "wscat", label: "wscat", lang: "bash" },
122
+ } satisfies Record<string, AsyncSampleLanguage>;
123
+
124
+ type ToolId = keyof typeof TOOLS;
125
+
126
+ /** The tools appropriate to each protocol binding, in display order. */
127
+ const PROTOCOL_TOOLS = new Map<string, readonly ToolId[]>([
128
+ ["kafka", ["kcat"]],
129
+ ["mqtt", ["mosquitto"]],
130
+ ["ws", ["wscat", "js"]],
131
+ ]);
132
+
133
+ /** Accepted spellings for configured `codeSamples` ids. */
134
+ const ALIASES = new Map<string, ToolId>([
135
+ ["javascript", "js"],
136
+ ["kafkacat", "kcat"],
137
+ ["mosquitto_pub", "mosquitto"],
138
+ ["mosquitto_sub", "mosquitto"],
139
+ ["node", "js"],
140
+ ["typescript", "js"],
141
+ ["websocket", "js"],
142
+ ]);
143
+
144
+ /**
145
+ * The sample tools to render for an operation. The protocol picks the
146
+ * candidate set; a non-empty `codeSamples` config filters and orders it
147
+ * (unknown ids are dropped, aliases accepted). No protocol, an unsupported
148
+ * one, or a filter that matches nothing yields no samples.
149
+ */
150
+ export const asyncSampleLanguages = (
151
+ ids: string[],
152
+ protocol?: string
153
+ ): AsyncSampleLanguage[] => {
154
+ const candidates = PROTOCOL_TOOLS.get(protocol ?? "") ?? [];
155
+ if (candidates.length === 0) {
156
+ return [];
157
+ }
158
+ if (ids.length === 0) {
159
+ return candidates.map((id) => TOOLS[id]);
160
+ }
161
+ const chosen: AsyncSampleLanguage[] = [];
162
+ const seen = new Set<string>();
163
+ for (const raw of ids) {
164
+ // Case-insensitive like `sampleLanguages` — the docs spell the tools
165
+ // `WebSocket`/`mosquitto_pub`, so configured ids arrive in any casing.
166
+ const id = ALIASES.get(raw.toLowerCase()) ?? raw.toLowerCase();
167
+ const match = candidates.find((candidate) => candidate === id);
168
+ if (match && !seen.has(match)) {
169
+ seen.add(match);
170
+ chosen.push(TOOLS[match]);
171
+ }
172
+ }
173
+ return chosen;
174
+ };