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.
- package/CHANGELOG.md +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ComponentOverride,
|
|
3
|
+
IslandDescriptor,
|
|
4
|
+
} from "../../core/define-components.ts";
|
|
5
|
+
|
|
6
|
+
/** A leftover path string an override resolved to (see `resolveSlot`). */
|
|
7
|
+
const isPathString = (override: ComponentOverride): override is string =>
|
|
8
|
+
typeof override === "string";
|
|
9
|
+
|
|
10
|
+
/** An `IslandDescriptor` whose `component` is actually present. */
|
|
11
|
+
const isResolvedIsland = (
|
|
12
|
+
override: ComponentOverride
|
|
13
|
+
): override is IslandDescriptor =>
|
|
14
|
+
typeof override === "object" &&
|
|
15
|
+
override !== null &&
|
|
16
|
+
"component" in override &&
|
|
17
|
+
override.component !== undefined &&
|
|
18
|
+
override.component !== null;
|
|
2
19
|
|
|
3
20
|
/**
|
|
4
21
|
* Resolve a layout-slot override to the component Astro should render, falling
|
|
@@ -15,20 +32,16 @@ export const resolveSlot = <T>(
|
|
|
15
32
|
override: ComponentOverride | undefined,
|
|
16
33
|
fallback: T
|
|
17
34
|
): T => {
|
|
18
|
-
if (
|
|
19
|
-
override === undefined ||
|
|
20
|
-
override === null ||
|
|
21
|
-
typeof override === "string"
|
|
22
|
-
) {
|
|
35
|
+
if (override === undefined || override === null || isPathString(override)) {
|
|
23
36
|
return fallback;
|
|
24
37
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
override.component !== null
|
|
30
|
-
) {
|
|
38
|
+
if (isResolvedIsland(override)) {
|
|
39
|
+
// SAFETY: `ComponentReference` is untyped (`unknown`); the generated
|
|
40
|
+
// components map stores real components for this slot, so the descriptor's
|
|
41
|
+
// component is renderable as the slot's component type.
|
|
31
42
|
return override.component as T;
|
|
32
43
|
}
|
|
44
|
+
// SAFETY: same untyped `ComponentReference` — a bare value here is the
|
|
45
|
+
// imported component the config referenced for this slot.
|
|
33
46
|
return override as T;
|
|
34
47
|
};
|
|
@@ -8,6 +8,7 @@ interface AlgoliaRecord {
|
|
|
8
8
|
title: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
content?: string;
|
|
11
|
+
version?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -27,15 +28,24 @@ export const createSearch = (opts: {
|
|
|
27
28
|
hitsPerPage: SEARCH_LIMIT,
|
|
28
29
|
indexName: opts.indexName,
|
|
29
30
|
query,
|
|
30
|
-
// The sync uploads `locale` on every record so
|
|
31
|
-
// scope hosted results to the active language
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// The sync uploads `locale` and `version` on every record so a
|
|
32
|
+
// site can scope hosted results to the active language and the
|
|
33
|
+
// viewed docs version (the current docs upload as "current").
|
|
34
|
+
...(() => {
|
|
35
|
+
const facetFilters = [
|
|
36
|
+
...(options?.locale ? [`locale:${options.locale}`] : []),
|
|
37
|
+
...(options?.version === undefined
|
|
38
|
+
? []
|
|
39
|
+
: [`version:${options.version || "current"}`]),
|
|
40
|
+
];
|
|
41
|
+
return facetFilters.length > 0 ? { facetFilters } : {};
|
|
42
|
+
})(),
|
|
35
43
|
},
|
|
36
44
|
],
|
|
37
45
|
});
|
|
38
46
|
const [first] = results;
|
|
47
|
+
// SAFETY: the build-time sync uploads every record in the AlgoliaRecord
|
|
48
|
+
// shape, so hits returned by that index carry those fields.
|
|
39
49
|
const records =
|
|
40
50
|
first && "hits" in first ? (first.hits as AlgoliaRecord[]) : [];
|
|
41
51
|
const hits = records.map((record) => ({
|
|
@@ -46,6 +56,9 @@ export const createSearch = (opts: {
|
|
|
46
56
|
),
|
|
47
57
|
title: highlight(record.title, query),
|
|
48
58
|
url: record.url,
|
|
59
|
+
// Records store the current docs' version as "current" (hosted backends
|
|
60
|
+
// treat empty facet values unreliably); the hit contract uses "".
|
|
61
|
+
version: record.version === "current" ? "" : record.version,
|
|
49
62
|
}));
|
|
50
63
|
return { hits, sections: [] };
|
|
51
64
|
};
|
|
@@ -18,6 +18,9 @@ export const createSearch =
|
|
|
18
18
|
if (!response.ok) {
|
|
19
19
|
return { hits: [], sections: [] };
|
|
20
20
|
}
|
|
21
|
+
// SAFETY: the endpoint is Blume-generated (`search-endpoint` template) and
|
|
22
|
+
// responds with the SearchHit list it built; title/excerpt are still
|
|
23
|
+
// escaped below before the dialog injects them as HTML.
|
|
21
24
|
const records = (await response.json()) as SearchHit[];
|
|
22
25
|
const hits = records.slice(0, SEARCH_LIMIT).map((hit) => ({
|
|
23
26
|
...hit,
|
|
@@ -3,6 +3,16 @@ import { Document } from "flexsearch";
|
|
|
3
3
|
import { buildResult, RESULT_POOL } from "./types.ts";
|
|
4
4
|
import type { IndexedDocument, SearchFn } from "./types.ts";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The fields FlexSearch indexes. An anonymous alias of the indexed fields:
|
|
8
|
+
* unlike the `IndexedDocument` interface it satisfies FlexSearch's
|
|
9
|
+
* index-signature `DocumentData` constraint structurally.
|
|
10
|
+
*/
|
|
11
|
+
type SearchDocument = Pick<
|
|
12
|
+
IndexedDocument,
|
|
13
|
+
"content" | "description" | "route" | "title"
|
|
14
|
+
>;
|
|
15
|
+
|
|
6
16
|
/**
|
|
7
17
|
* FlexSearch: reuse the same static `blume-search.json` Orama ships, but build
|
|
8
18
|
* a FlexSearch document index in the browser. Keyless; works in dev and build.
|
|
@@ -13,17 +23,17 @@ export const createSearch = async (opts: {
|
|
|
13
23
|
indexUrl: string;
|
|
14
24
|
}): Promise<SearchFn> => {
|
|
15
25
|
const response = await fetch(opts.indexUrl);
|
|
26
|
+
// SAFETY: `blume-search.json` is generated by our own search indexer, which
|
|
27
|
+
// writes exactly this document shape.
|
|
16
28
|
const documents = (await response.json()) as IndexedDocument[];
|
|
17
29
|
const byRoute = new Map(documents.map((doc) => [doc.route, doc]));
|
|
18
30
|
|
|
19
|
-
const index = new Document({
|
|
31
|
+
const index = new Document<SearchDocument>({
|
|
20
32
|
document: { id: "route", index: ["title", "description", "content"] },
|
|
21
33
|
tokenize: "forward",
|
|
22
34
|
});
|
|
23
35
|
for (const doc of documents) {
|
|
24
|
-
|
|
25
|
-
// record satisfies it structurally but TS needs the cast.
|
|
26
|
-
index.add(doc as unknown as Record<string, string>);
|
|
36
|
+
index.add(doc);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
// FlexSearch's in-memory search is synchronous, so the SearchFn resolves
|
|
@@ -40,9 +50,15 @@ export const createSearch = async (opts: {
|
|
|
40
50
|
}
|
|
41
51
|
seen.add(route);
|
|
42
52
|
const doc = byRoute.get(route);
|
|
43
|
-
// Filter to the active locale (when
|
|
44
|
-
// so section counts and results stay within
|
|
45
|
-
|
|
53
|
+
// Filter to the active locale and version (when requested) before
|
|
54
|
+
// shaping, so section counts and results stay within scope. `""` is
|
|
55
|
+
// the current version, so version presence is tested explicitly.
|
|
56
|
+
if (
|
|
57
|
+
doc &&
|
|
58
|
+
(!options?.locale || doc.locale === options.locale) &&
|
|
59
|
+
(options?.version === undefined ||
|
|
60
|
+
(doc.version ?? "") === options.version)
|
|
61
|
+
) {
|
|
46
62
|
matched.push(doc);
|
|
47
63
|
}
|
|
48
64
|
}
|
|
@@ -31,7 +31,7 @@ export const createSearch = (opts: {
|
|
|
31
31
|
...(options?.locale && { where: { locale: options.locale } }),
|
|
32
32
|
});
|
|
33
33
|
const hits = (results?.hits ?? []).map((hit) => {
|
|
34
|
-
const doc = hit.document
|
|
34
|
+
const doc: OramaCloudRecord = hit.document;
|
|
35
35
|
return {
|
|
36
36
|
content: doc.content ?? "",
|
|
37
37
|
excerpt: highlight(
|
|
@@ -19,13 +19,16 @@ export const createSearch = async (opts: {
|
|
|
19
19
|
locale?: string;
|
|
20
20
|
}): Promise<SearchFn> => {
|
|
21
21
|
const response = await fetch(opts.indexUrl);
|
|
22
|
+
// SAFETY: `blume-search.json` is generated by our own indexer
|
|
23
|
+
// (`buildSearchDocuments`), which writes exactly this document shape.
|
|
22
24
|
const documents = (await response.json()) as IndexedDocument[];
|
|
23
25
|
const db = await buildOramaIndex(documents, opts.locale);
|
|
24
26
|
|
|
25
27
|
return async (query, options) => {
|
|
26
28
|
const docs = await queryOramaIndex(db, query, RESULT_POOL, {
|
|
27
29
|
locale: options?.locale,
|
|
30
|
+
version: options?.version,
|
|
28
31
|
});
|
|
29
|
-
return buildResult(docs
|
|
32
|
+
return buildResult(docs, query, options?.section);
|
|
30
33
|
};
|
|
31
34
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { highlight, SEARCH_LIMIT } from "./types.ts";
|
|
1
|
+
import { highlight, sanitizeExcerpt, SEARCH_LIMIT } from "./types.ts";
|
|
2
2
|
import type { SearchFn } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
interface PagefindResult {
|
|
@@ -23,21 +23,24 @@ export const createSearch = async (opts: {
|
|
|
23
23
|
}): Promise<SearchFn> => {
|
|
24
24
|
// The pagefind bundle lives in the built site (not node_modules) and is
|
|
25
25
|
// resolved at runtime by URL — it can't be a static, code-splittable path.
|
|
26
|
+
// SAFETY: the URL points at the `pagefind.js` module our own build emitted,
|
|
27
|
+
// whose export contract (`search()`) is fixed by pagefind.
|
|
26
28
|
// oxlint-disable-next-line react-doctor/no-dynamic-import-path
|
|
27
29
|
const pagefind = (await import(
|
|
28
30
|
/* @vite-ignore */
|
|
29
31
|
opts.url
|
|
30
32
|
)) as PagefindModule;
|
|
31
|
-
// Pagefind builds its own marked-up excerpt; we keep
|
|
32
|
-
//
|
|
33
|
-
// the
|
|
33
|
+
// Pagefind builds its own marked-up excerpt; we keep its `<mark>` highlights
|
|
34
|
+
// (dropping any other markup — the excerpt is rendered via innerHTML) and
|
|
35
|
+
// only highlight the title ourselves. It carries no section/breadcrumb data,
|
|
36
|
+
// so pills stay hidden and the preview pane falls back to the excerpt.
|
|
34
37
|
return async (query) => {
|
|
35
38
|
const response = await pagefind.search(query);
|
|
36
39
|
const docs = await Promise.all(
|
|
37
40
|
response.results.slice(0, SEARCH_LIMIT).map((result) => result.data())
|
|
38
41
|
);
|
|
39
42
|
const hits = docs.map((doc) => ({
|
|
40
|
-
excerpt: doc.excerpt,
|
|
43
|
+
excerpt: sanitizeExcerpt(doc.excerpt),
|
|
41
44
|
title: highlight(doc.meta?.title ?? doc.url, query),
|
|
42
45
|
url: doc.url,
|
|
43
46
|
}));
|
|
@@ -13,6 +13,11 @@ export interface SearchHit {
|
|
|
13
13
|
section?: string;
|
|
14
14
|
/** Plain-text page content, used to render the preview pane. */
|
|
15
15
|
content?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Docs version the hit belongs to (`""` = current). Local indexes and the
|
|
18
|
+
* hosted Algolia/Typesense adapters set it; other providers leave it unset.
|
|
19
|
+
*/
|
|
20
|
+
version?: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/** A category pill with its result count. */
|
|
@@ -30,7 +35,12 @@ export interface SearchResult {
|
|
|
30
35
|
/** A configured query function — the common contract every provider returns. */
|
|
31
36
|
export type SearchFn = (
|
|
32
37
|
query: string,
|
|
33
|
-
options?: {
|
|
38
|
+
options?: {
|
|
39
|
+
section?: string;
|
|
40
|
+
locale?: string;
|
|
41
|
+
/** Docs version to scope to (`""` = current); omitted disables it. */
|
|
42
|
+
version?: string;
|
|
43
|
+
}
|
|
34
44
|
) => Promise<SearchResult>;
|
|
35
45
|
|
|
36
46
|
/** A document in the client-loaded `blume-search.json` index. */
|
|
@@ -42,6 +52,7 @@ export interface IndexedDocument {
|
|
|
42
52
|
breadcrumb?: string[];
|
|
43
53
|
section?: string;
|
|
44
54
|
locale?: string;
|
|
55
|
+
version?: string;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/** Max results surfaced in the dialog. */
|
|
@@ -84,6 +95,38 @@ export const highlight = (text: string, query: string): string => {
|
|
|
84
95
|
.join("");
|
|
85
96
|
};
|
|
86
97
|
|
|
98
|
+
// Either a tag-shaped run — an opening `<` with a letter or `/` after it,
|
|
99
|
+
// through the closing `>` (or end of string for an unterminated tag) — or a
|
|
100
|
+
// lone `<`. A run can't span a later `<` (`[^<>]`), so between the two
|
|
101
|
+
// alternatives every `<` in the input lands inside a captured run.
|
|
102
|
+
const ANGLE_RUN = /(?<run><\/?[a-z][^<>]*>?|<)/iu;
|
|
103
|
+
const BARE_MARK = /^<\/?mark>$/iu;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reduce provider-supplied excerpt markup to the `<mark>` highlighting the
|
|
107
|
+
* dialog expects. Remote excerpts (Pagefind's index, hosted engines) are
|
|
108
|
+
* rendered via `innerHTML`, so the output alphabet is pinned: bare
|
|
109
|
+
* `<mark>`/`</mark>` tags (attributes make even a mark untrusted), text, and
|
|
110
|
+
* entities. Tag-shaped runs are dropped; every other `<` is escaped, which
|
|
111
|
+
* renders identically but can't be parsed as markup (`<!--` would otherwise
|
|
112
|
+
* open a comment in `innerHTML` and swallow the rest of the excerpt). Split on
|
|
113
|
+
* runs covering every `<` rather than deleting tags in place: a deletion can
|
|
114
|
+
* splice the text around it into a fresh tag (`<<b>script>` → `<script>`),
|
|
115
|
+
* while here no `<` survives outside a run, so the only ones emitted are the
|
|
116
|
+
* bare mark tags. String-level on purpose: this also runs under DOM-less
|
|
117
|
+
* tests, where DOMPurify/DOMParser don't exist.
|
|
118
|
+
*/
|
|
119
|
+
export const sanitizeExcerpt = (html: string): string =>
|
|
120
|
+
html
|
|
121
|
+
.split(ANGLE_RUN)
|
|
122
|
+
.map((part, index) => {
|
|
123
|
+
if (index % 2 === 0 || BARE_MARK.test(part)) {
|
|
124
|
+
return part;
|
|
125
|
+
}
|
|
126
|
+
return part === "<" ? "<" : "";
|
|
127
|
+
})
|
|
128
|
+
.join("");
|
|
129
|
+
|
|
87
130
|
/** First index in `text` where any query token matches (case-insensitive). */
|
|
88
131
|
const matchIndex = (text: string, query: string): number => {
|
|
89
132
|
const tokens = queryTokens(query);
|
|
@@ -161,6 +204,7 @@ export const buildResult = (
|
|
|
161
204
|
section: doc.section ?? "",
|
|
162
205
|
title: highlight(doc.title, query),
|
|
163
206
|
url: doc.route,
|
|
207
|
+
version: doc.version,
|
|
164
208
|
}));
|
|
165
209
|
return { hits, sections };
|
|
166
210
|
};
|
|
@@ -8,6 +8,7 @@ interface TypesenseRecord extends Record<string, unknown> {
|
|
|
8
8
|
title: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
content?: string;
|
|
11
|
+
version?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -40,9 +41,20 @@ export const createSearch = (opts: {
|
|
|
40
41
|
per_page: SEARCH_LIMIT,
|
|
41
42
|
q: query,
|
|
42
43
|
query_by: "title,description,content",
|
|
43
|
-
// The sync marks `locale` as
|
|
44
|
-
//
|
|
45
|
-
|
|
44
|
+
// The sync marks `locale` and `version` as facets so hosted results
|
|
45
|
+
// scope to the active language and the viewed docs version (the
|
|
46
|
+
// current docs upload as "current").
|
|
47
|
+
...(() => {
|
|
48
|
+
const clauses = [
|
|
49
|
+
...(options?.locale ? [`locale:=${options.locale}`] : []),
|
|
50
|
+
...(options?.version === undefined
|
|
51
|
+
? []
|
|
52
|
+
: [`version:=${options.version || "current"}`]),
|
|
53
|
+
];
|
|
54
|
+
return clauses.length > 0
|
|
55
|
+
? { filter_by: clauses.join(" && ") }
|
|
56
|
+
: {};
|
|
57
|
+
})(),
|
|
46
58
|
},
|
|
47
59
|
{}
|
|
48
60
|
);
|
|
@@ -56,6 +68,10 @@ export const createSearch = (opts: {
|
|
|
56
68
|
),
|
|
57
69
|
title: highlight(doc.title, query),
|
|
58
70
|
url: doc.url,
|
|
71
|
+
// Records store the current docs' version as "current" (hosted
|
|
72
|
+
// backends treat empty facet values unreliably); the hit contract
|
|
73
|
+
// uses "".
|
|
74
|
+
version: doc.version === "current" ? "" : doc.version,
|
|
59
75
|
};
|
|
60
76
|
});
|
|
61
77
|
return { hits, sections: [] };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
3
|
|
|
4
|
+
import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
|
|
5
|
+
|
|
4
6
|
// The spec-level metadata block (version + base URLs) at the top of an API
|
|
5
7
|
// overview page. The tag sections that follow are emitted by `overviewMdx` as
|
|
6
8
|
// markdown headings plus `<ApiTagOperations>` lists, so they land in the
|
|
@@ -11,8 +13,32 @@ interface Props {
|
|
|
11
13
|
|
|
12
14
|
const { source } = Astro.props;
|
|
13
15
|
const spec = specs[source];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
// OpenAPI declares `servers` as an array of URLs; AsyncAPI as a named map of
|
|
18
|
+
// host/protocol/pathname. Both flatten into one list of address chips.
|
|
19
|
+
const addresses: string[] = [];
|
|
20
|
+
if (spec?.kind === "asyncapi") {
|
|
21
|
+
const servers = (spec.document as AsyncApiDocument).servers ?? {};
|
|
22
|
+
for (const server of Object.values(servers)) {
|
|
23
|
+
if (server?.host) {
|
|
24
|
+
addresses.push(
|
|
25
|
+
`${server.protocol ? `${server.protocol}://` : ""}${server.host}${server.pathname ?? ""}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// Hand-written specs sometimes declare `servers` as a bare object; degrade
|
|
31
|
+
// to no address chips instead of throwing mid-build.
|
|
32
|
+
const declared = ((spec?.document ?? {}) as { servers?: { url?: string }[] })
|
|
33
|
+
.servers;
|
|
34
|
+
const servers = Array.isArray(declared) ? declared : [];
|
|
35
|
+
for (const server of servers) {
|
|
36
|
+
if (server.url) {
|
|
37
|
+
addresses.push(server.url);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const addressLabel = spec?.kind === "asyncapi" ? "Servers" : "Base URL";
|
|
16
42
|
---
|
|
17
43
|
|
|
18
44
|
{
|
|
@@ -23,12 +49,12 @@ const servers =
|
|
|
23
49
|
Version {spec.version}
|
|
24
50
|
</div>
|
|
25
51
|
)}
|
|
26
|
-
{
|
|
52
|
+
{addresses.length > 0 && (
|
|
27
53
|
<div class="not-prose mb-8 flex flex-wrap items-center gap-2">
|
|
28
|
-
<span class="text-muted-foreground text-xs">
|
|
29
|
-
{
|
|
54
|
+
<span class="text-muted-foreground text-xs">{addressLabel}</span>
|
|
55
|
+
{addresses.map((address) => (
|
|
30
56
|
<code class="rounded bg-muted px-2 py-0.5 text-foreground text-xs">
|
|
31
|
-
{
|
|
57
|
+
{address}
|
|
32
58
|
</code>
|
|
33
59
|
))}
|
|
34
60
|
</div>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
import data from "blume:data";
|
|
3
|
+
import specs from "blume:openapi";
|
|
4
|
+
|
|
5
|
+
import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
|
|
6
|
+
import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
|
|
7
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
asyncApiSecurityEntries,
|
|
10
|
+
bindingGroups,
|
|
11
|
+
channelParameters,
|
|
12
|
+
channelServers,
|
|
13
|
+
messageLabel,
|
|
14
|
+
operationMessages,
|
|
15
|
+
payloadSchema,
|
|
16
|
+
protocolOf,
|
|
17
|
+
schemaOf,
|
|
18
|
+
} from "./async.ts";
|
|
19
|
+
import type { MessageSample } from "./async-snippets.ts";
|
|
20
|
+
import { asyncSampleLanguages } from "./async-snippets.ts";
|
|
21
|
+
import Authorization from "./Authorization.astro";
|
|
22
|
+
import Bindings from "./Bindings.astro";
|
|
23
|
+
import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
|
|
24
|
+
import MethodBadge from "./MethodBadge.astro";
|
|
25
|
+
import PanelTabs from "./PanelTabs.astro";
|
|
26
|
+
import ParametersTable from "./ParametersTable.astro";
|
|
27
|
+
import SchemaTable from "./SchemaTable.astro";
|
|
28
|
+
import type { SecuritySchemeLike } from "./security.ts";
|
|
29
|
+
import { resolveAsyncApiSecurity } from "./security.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The AsyncAPI front-end of the operation page: message payloads instead of
|
|
33
|
+
* request/response, channel parameters instead of path/query, protocol
|
|
34
|
+
* bindings, and binding-aware samples in the right rail. Everything below the
|
|
35
|
+
* dispatch — schema tables, parameter rows, authorization, the tabbed panels —
|
|
36
|
+
* is the same component set the OpenAPI operation renders.
|
|
37
|
+
*/
|
|
38
|
+
interface Props {
|
|
39
|
+
source: string;
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { source, id } = Astro.props;
|
|
44
|
+
const spec = specs[source];
|
|
45
|
+
const ref = spec?.operations[id];
|
|
46
|
+
const document = (spec?.document ?? {}) as AsyncApiDocument;
|
|
47
|
+
const operation = ref ? asyncApiOperationObject(document, ref) : undefined;
|
|
48
|
+
const channel = ref?.channelId
|
|
49
|
+
? document.channels?.[ref.channelId]
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
const schemas = (document.components?.schemas ?? {}) as Record<
|
|
53
|
+
string,
|
|
54
|
+
SchemaLike
|
|
55
|
+
>;
|
|
56
|
+
const servers = channelServers(channel, document);
|
|
57
|
+
const security = resolveAsyncApiSecurity(
|
|
58
|
+
asyncApiSecurityEntries(operation, servers),
|
|
59
|
+
document.components?.securitySchemes as
|
|
60
|
+
| Record<string, SecuritySchemeLike>
|
|
61
|
+
| undefined
|
|
62
|
+
);
|
|
63
|
+
const parameters = channelParameters(channel, document);
|
|
64
|
+
const messages = operation ? operationMessages(operation, channel, document) : [];
|
|
65
|
+
const protocol = protocolOf(operation, channel, servers);
|
|
66
|
+
|
|
67
|
+
/** Declared example first, else a sampled value from the payload schema. */
|
|
68
|
+
const exampleOf = (message: (typeof messages)[number]["message"]): unknown =>
|
|
69
|
+
message.examples?.[0]?.payload ?? exampleValue(payloadSchema(message), schemas);
|
|
70
|
+
|
|
71
|
+
const messagePanels = await Promise.all(
|
|
72
|
+
messages.map(async (named, index) => {
|
|
73
|
+
const example = exampleOf(named.message);
|
|
74
|
+
return {
|
|
75
|
+
html:
|
|
76
|
+
example === undefined || example === null
|
|
77
|
+
? null
|
|
78
|
+
: await highlightCode(toJson(example), "json", {
|
|
79
|
+
icons: false,
|
|
80
|
+
themes: data.config.codeThemes,
|
|
81
|
+
}),
|
|
82
|
+
key: `message-${index}`,
|
|
83
|
+
label: messageLabel(named),
|
|
84
|
+
text: named.message.summary || "No example payload.",
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const sample: MessageSample | null = ref
|
|
90
|
+
? {
|
|
91
|
+
action: ref.method === "send" ? "send" : "receive",
|
|
92
|
+
address: ref.path,
|
|
93
|
+
payload: messages.length > 0 ? exampleOf(messages[0].message) : undefined,
|
|
94
|
+
server: servers[0],
|
|
95
|
+
}
|
|
96
|
+
: null;
|
|
97
|
+
const languages = asyncSampleLanguages(spec?.codeSamples ?? [], protocol);
|
|
98
|
+
const samplePanels = sample
|
|
99
|
+
? await Promise.all(
|
|
100
|
+
languages.map(async (language) => ({
|
|
101
|
+
html: await highlightCode(language.build(sample), language.lang, {
|
|
102
|
+
icons: false,
|
|
103
|
+
themes: data.config.codeThemes,
|
|
104
|
+
}),
|
|
105
|
+
key: language.id,
|
|
106
|
+
label: language.label,
|
|
107
|
+
}))
|
|
108
|
+
)
|
|
109
|
+
: [];
|
|
110
|
+
|
|
111
|
+
const operationBindings = bindingGroups(operation?.bindings);
|
|
112
|
+
const channelBindings = bindingGroups(channel?.bindings);
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
!(spec && ref && operation) ? (
|
|
117
|
+
<div class="text-muted-foreground">This API operation could not be found.</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div class="not-prose">
|
|
120
|
+
<div class="mb-6 flex flex-wrap items-center gap-3">
|
|
121
|
+
<MethodBadge method={ref.method} />
|
|
122
|
+
<code class="break-all font-mono text-foreground text-sm">
|
|
123
|
+
{ref.path}
|
|
124
|
+
</code>
|
|
125
|
+
{ref.deprecated && (
|
|
126
|
+
<span class="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
|
|
127
|
+
deprecated
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
132
|
+
<div>
|
|
133
|
+
<Authorization security={security} />
|
|
134
|
+
<ParametersTable parameters={parameters} schemas={schemas} />
|
|
135
|
+
{messages.map((named) => {
|
|
136
|
+
const payload = payloadSchema(named.message);
|
|
137
|
+
// Headers may be multi-format too; undefined with `headers`
|
|
138
|
+
// present means an inline-unrenderable schema format.
|
|
139
|
+
const headers = schemaOf(named.message.headers);
|
|
140
|
+
const messageBindings = bindingGroups(named.message.bindings);
|
|
141
|
+
return (
|
|
142
|
+
<section class="mt-6">
|
|
143
|
+
<div
|
|
144
|
+
aria-level="2"
|
|
145
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
146
|
+
role="heading"
|
|
147
|
+
>
|
|
148
|
+
{messages.length > 1
|
|
149
|
+
? `Message: ${messageLabel(named)}`
|
|
150
|
+
: "Message"}
|
|
151
|
+
</div>
|
|
152
|
+
{named.message.description && (
|
|
153
|
+
<p class="mb-2 text-muted-foreground text-sm">
|
|
154
|
+
{named.message.description}
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
{named.message.contentType && (
|
|
158
|
+
<div class="mb-2 text-muted-foreground text-xs">
|
|
159
|
+
<code class="rounded bg-muted px-1 py-0.5">
|
|
160
|
+
{named.message.contentType}
|
|
161
|
+
</code>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
{payload && (
|
|
165
|
+
<div class="not-prose rounded-blume border border-border px-4 py-3">
|
|
166
|
+
<SchemaTable
|
|
167
|
+
expandAll={spec.expandSchemas}
|
|
168
|
+
schema={payload}
|
|
169
|
+
schemas={schemas}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
{/* Payload is optional on a message — only a payload that
|
|
174
|
+
failed to unwrap earns the can't-render note. */}
|
|
175
|
+
{named.message.payload !== undefined && !payload && (
|
|
176
|
+
<p class="text-muted-foreground text-sm">
|
|
177
|
+
The payload uses a schema format this reference can't
|
|
178
|
+
render inline.
|
|
179
|
+
</p>
|
|
180
|
+
)}
|
|
181
|
+
{named.message.headers && (
|
|
182
|
+
<div class="mt-3">
|
|
183
|
+
<div class="mb-1 font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
184
|
+
Headers
|
|
185
|
+
</div>
|
|
186
|
+
{headers ? (
|
|
187
|
+
<div class="not-prose rounded-blume border border-border px-4 py-3">
|
|
188
|
+
<SchemaTable
|
|
189
|
+
expandAll={spec.expandSchemas}
|
|
190
|
+
schema={headers}
|
|
191
|
+
schemas={schemas}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
) : (
|
|
195
|
+
<p class="text-muted-foreground text-sm">
|
|
196
|
+
The headers use a schema format this reference can't
|
|
197
|
+
render inline.
|
|
198
|
+
</p>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
{messageBindings.length > 0 && (
|
|
203
|
+
<Bindings
|
|
204
|
+
expandAll={spec.expandSchemas}
|
|
205
|
+
groups={messageBindings}
|
|
206
|
+
schemas={schemas}
|
|
207
|
+
title="Message bindings"
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
</section>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
<Bindings
|
|
214
|
+
expandAll={spec.expandSchemas}
|
|
215
|
+
groups={operationBindings}
|
|
216
|
+
schemas={schemas}
|
|
217
|
+
title="Operation bindings"
|
|
218
|
+
/>
|
|
219
|
+
<Bindings
|
|
220
|
+
expandAll={spec.expandSchemas}
|
|
221
|
+
groups={channelBindings}
|
|
222
|
+
schemas={schemas}
|
|
223
|
+
title="Channel bindings"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
{(samplePanels.length > 0 || messagePanels.length > 0) && (
|
|
227
|
+
<div class="xl:sticky xl:top-24 xl:self-start">
|
|
228
|
+
<div class="not-prose flex flex-col gap-6">
|
|
229
|
+
<PanelTabs copy heading="Example" panels={samplePanels} />
|
|
230
|
+
<PanelTabs heading="Message" panels={messagePanels} />
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|