@stainless-api/docs 0.1.0-beta.137 → 0.1.0-beta.138
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 +9 -0
- package/eslint-suppressions.json +0 -5
- package/package.json +5 -5
- package/plugin/components/RequestBuilder/index.tsx +0 -3
- package/shared/virtualModule.ts +0 -17
- package/stl-docs/aiChatExamples.ts +11 -75
- package/stl-docs/index.ts +32 -45
- package/stl-docs/proseSearchIndexing.ts +0 -218
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @stainless-api/docs
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.138
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bb5f952: Stop loading AI chat examples from obsolete Stainless endpoint
|
|
8
|
+
- 25d7a11: Stop creating Algolia prose indexes during docs site builds. Vector-based prose indexing is unaffected and remains controlled by the `experimental.disableStainlessProseIndexing` flag.
|
|
9
|
+
- Updated dependencies [25d7a11]
|
|
10
|
+
- @stainless-api/docs-search@0.1.0-beta.49
|
|
11
|
+
|
|
3
12
|
## 0.1.0-beta.137
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/eslint-suppressions.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stainless-api/docs",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.138",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
"shiki": "^4.0.2",
|
|
73
73
|
"unified": "^11.0.5",
|
|
74
74
|
"web-worker": "^1.5.0",
|
|
75
|
+
"@stainless-api/docs-search": "0.1.0-beta.49",
|
|
75
76
|
"@stainless-api/docs-ui": "0.1.0-beta.95",
|
|
76
|
-
"@stainless-api/ui-primitives": "0.1.0-beta.53"
|
|
77
|
-
"@stainless-api/docs-search": "0.1.0-beta.48"
|
|
77
|
+
"@stainless-api/ui-primitives": "0.1.0-beta.53"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@astrojs/check": "^0.9.9",
|
|
@@ -83,13 +83,13 @@
|
|
|
83
83
|
"@types/react": "19.2.14",
|
|
84
84
|
"@types/react-dom": "^19.2.3",
|
|
85
85
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
86
|
-
"astro": "^6.2.
|
|
86
|
+
"astro": "^6.2.2",
|
|
87
87
|
"react": "^19.2.5",
|
|
88
88
|
"react-dom": "^19.2.5",
|
|
89
89
|
"typescript": "6.0.3",
|
|
90
90
|
"vite": "^7.3.2",
|
|
91
91
|
"vitest": "^4.1.5",
|
|
92
|
-
"zod": "^4.4.
|
|
92
|
+
"zod": "^4.4.3",
|
|
93
93
|
"@stainless/eslint-config": "0.1.0-beta.2",
|
|
94
94
|
"@stainless/sdk-json": "^0.1.0-beta.10"
|
|
95
95
|
},
|
|
@@ -16,8 +16,6 @@ export function RequestBuilder({
|
|
|
16
16
|
}) {
|
|
17
17
|
let params: Param[];
|
|
18
18
|
const spec = useSpec();
|
|
19
|
-
// https://github.com/Rel1cx/eslint-react/issues/1617
|
|
20
|
-
/* eslint-disable @eslint-react/error-boundaries */
|
|
21
19
|
try {
|
|
22
20
|
if (!spec) throw new Error('Spec is required for RequestBuilder');
|
|
23
21
|
params = spec && extractParams(spec, method);
|
|
@@ -25,7 +23,6 @@ export function RequestBuilder({
|
|
|
25
23
|
console.warn(e);
|
|
26
24
|
return <div className={className}>{children}</div>;
|
|
27
25
|
}
|
|
28
|
-
/* eslint-enable */
|
|
29
26
|
const [httpMethod, path] = method.endpoint.split(' ') as [string, string];
|
|
30
27
|
|
|
31
28
|
return (
|
package/shared/virtualModule.ts
CHANGED
|
@@ -10,23 +10,6 @@ export function buildVirtualModuleString<T extends Record<string, unknown>>(vars
|
|
|
10
10
|
.join('\n');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function makeVirtualModPlugin(bareId: string, content: string): VitePlugin {
|
|
14
|
-
return {
|
|
15
|
-
name: `stl-virtual-module-loader-${bareId}`,
|
|
16
|
-
resolveId(id) {
|
|
17
|
-
// The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
|
|
18
|
-
if (id === bareId) {
|
|
19
|
-
return `\0${bareId}`;
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
load(id) {
|
|
23
|
-
if (id === `\0${bareId}`) {
|
|
24
|
-
return content;
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
13
|
export function makeAsyncVirtualModPlugin<T extends Record<string, unknown>>(
|
|
31
14
|
bareId: string,
|
|
32
15
|
contentLoader: () => Promise<T | string>,
|
|
@@ -1,95 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import z from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
buildVirtualModuleString,
|
|
5
|
-
makeAsyncVirtualModPlugin,
|
|
6
|
-
makeVirtualModPlugin,
|
|
7
|
-
} from '../shared/virtualModule';
|
|
1
|
+
import { buildVirtualModuleString } from '../shared/virtualModule';
|
|
8
2
|
import type * as virtualExampleModule from 'virtual:stl-docs-ai-chat-examples';
|
|
9
3
|
type VirtualExampleModule = typeof virtualExampleModule;
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}),
|
|
17
|
-
);
|
|
18
|
-
export type ExamplePromptResponse = z.infer<typeof exampleSchema>;
|
|
5
|
+
export type ExamplePromptResponse = {
|
|
6
|
+
shortPrompt: string;
|
|
7
|
+
longPrompt: string;
|
|
8
|
+
icon: string;
|
|
9
|
+
}[];
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
logger: AstroIntegrationLogger,
|
|
24
|
-
): Promise<ExamplePromptResponse | undefined> {
|
|
25
|
-
try {
|
|
26
|
-
const response = await fetch(`https://api.stainless.com/api/ai/steelie-examples/${projectName}`, {
|
|
27
|
-
method: 'GET',
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const text = await response.text();
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
logger.error(`failed to fetch AI chat examples: ${text}`);
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
const examples = exampleSchema.parse(JSON.parse(text));
|
|
36
|
-
return examples;
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (error instanceof Error) logger.error(`failed to fetch AI chat examples: ${error.message}`);
|
|
39
|
-
else throw error;
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export default async function generateExamplesPlugin({
|
|
45
|
-
projectName,
|
|
46
|
-
logger,
|
|
47
|
-
exampleOverrides,
|
|
48
|
-
}: {
|
|
49
|
-
projectName: string | undefined;
|
|
50
|
-
logger: AstroIntegrationLogger;
|
|
51
|
-
exampleOverrides?: ExamplePromptResponse;
|
|
52
|
-
}) {
|
|
53
|
-
// if the user has specified any examples, return those immediately
|
|
54
|
-
// instead of loading them via the web.
|
|
55
|
-
if (exampleOverrides) {
|
|
56
|
-
return makeVirtualModPlugin(
|
|
57
|
-
'virtual:stl-docs-ai-chat-examples',
|
|
58
|
-
generateVirtualModuleString(exampleOverrides),
|
|
59
|
-
);
|
|
11
|
+
export function generateExamplesVirtualModule(exampleOverrides: ExamplePromptResponse | undefined): string {
|
|
12
|
+
if (!exampleOverrides) {
|
|
13
|
+
return buildVirtualModuleString({ examples: undefined } satisfies VirtualExampleModule);
|
|
60
14
|
}
|
|
61
|
-
// if we don't have a defined project name, don't try to fetch examples
|
|
62
|
-
if (!projectName) {
|
|
63
|
-
return makeVirtualModPlugin(
|
|
64
|
-
'virtual:stl-docs-ai-chat-examples',
|
|
65
|
-
buildVirtualModuleString({ examples: undefined } satisfies VirtualExampleModule),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// otherwise, promise to get the right examples at some point later on
|
|
70
|
-
const examplesPromise = loadExamples(projectName, logger);
|
|
71
|
-
return makeAsyncVirtualModPlugin<VirtualExampleModule>('virtual:stl-docs-ai-chat-examples', async () => {
|
|
72
|
-
const examples = await examplesPromise;
|
|
73
|
-
return generateVirtualModuleString(examples);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function generateVirtualModuleString(examples: ExamplePromptResponse | undefined) {
|
|
78
|
-
if (!examples) return 'export const examples = undefined;';
|
|
79
15
|
|
|
80
16
|
// Generate icon imports
|
|
81
17
|
// prettier-ignore
|
|
82
18
|
const pascalToKebab = (str: string) => str.split(/(?=[A-Z])/).join('-').toLowerCase();
|
|
83
19
|
const iconImportPath = (iconName: string) =>
|
|
84
20
|
import.meta.resolve(`lucide-react/dist/esm/icons/${pascalToKebab(iconName)}.js`);
|
|
85
|
-
const iconImports =
|
|
21
|
+
const iconImports = exampleOverrides.map(
|
|
86
22
|
({ icon }) => `import ${icon} from ${JSON.stringify(iconImportPath(icon))}`,
|
|
87
23
|
);
|
|
88
24
|
|
|
89
25
|
// Reference icon imports in `examples` exported object
|
|
90
26
|
// "icon":"Sparkles" -> "icon":Sparkles
|
|
91
27
|
const iconStringsToIdents = (jsonBlob: string) => jsonBlob.replace(/"icon":\s*"(\w+)"/g, '"icon":$1');
|
|
92
|
-
const exportBody = `export const examples = ${iconStringsToIdents(JSON.stringify(
|
|
28
|
+
const exportBody = `export const examples = ${iconStringsToIdents(JSON.stringify(exampleOverrides))};`;
|
|
93
29
|
|
|
94
30
|
return [...iconImports, exportBody].join('\n');
|
|
95
31
|
}
|
package/stl-docs/index.ts
CHANGED
|
@@ -20,13 +20,12 @@ import {
|
|
|
20
20
|
import { buildVirtualModuleString } from '../shared/virtualModule';
|
|
21
21
|
import { resolveSrcFile } from '../resolveSrcFile';
|
|
22
22
|
import { stainlessDocsMarkdownRenderer } from './proseMarkdown/proseMarkdownIntegration';
|
|
23
|
-
import {
|
|
23
|
+
import { setSharedLogger } from '../shared/getSharedLogger';
|
|
24
24
|
import { stainlessDocsVectorProseIndexing } from './proseDocSync';
|
|
25
|
-
import { stainlessDocsAlgoliaProseIndexing } from './proseSearchIndexing';
|
|
26
25
|
import { stainlessStarlight } from '../plugin';
|
|
27
26
|
import { getFontRoles, flattenFonts } from './fonts';
|
|
28
27
|
import conditionalIntegration from '../shared/conditionalIntegration';
|
|
29
|
-
import
|
|
28
|
+
import { generateExamplesVirtualModule } from './aiChatExamples';
|
|
30
29
|
import { ogImageStarlightPlugin } from './og-image';
|
|
31
30
|
|
|
32
31
|
export * from '../plugin';
|
|
@@ -172,8 +171,7 @@ function stainlessDocsIntegration(
|
|
|
172
171
|
return {
|
|
173
172
|
name: 'stl-docs-astro',
|
|
174
173
|
hooks: {
|
|
175
|
-
'astro:config:setup':
|
|
176
|
-
const logger = getSharedLogger({ fallback: localLogger });
|
|
174
|
+
'astro:config:setup': ({ updateConfig, command, config: astroConfig }) => {
|
|
177
175
|
// we only handle redirects for builds
|
|
178
176
|
// in dev, Astro handles them for us
|
|
179
177
|
if (command === 'build' && astroConfig.redirects) {
|
|
@@ -195,30 +193,35 @@ function stainlessDocsIntegration(
|
|
|
195
193
|
vmAiChatHandlerExport = `export { default as AI_CHAT_HANDLER } from '${handlerEntrypoint}';`;
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
const virtualModules = new Map(
|
|
199
|
-
|
|
200
|
-
'virtual:stl-docs-virtual-module'
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
196
|
+
const virtualModules = new Map<string, string>([
|
|
197
|
+
[
|
|
198
|
+
'virtual:stl-docs-virtual-module',
|
|
199
|
+
buildVirtualModuleString({
|
|
200
|
+
TABS: config.tabs.map((tab) => ({ ...tab, link: withBase(tab.link) })),
|
|
201
|
+
SPLIT_TABS_ENABLED: config.splitTabsEnabled,
|
|
202
|
+
HEADER_LINKS: config.header.links.map((link) => ({ ...link, link: withBase(link.link) })),
|
|
203
|
+
HEADER_LAYOUT: config.header.layout,
|
|
204
|
+
ENABLE_CLIENT_ROUTER: config.enableClientRouter,
|
|
205
|
+
API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
|
|
206
|
+
ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
|
|
207
|
+
ENABLE_CONTEXT_MENU: !!config.contextMenu, // TODO: do not duplicate this between both virtual modules
|
|
208
|
+
CONTEXT_MENU_ENABLE_THIRD_PARTY:
|
|
209
|
+
(typeof config.contextMenu === 'object' ? config.contextMenu.thirdParty : null) ?? true,
|
|
210
|
+
RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
|
|
211
|
+
FONTS: getFontRoles(config.fonts),
|
|
212
|
+
LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: config.linkGroupTitlesToOverviewPages,
|
|
213
|
+
RENDER_CREDITS: config.credits,
|
|
214
|
+
SITE_TITLE: config.siteTitle,
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
['virtual:stl-docs-ai-chat', vmAiChatHandlerExport],
|
|
218
|
+
]);
|
|
219
|
+
if (config.aiChat) {
|
|
220
|
+
virtualModules.set(
|
|
221
|
+
'virtual:stl-docs-ai-chat-examples',
|
|
222
|
+
generateExamplesVirtualModule(config.aiChat.examples),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
222
225
|
|
|
223
226
|
updateConfig({
|
|
224
227
|
fonts: [...flattenFonts(config.fonts), ...(astroConfig?.fonts ?? [])],
|
|
@@ -239,17 +242,6 @@ function stainlessDocsIntegration(
|
|
|
239
242
|
if (virtualModules.has(bare)) return virtualModules.get(bare);
|
|
240
243
|
},
|
|
241
244
|
},
|
|
242
|
-
// Separate plugin for the examples because it has async resolution; not a simple string
|
|
243
|
-
// like the above plugins
|
|
244
|
-
...(config.aiChat
|
|
245
|
-
? [
|
|
246
|
-
await generateExamplesPlugin({
|
|
247
|
-
projectName: config.apiReference?.stainlessProject ?? undefined,
|
|
248
|
-
logger,
|
|
249
|
-
exampleOverrides: config.aiChat.examples,
|
|
250
|
-
}),
|
|
251
|
-
]
|
|
252
|
-
: []),
|
|
253
245
|
],
|
|
254
246
|
},
|
|
255
247
|
build: {
|
|
@@ -311,11 +303,6 @@ export function stainlessDocs(config: StainlessDocsUserConfig): AstroIntegration
|
|
|
311
303
|
integration: stainlessDocsMarkdownRenderer({ apiReferenceBasePath }),
|
|
312
304
|
reason: 'disabled by experimental config "disableProseMarkdownRendering"',
|
|
313
305
|
}),
|
|
314
|
-
conditionalIntegration({
|
|
315
|
-
condition: !config.experimental?.disableStainlessProseIndexing,
|
|
316
|
-
integration: stainlessDocsAlgoliaProseIndexing({ apiReferenceBasePath }),
|
|
317
|
-
reason: 'disabled by experimental config "disableStainlessProseIndexing"',
|
|
318
|
-
}),
|
|
319
306
|
conditionalIntegration({
|
|
320
307
|
condition: !config.experimental?.disableStainlessProseIndexing,
|
|
321
308
|
integration: stainlessDocsVectorProseIndexing(normalizedConfig, apiReferenceBasePath),
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import type { AstroIntegration } from 'astro';
|
|
2
|
-
import { readFile } from 'fs/promises';
|
|
3
|
-
import { getProsePages } from '../shared/getProsePages';
|
|
4
|
-
import { getSharedLogger } from '../shared/getSharedLogger';
|
|
5
|
-
import { bold } from '../shared/terminalUtils';
|
|
6
|
-
import * as cheerio from 'cheerio';
|
|
7
|
-
import { buildProseIndex } from '@stainless-api/docs-search/providers/algolia';
|
|
8
|
-
|
|
9
|
-
class SectionContext {
|
|
10
|
-
headers: { level: number; text: string }[] = [];
|
|
11
|
-
headerId: string | undefined;
|
|
12
|
-
headerTag: string | undefined;
|
|
13
|
-
headerText: string | undefined;
|
|
14
|
-
hasContent = false;
|
|
15
|
-
|
|
16
|
-
get(): string | undefined {
|
|
17
|
-
if (this.headers.length === 0) return;
|
|
18
|
-
return this.headers.map((h) => h.text).join(' > ');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
header({ id, tag, text }: { id: string; tag: string; text: string }) {
|
|
22
|
-
const level = getHeaderLevel(tag);
|
|
23
|
-
if (level > 0) {
|
|
24
|
-
while (this.headers.length > 0 && this.headers[this.headers.length - 1]!.level >= level) {
|
|
25
|
-
this.headers.pop();
|
|
26
|
-
}
|
|
27
|
-
this.headers.push({ level, text });
|
|
28
|
-
}
|
|
29
|
-
this.headerId = id;
|
|
30
|
-
this.headerTag = tag;
|
|
31
|
-
this.headerText = text;
|
|
32
|
-
this.hasContent = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function slugify(text: string): string {
|
|
37
|
-
return text
|
|
38
|
-
.toLowerCase()
|
|
39
|
-
.replace(/`/g, '')
|
|
40
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
41
|
-
.replace(/^-|-$/g, '');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getHeaderLevel(tag: string): number {
|
|
45
|
-
const match = tag.match(/^h(\d)$/);
|
|
46
|
-
return match ? parseInt(match[1]!, 10) : 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Chunking configuration
|
|
50
|
-
// We target 64-256 tokens per chunk, using ~1.3 tokens/word for English text
|
|
51
|
-
const TOKENS_PER_WORD = 1.3;
|
|
52
|
-
const MIN_TOKENS = 64;
|
|
53
|
-
const MAX_TOKENS = 256;
|
|
54
|
-
const MIN_WORDS = Math.floor(MIN_TOKENS / TOKENS_PER_WORD); // ~49 words
|
|
55
|
-
const MAX_WORDS = Math.floor(MAX_TOKENS / TOKENS_PER_WORD); // ~197 words
|
|
56
|
-
const SENTENCE_BREAK_WORDS = Math.floor((MAX_TOKENS * 0.875) / TOKENS_PER_WORD); // ~172 words
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Chunks text content into segments of 64-256 tokens using word-based boundaries.
|
|
60
|
-
* Prefers breaking at sentence endings for natural chunk boundaries.
|
|
61
|
-
*/
|
|
62
|
-
function chunkTextByWords(text: string): string[] {
|
|
63
|
-
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
64
|
-
|
|
65
|
-
if (words.length <= MAX_WORDS) {
|
|
66
|
-
return words.length > 0 ? [words.join(' ')] : [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const chunks: string[] = [];
|
|
70
|
-
let currentChunk: string[] = [];
|
|
71
|
-
|
|
72
|
-
for (const word of words) {
|
|
73
|
-
currentChunk.push(word);
|
|
74
|
-
|
|
75
|
-
// Force break at max words
|
|
76
|
-
if (currentChunk.length >= MAX_WORDS) {
|
|
77
|
-
chunks.push(currentChunk.join(' '));
|
|
78
|
-
currentChunk = [];
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Prefer breaking at sentence boundaries after threshold
|
|
83
|
-
if (currentChunk.length >= SENTENCE_BREAK_WORDS && /[.!?]["']?$/.test(word)) {
|
|
84
|
-
chunks.push(currentChunk.join(' '));
|
|
85
|
-
currentChunk = [];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (currentChunk.length > 0) {
|
|
90
|
-
if (currentChunk.length < MIN_WORDS && chunks.length > 0) {
|
|
91
|
-
const lastChunk = chunks[chunks.length - 1]!;
|
|
92
|
-
const mergedWords = lastChunk.split(/\s+/).length + currentChunk.length;
|
|
93
|
-
if (mergedWords <= MAX_WORDS) {
|
|
94
|
-
chunks[chunks.length - 1] = lastChunk + ' ' + currentChunk.join(' ');
|
|
95
|
-
} else {
|
|
96
|
-
chunks.push(currentChunk.join(' '));
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
chunks.push(currentChunk.join(' '));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return chunks;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type IndexEntry = {
|
|
107
|
-
chunk: { id: string; index: number; total: number };
|
|
108
|
-
id: string;
|
|
109
|
-
tag: string;
|
|
110
|
-
content: string;
|
|
111
|
-
language?: string;
|
|
112
|
-
sectionContext?: string;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const DEFAULT_ROOT = 'main';
|
|
116
|
-
const DEFAULT_PATTERN = 'h1, h2, h3, h4, h5, h6, p, li, pre code';
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Indexes HTML content for search, with section context and code language extraction.
|
|
120
|
-
*
|
|
121
|
-
* Features:
|
|
122
|
-
* - Tracks header hierarchy to prepend section context (e.g., "Guide > Setup: ...")
|
|
123
|
-
* - Extracts language metadata from code blocks (class="language-js")
|
|
124
|
-
* - Uses word-based chunking with sentence boundary detection
|
|
125
|
-
*/
|
|
126
|
-
function* indexHTML(content: string, root = DEFAULT_ROOT, pattern = DEFAULT_PATTERN): Generator<IndexEntry> {
|
|
127
|
-
const $ = cheerio.load(content);
|
|
128
|
-
const matches = $(root).find(pattern);
|
|
129
|
-
|
|
130
|
-
const ctx = new SectionContext();
|
|
131
|
-
|
|
132
|
-
for (const match of matches) {
|
|
133
|
-
const tagName = match.tagName.toLowerCase();
|
|
134
|
-
const rawText = $(match).text().trim();
|
|
135
|
-
|
|
136
|
-
if (getHeaderLevel(tagName) > 0) {
|
|
137
|
-
ctx.header({ id: $(match).attr('id') ?? slugify(rawText), tag: tagName, text: rawText });
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Check if this is a code block and extract language
|
|
142
|
-
const isCode = tagName === 'code' && $(match).parent().is('pre');
|
|
143
|
-
let language: string | undefined;
|
|
144
|
-
if (isCode) {
|
|
145
|
-
const classes = $(match).attr('class') || '';
|
|
146
|
-
const langMatch = classes.match(/(?:language-|lang-)([a-zA-Z0-9+-]+)/);
|
|
147
|
-
language = langMatch ? langMatch[1] : undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Build content with section context
|
|
151
|
-
const sectionContext = ctx.get();
|
|
152
|
-
const chunks = chunkTextByWords(rawText);
|
|
153
|
-
const chunkId = crypto.randomUUID();
|
|
154
|
-
|
|
155
|
-
for (const [chunkN, chunkText] of chunks.entries()) {
|
|
156
|
-
yield {
|
|
157
|
-
id: ctx.headerId ?? $(match).attr('id') ?? chunkId,
|
|
158
|
-
tag: isCode ? 'code' : tagName,
|
|
159
|
-
content: chunkText,
|
|
160
|
-
...(sectionContext ? { sectionContext } : {}),
|
|
161
|
-
...(language && { language }),
|
|
162
|
-
chunk: {
|
|
163
|
-
id: chunkId,
|
|
164
|
-
index: chunkN,
|
|
165
|
-
total: chunks.length,
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
ctx.hasContent = true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function stainlessDocsAlgoliaProseIndexing({
|
|
174
|
-
apiReferenceBasePath,
|
|
175
|
-
}: {
|
|
176
|
-
apiReferenceBasePath: string | null;
|
|
177
|
-
}): AstroIntegration {
|
|
178
|
-
return {
|
|
179
|
-
name: 'stl-docs-prose-indexing',
|
|
180
|
-
hooks: {
|
|
181
|
-
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
182
|
-
const logger = getSharedLogger({ fallback: localLogger });
|
|
183
|
-
const outputBasePath = dir.pathname;
|
|
184
|
-
|
|
185
|
-
const {
|
|
186
|
-
PUBLIC_ALGOLIA_APP_ID: appId,
|
|
187
|
-
PUBLIC_ALGOLIA_INDEX: indexName,
|
|
188
|
-
PRIVATE_ALGOLIA_WRITE_KEY: algoliaWriteKey,
|
|
189
|
-
} = process.env;
|
|
190
|
-
|
|
191
|
-
if (!appId || !indexName || !algoliaWriteKey) {
|
|
192
|
-
logger.info('Skipping algolia indexing due to missing environment variables');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
197
|
-
logger.info(bold(`Indexing ${pagesToRender.length} prose pages for algolia search`));
|
|
198
|
-
|
|
199
|
-
const objects = [];
|
|
200
|
-
for (const absHtmlPath of pagesToRender) {
|
|
201
|
-
const content = await readFile(absHtmlPath, 'utf-8');
|
|
202
|
-
const idx = indexHTML(content);
|
|
203
|
-
for (const entry of idx)
|
|
204
|
-
objects.push({
|
|
205
|
-
...entry,
|
|
206
|
-
source: absHtmlPath.slice(outputBasePath.length),
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
await buildProseIndex(appId, `${indexName}-prose`, algoliaWriteKey, objects);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
logger.error(`Failed to index prose content: ${err}`);
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|