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.
- package/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/search/documents.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import type { Nodes } from "mdast";
|
|
2
|
-
import {
|
|
3
|
-
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
4
|
-
import { gfm } from "micromark-extension-gfm";
|
|
2
|
+
import { markdownToMdast, mdxToMdast } from "satteri";
|
|
5
3
|
|
|
4
|
+
import {
|
|
5
|
+
componentRegistry,
|
|
6
|
+
downlevelComponentNode,
|
|
7
|
+
downlevelComponents,
|
|
8
|
+
exampleComponentSerializers,
|
|
9
|
+
} from "../ai/component-markdown.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ComponentMarkdown,
|
|
12
|
+
DownlevelWalk,
|
|
13
|
+
MdastNode as DownlevelNode,
|
|
14
|
+
} from "../ai/component-markdown.ts";
|
|
6
15
|
import { applyAudienceVisibility } from "../ai/visibility.ts";
|
|
7
16
|
import type { VisibilityAudience } from "../ai/visibility.ts";
|
|
8
17
|
import matter from "../core/frontmatter.ts";
|
|
18
|
+
import { parseHeadingMarkers } from "../core/heading-markers.ts";
|
|
9
19
|
import { contentIndexable } from "../core/manifest.ts";
|
|
10
20
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
21
|
+
import { HTML_COMMENT } from "../core/sources/normalize.ts";
|
|
22
|
+
import { readExpandedEntryText } from "../core/sources/read.ts";
|
|
23
|
+
import type { NavNode, PageRecord } from "../core/types.ts";
|
|
24
|
+
import { parseCodeTitle } from "../markdown/code-title.ts";
|
|
25
|
+
import { MARKDOWN_FEATURES, MDX_FEATURES } from "../markdown/features.ts";
|
|
13
26
|
import { pageFacets } from "./facets.ts";
|
|
14
27
|
|
|
15
28
|
/** A document indexed by the client-side search providers (Orama, FlexSearch). */
|
|
@@ -76,37 +89,163 @@ const INLINE_PARENTS = new Set([
|
|
|
76
89
|
"footnoteReference",
|
|
77
90
|
"link",
|
|
78
91
|
"linkReference",
|
|
92
|
+
"mdxJsxTextElement",
|
|
79
93
|
"strong",
|
|
94
|
+
"subscript",
|
|
95
|
+
"superscript",
|
|
96
|
+
"textDirective",
|
|
80
97
|
]);
|
|
81
98
|
|
|
99
|
+
// Nodes that never render as prose: image alt text was never indexed, MDX
|
|
100
|
+
// expressions (`{props.x}`) and ESM (`export const meta`) are code, and math
|
|
101
|
+
// is LaTeX source rendered by KaTeX, not searchable words.
|
|
102
|
+
const NON_PROSE = new Set<Nodes["type"]>([
|
|
103
|
+
"image",
|
|
104
|
+
"imageReference",
|
|
105
|
+
"inlineMath",
|
|
106
|
+
"math",
|
|
107
|
+
"mdxFlowExpression",
|
|
108
|
+
"mdxTextExpression",
|
|
109
|
+
"mdxjsEsm",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
type PageFormat = PageRecord["format"];
|
|
113
|
+
|
|
114
|
+
/** What the plain-text walk carries: the code-block policy and the downlevel context. */
|
|
115
|
+
interface Walk {
|
|
116
|
+
includeCodeBlocks: boolean;
|
|
117
|
+
downlevel: DownlevelWalk;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Front matter is already off (core/frontmatter.ts) by the time a body gets
|
|
121
|
+
// here, so a body that opens with a `---` divider must read as a thematic
|
|
122
|
+
// break rather than a second front matter block.
|
|
123
|
+
const MD_PARSE = { features: { ...MARKDOWN_FEATURES, frontmatter: false } };
|
|
124
|
+
const MDX_PARSE = { features: { ...MDX_FEATURES, frontmatter: false } };
|
|
125
|
+
|
|
126
|
+
const parseMdx = (markdown: string): Nodes | undefined => {
|
|
127
|
+
try {
|
|
128
|
+
return mdxToMdast(markdown, MDX_PARSE);
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a page body the way the renderer reads it. `.mdx` pages go through
|
|
136
|
+
* the MDX grammar with the renderer's feature set, so components become
|
|
137
|
+
* `mdxJsx*` nodes whose children — prose and fences, however they're indented
|
|
138
|
+
* — walk like top-level content, and `:::` directives and `$$` math parse as
|
|
139
|
+
* such; Markdown would instead fold a tight component into one html node and
|
|
140
|
+
* read its indented children as an indented code block. A page the MDX
|
|
141
|
+
* grammar rejects (an HTML comment, an unclosed tag) fails to render too, so
|
|
142
|
+
* it has no body to index: reading it as Markdown instead would put its ESM,
|
|
143
|
+
* expressions, and raw tags in the index as prose.
|
|
144
|
+
*/
|
|
145
|
+
const parseMarkdown = (
|
|
146
|
+
markdown: string,
|
|
147
|
+
format: PageFormat
|
|
148
|
+
): Nodes | undefined =>
|
|
149
|
+
format === "mdx" ? parseMdx(markdown) : markdownToMdast(markdown, MD_PARSE);
|
|
150
|
+
|
|
151
|
+
// Fenced code is excluded from the plain index by default (ranking noise) —
|
|
152
|
+
// the "markdown" extraction keeps it for Ask AI grounding. Code-heavy docs opt
|
|
153
|
+
// in via `search.indexing.includeCodeBlocks`, which indexes the fence body and
|
|
154
|
+
// its rendered title (```ts blume.config.ts) but never the fence markers,
|
|
155
|
+
// language, or other meta keywords.
|
|
156
|
+
const collectCode = (
|
|
157
|
+
node: Extract<Nodes, { type: "code" }>,
|
|
158
|
+
out: string[],
|
|
159
|
+
walk: Walk
|
|
160
|
+
): void => {
|
|
161
|
+
if (!walk.includeCodeBlocks) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const title = parseCodeTitle(node.meta ?? undefined);
|
|
165
|
+
if (title) {
|
|
166
|
+
out.push(title, " ");
|
|
167
|
+
}
|
|
168
|
+
// The trailing space keeps the fence apart from what follows it.
|
|
169
|
+
out.push(node.value, " ");
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Block boundaries separate words; so does an empty inline element (`<br />`,
|
|
173
|
+
// a self-closing icon), which otherwise fuses its neighbors.
|
|
174
|
+
const separatesWords = (node: Extract<Nodes, { children: unknown }>): boolean =>
|
|
175
|
+
!INLINE_PARENTS.has(node.type) || node.children.length === 0;
|
|
176
|
+
|
|
82
177
|
/** Fold one mdast node into the plain-text accumulator. */
|
|
83
|
-
const collectText = (node: Nodes, out: string[]): void => {
|
|
178
|
+
const collectText = (node: Nodes, out: string[], walk: Walk): void => {
|
|
179
|
+
if (NON_PROSE.has(node.type)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
84
182
|
switch (node.type) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// text was never indexed.
|
|
88
|
-
case "code":
|
|
89
|
-
case "image":
|
|
90
|
-
case "imageReference": {
|
|
183
|
+
case "code": {
|
|
184
|
+
collectCode(node, out, walk);
|
|
91
185
|
return;
|
|
92
186
|
}
|
|
187
|
+
// A component with a serializer indexes as the text the page shows —
|
|
188
|
+
// a Card's title and call to action, a TypeTable's descriptions — read
|
|
189
|
+
// from the Markdown the serializer emits (its contract is Markdown, not
|
|
190
|
+
// MDX). One without a serializer walks its children like any block.
|
|
191
|
+
case "mdxJsxFlowElement": {
|
|
192
|
+
// SAFETY: DownlevelNode is a structural subset of Satteri's mdast
|
|
193
|
+
// output — the walk checks for offsets before reading them.
|
|
194
|
+
const text = downlevelComponentNode(
|
|
195
|
+
node as DownlevelNode,
|
|
196
|
+
walk.downlevel
|
|
197
|
+
);
|
|
198
|
+
if (text !== null) {
|
|
199
|
+
collectText(markdownToMdast(text, MD_PARSE), out, walk);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
93
204
|
// Inline code is kept verbatim — `Array<T>` is a type parameter, not a
|
|
94
205
|
// tag, and its tokens must stay searchable.
|
|
95
206
|
case "inlineCode": {
|
|
96
207
|
out.push(node.value);
|
|
97
208
|
return;
|
|
98
209
|
}
|
|
99
|
-
// A raw-HTML
|
|
100
|
-
//
|
|
101
|
-
//
|
|
210
|
+
// A raw-HTML run. In a `.md` page (or an `.mdx` page MDX couldn't parse)
|
|
211
|
+
// CommonMark folds a block-level `<Callout>` with no blank lines into ONE
|
|
212
|
+
// html node holding all its inner prose, so the node can't just be
|
|
213
|
+
// dropped — strip the tag-shaped runs and keep the text.
|
|
102
214
|
case "html": {
|
|
103
|
-
out.push(
|
|
215
|
+
out.push(
|
|
216
|
+
node.value.replaceAll(HTML_COMMENT, " ").replaceAll(HTML_OR_JSX, " "),
|
|
217
|
+
" "
|
|
218
|
+
);
|
|
104
219
|
return;
|
|
105
220
|
}
|
|
106
221
|
case "break": {
|
|
107
222
|
out.push(" ");
|
|
108
223
|
return;
|
|
109
224
|
}
|
|
225
|
+
// Trailing heading markers (`[#custom-id]`, `{#custom-id}`, `[!toc]`,
|
|
226
|
+
// `[toc]`) are anchor
|
|
227
|
+
|
|
228
|
+
// metadata, not prose — strip them so they never pollute the index. Only
|
|
229
|
+
// a marker that ends the heading's final plain-text child counts,
|
|
230
|
+
// mirroring the renderer: a heading ending in inline code or an image
|
|
231
|
+
// keeps its bracketed text on the page (so it stays searchable), and a
|
|
232
|
+
// heading that is nothing but markers renders them literally.
|
|
233
|
+
case "heading": {
|
|
234
|
+
const last = node.children.at(-1);
|
|
235
|
+
const trailing = last?.type === "text" ? last : undefined;
|
|
236
|
+
const inner: string[] = [];
|
|
237
|
+
const kept = trailing ? node.children.slice(0, -1) : node.children;
|
|
238
|
+
for (const child of kept) {
|
|
239
|
+
collectText(child, inner, walk);
|
|
240
|
+
}
|
|
241
|
+
if (trailing) {
|
|
242
|
+
const stripped = parseHeadingMarkers(trailing.value).text;
|
|
243
|
+
const literal = stripped === "" && node.children.length === 1;
|
|
244
|
+
inner.push(literal ? trailing.value : stripped);
|
|
245
|
+
}
|
|
246
|
+
out.push(inner.join("").trimEnd(), " ");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
110
249
|
default: {
|
|
111
250
|
break;
|
|
112
251
|
}
|
|
@@ -117,9 +256,9 @@ const collectText = (node: Nodes, out: string[]): void => {
|
|
|
117
256
|
}
|
|
118
257
|
if ("children" in node) {
|
|
119
258
|
for (const child of node.children) {
|
|
120
|
-
collectText(child, out);
|
|
259
|
+
collectText(child, out, walk);
|
|
121
260
|
}
|
|
122
|
-
if (
|
|
261
|
+
if (separatesWords(node)) {
|
|
123
262
|
out.push(" ");
|
|
124
263
|
}
|
|
125
264
|
}
|
|
@@ -132,13 +271,17 @@ const collectText = (node: Nodes, out: string[]): void => {
|
|
|
132
271
|
* reduce correctly. This feeds the client index *and* every hosted-provider
|
|
133
272
|
* record, so anything lost here is a permanent search-quality loss.
|
|
134
273
|
*/
|
|
135
|
-
const toPlainText = (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
274
|
+
const toPlainText = (
|
|
275
|
+
markdown: string,
|
|
276
|
+
format: PageFormat,
|
|
277
|
+
walk: Walk
|
|
278
|
+
): string => {
|
|
279
|
+
const tree = parseMarkdown(markdown, format);
|
|
280
|
+
if (!tree) {
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
140
283
|
const out: string[] = [];
|
|
141
|
-
collectText(tree, out);
|
|
284
|
+
collectText(tree, out, walk);
|
|
142
285
|
return out.join("").replaceAll(WHITESPACE, " ").trim();
|
|
143
286
|
};
|
|
144
287
|
|
|
@@ -188,6 +331,47 @@ const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
|
188
331
|
return index;
|
|
189
332
|
};
|
|
190
333
|
|
|
334
|
+
interface BuildOptions {
|
|
335
|
+
includeWhenDisabled?: boolean;
|
|
336
|
+
content?: "markdown" | "plain";
|
|
337
|
+
audience?: VisibilityAudience;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Read a page's body and reduce it per the extraction options. The
|
|
342
|
+
* `"markdown"` body is agent-facing, so its components are downleveled in the
|
|
343
|
+
* source with the serializers every agent surface uses (front matter in scope
|
|
344
|
+
* for prop expressions). The plain body is parsed as written and walked, with
|
|
345
|
+
* the same serializers applied per component on the way, so the index reads a
|
|
346
|
+
* `<Card>` or `<TypeTable>` as the text it shows.
|
|
347
|
+
*/
|
|
348
|
+
const pageBody = async (
|
|
349
|
+
project: BlumeProject,
|
|
350
|
+
page: PageRecord | undefined,
|
|
351
|
+
options: Pick<BuildOptions, "audience" | "content"> | undefined,
|
|
352
|
+
components: Record<string, ComponentMarkdown>
|
|
353
|
+
): Promise<string> => {
|
|
354
|
+
if (!page) {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
const parsed = matter(await readExpandedEntryText(project, page));
|
|
358
|
+
const visible = applyAudienceVisibility(
|
|
359
|
+
parsed.content,
|
|
360
|
+
options?.audience ?? "web"
|
|
361
|
+
);
|
|
362
|
+
if (options?.content === "markdown") {
|
|
363
|
+
return downlevelComponents(visible.trim(), components, parsed.data);
|
|
364
|
+
}
|
|
365
|
+
return toPlainText(visible, page.format, {
|
|
366
|
+
downlevel: {
|
|
367
|
+
frontmatter: parsed.data,
|
|
368
|
+
registry: componentRegistry(components),
|
|
369
|
+
source: visible,
|
|
370
|
+
},
|
|
371
|
+
includeCodeBlocks: project.config.search.indexing.includeCodeBlocks,
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
|
|
191
375
|
/**
|
|
192
376
|
* Build search documents from the content graph. Only indexable pages are
|
|
193
377
|
* included (per the route manifest), and content comes from the source files,
|
|
@@ -201,19 +385,25 @@ const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
|
201
385
|
* searchable text; `"markdown"` keeps the body's Markdown — code blocks, lists,
|
|
202
386
|
* headings — for Ask AI grounding, where fenced examples are often the answer
|
|
203
387
|
* and stripping them makes the model unable to cite content the docs do contain.
|
|
388
|
+
* The `"markdown"` body is agent-facing, so components are downleveled with the
|
|
389
|
+
* same serializers the `.md` mirror, llms-full.txt and MCP `get_page` use: a
|
|
390
|
+
* page whose body is a `<CardGroup>` of `<Card>`s is prose to a reader and bare
|
|
391
|
+
* JSX to anything reading the source, and section landing pages are exactly
|
|
392
|
+
* that shape.
|
|
204
393
|
*
|
|
205
394
|
* `audience` resolves `<Visibility>` blocks before extraction: `"web"`
|
|
206
395
|
* (default) keeps web-only content and drops agents-only blocks — the site
|
|
207
396
|
* search and hosted syncs must not surface content the page hides — while
|
|
208
397
|
* `"agents"` mirrors llms-full.txt/MCP `get_page` (web removed, agents kept).
|
|
398
|
+
*
|
|
399
|
+
* Whether fenced code joins the plain extraction is site-wide policy, read
|
|
400
|
+
* from `search.indexing.includeCodeBlocks` here rather than threaded by each
|
|
401
|
+
* caller, so the client index, hosted syncs, eject, and the MCP `search_docs`
|
|
402
|
+
* index can't drift apart.
|
|
209
403
|
*/
|
|
210
404
|
export const buildSearchDocuments = async (
|
|
211
405
|
project: BlumeProject,
|
|
212
|
-
options?:
|
|
213
|
-
includeWhenDisabled?: boolean;
|
|
214
|
-
content?: "markdown" | "plain";
|
|
215
|
-
audience?: VisibilityAudience;
|
|
216
|
-
}
|
|
406
|
+
options?: BuildOptions
|
|
217
407
|
): Promise<SearchDocument[]> => {
|
|
218
408
|
const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
|
|
219
409
|
|
|
@@ -247,17 +437,19 @@ export const buildSearchDocuments = async (
|
|
|
247
437
|
return page ? contentIndexable(page, project.config) : false;
|
|
248
438
|
});
|
|
249
439
|
|
|
440
|
+
// Serializers for downleveling, layered the way `buildRawMarkdown` layers
|
|
441
|
+
// them: examples first, so a user `markdownComponents` entry of the same
|
|
442
|
+
// name still wins. Built once — `downlevelComponents` rebuilds its registry
|
|
443
|
+
// per call otherwise.
|
|
444
|
+
const components = {
|
|
445
|
+
...exampleComponentSerializers(project.examples ?? {}),
|
|
446
|
+
...project.config.ai.markdownComponents,
|
|
447
|
+
};
|
|
448
|
+
|
|
250
449
|
return await Promise.all(
|
|
251
450
|
indexable.map(async (route) => {
|
|
252
451
|
const page = pageById.get(route.id);
|
|
253
|
-
const
|
|
254
|
-
const source = raw ? matter(raw).content : "";
|
|
255
|
-
const visible = applyAudienceVisibility(
|
|
256
|
-
source,
|
|
257
|
-
options?.audience ?? "web"
|
|
258
|
-
);
|
|
259
|
-
const body =
|
|
260
|
-
options?.content === "markdown" ? visible.trim() : toPlainText(visible);
|
|
452
|
+
const body = await pageBody(project, page, options, components);
|
|
261
453
|
const tags = page?.meta?.search?.tags;
|
|
262
454
|
const crumb = crumbs.get(route.path);
|
|
263
455
|
const facets = page ? pageFacets(page, project.config) : undefined;
|
|
@@ -68,13 +68,17 @@ const LATIN_SCRIPT = "Latn";
|
|
|
68
68
|
/**
|
|
69
69
|
* Parse a locale tag, tolerating the legacy forms that reach here from
|
|
70
70
|
* hand-written config: underscores (`ru_RU`), POSIX suffixes (`ja_JP.UTF-8`,
|
|
71
|
-
* `zh_TW@Big5`), and extlang tags ICU rejects (`zh-cmn-Hans`).
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* `zh_TW@Big5`), and extlang tags ICU rejects (`zh-cmn-Hans`). The POSIX
|
|
72
|
+
* codeset/modifier suffix is dropped first — it carries no BCP 47 meaning,
|
|
73
|
+
* and dropping it keeps an explicit script subtag in play: `az_Cyrl.UTF-8`
|
|
74
|
+
* must resolve to `Cyrl`, not fall back to `az` and maximize to Latin.
|
|
75
|
+
* `Intl.Locale` still throws on extlang forms, so a failed parse retries with
|
|
76
|
+
* the primary language subtag alone — enough to resolve the script, which is
|
|
77
|
+
* all that gates the tokenizer. Returns `undefined` when even that subtag is
|
|
78
|
+
* unparseable.
|
|
75
79
|
*/
|
|
76
80
|
const parseLocale = (tag: string): Intl.Locale | undefined => {
|
|
77
|
-
const hyphenated = tag.replaceAll("_", "-");
|
|
81
|
+
const hyphenated = tag.replace(/[.@].*$/u, "").replaceAll("_", "-");
|
|
78
82
|
try {
|
|
79
83
|
return new Intl.Locale(hyphenated);
|
|
80
84
|
} catch {
|