@stainless-api/docs 0.1.0-beta.6 → 0.1.0-beta.61
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 +486 -0
- package/README.md +1 -1
- package/eslint-suppressions.json +52 -0
- package/locals.d.ts +16 -0
- package/package.json +45 -40
- package/plugin/assets/languages/csharp.svg +1 -0
- package/plugin/buildAlgoliaIndex.ts +32 -7
- package/plugin/cms/server.ts +130 -58
- package/plugin/cms/sidebar-builder.ts +7 -26
- package/plugin/cms/worker.ts +83 -5
- package/plugin/components/MethodDescription.tsx +54 -0
- package/plugin/components/SDKSelect.astro +7 -87
- package/plugin/components/SnippetCode.tsx +53 -8
- package/plugin/components/search/SearchAlgolia.astro +14 -26
- package/plugin/components/search/SearchIsland.tsx +38 -24
- package/plugin/create-playground.shim.tsx +3 -0
- package/plugin/generateAPIReferenceLink.ts +2 -2
- package/plugin/globalJs/ai-dropdown-options.ts +235 -0
- package/plugin/globalJs/code-snippets.ts +15 -8
- package/plugin/globalJs/copy.ts +81 -16
- package/plugin/globalJs/method-descriptions.ts +33 -0
- package/plugin/globalJs/navigation.ts +7 -4
- package/plugin/index.ts +179 -35
- package/plugin/languages.ts +5 -2
- package/plugin/loadPluginConfig.ts +121 -32
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +1 -1
- package/plugin/react/Routing.tsx +208 -104
- package/plugin/referencePlaceholderUtils.ts +1 -1
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +5 -1
- package/plugin/routes/Docs.astro +61 -83
- package/plugin/routes/Overview.astro +10 -16
- package/plugin/routes/markdown.ts +7 -7
- package/plugin/vendor/preview.worker.docs.js +19768 -17702
- package/plugin/vendor/templates/go.md +1 -1
- package/plugin/vendor/templates/python.md +1 -1
- package/resolveSrcFile.ts +10 -0
- package/scripts/vendor_deps.ts +5 -5
- package/shared/getSharedLogger.ts +15 -0
- package/shared/terminalUtils.ts +3 -0
- package/src/content.config.ts +9 -0
- package/stl-docs/components/AIDropdown.tsx +63 -0
- package/stl-docs/components/AiChatIsland.tsx +10 -0
- package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +10 -18
- package/stl-docs/components/Head.astro +16 -0
- package/stl-docs/components/Header.astro +6 -8
- package/stl-docs/components/PageFrame.astro +14 -0
- package/stl-docs/components/PageTitle.astro +82 -0
- package/stl-docs/components/TableOfContents.astro +34 -0
- package/stl-docs/components/ThemeSelect.astro +118 -136
- package/stl-docs/components/content-panel/ContentPanel.astro +16 -25
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +17 -1
- package/stl-docs/components/headers/StackedHeader.astro +29 -24
- package/stl-docs/components/icons/chat-gpt.tsx +17 -0
- package/stl-docs/components/icons/claude.tsx +10 -0
- package/stl-docs/components/icons/cursor.tsx +10 -0
- package/stl-docs/components/icons/gemini.tsx +19 -0
- package/stl-docs/components/icons/markdown.tsx +10 -0
- package/stl-docs/components/index.ts +1 -0
- package/stl-docs/components/mintlify-compat/Accordion.astro +7 -5
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +7 -3
- package/stl-docs/components/mintlify-compat/Columns.astro +40 -42
- package/stl-docs/components/mintlify-compat/Frame.astro +16 -18
- package/stl-docs/components/mintlify-compat/Step.astro +30 -32
- package/stl-docs/components/mintlify-compat/Steps.astro +8 -10
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +1 -1
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +1 -1
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +1 -1
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +1 -1
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +1 -1
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +1 -1
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +1 -1
- package/stl-docs/components/mintlify-compat/card.css +33 -35
- package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -70
- package/stl-docs/components/nav-tabs/NavTabs.astro +78 -80
- package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -8
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +3 -2
- package/stl-docs/components/pagination/HomeLink.astro +10 -0
- package/stl-docs/components/pagination/Pagination.astro +175 -0
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
- package/stl-docs/components/pagination/util.ts +71 -0
- package/stl-docs/components/scripts.ts +1 -0
- package/stl-docs/disableCalloutSyntax.ts +36 -0
- package/stl-docs/index.ts +130 -48
- package/stl-docs/loadStlDocsConfig.ts +44 -4
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +64 -0
- package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +34 -0
- package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
- package/stl-docs/proseSearchIndexing.ts +113 -0
- package/stl-docs/tabsMiddleware.ts +11 -3
- package/styles/code.css +108 -140
- package/styles/fonts.css +32 -17
- package/styles/links.css +11 -48
- package/styles/method-descriptions.css +36 -0
- package/styles/overrides.css +48 -60
- package/styles/page.css +92 -52
- package/styles/sdk_select.css +9 -7
- package/styles/search.css +58 -69
- package/styles/sidebar.css +211 -131
- package/styles/{variables.css → sl-variables.css} +3 -2
- package/styles/stldocs-variables.css +6 -0
- package/styles/toc.css +41 -34
- package/theme.css +10 -10
- package/tsconfig.json +2 -5
- package/virtual-module.d.ts +23 -3
- package/components/variables.css +0 -135
- /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
package/stl-docs/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import starlight from '@astrojs/starlight';
|
|
2
2
|
import react from '@astrojs/react';
|
|
3
|
+
import type { StarlightPlugin } from '@astrojs/starlight/types';
|
|
3
4
|
import { stainlessStarlight } from '../plugin';
|
|
5
|
+
import { disableCalloutSyntaxStarlightPlugin } from './disableCalloutSyntax';
|
|
4
6
|
|
|
5
7
|
import type { AstroIntegration } from 'astro';
|
|
6
8
|
|
|
@@ -16,11 +18,16 @@ import {
|
|
|
16
18
|
type StarlightSidebarConfig,
|
|
17
19
|
} from './loadStlDocsConfig';
|
|
18
20
|
import { buildVirtualModuleString } from '../shared/virtualModule';
|
|
19
|
-
|
|
20
|
-
import
|
|
21
|
+
import type * as StlDocsVirtualModule from 'virtual:stl-docs-virtual-module';
|
|
22
|
+
import { resolveSrcFile } from '../resolveSrcFile';
|
|
23
|
+
import { stainlessDocsMarkdownRenderer } from './proseMarkdown/proseMarkdownIntegration';
|
|
24
|
+
import { setSharedLogger } from '../shared/getSharedLogger';
|
|
25
|
+
import { stainlessDocsProseIndexing } from './proseSearchIndexing';
|
|
21
26
|
|
|
22
27
|
export * from '../plugin';
|
|
23
28
|
|
|
29
|
+
const COMPONENTS_FOLDER = '/stl-docs/components';
|
|
30
|
+
|
|
24
31
|
function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig) {
|
|
25
32
|
// We transform our tabs into a Starlight sidebar
|
|
26
33
|
// This gives them all the built-in features of Starlight (eg. auto-generated entries by directory)
|
|
@@ -40,21 +47,39 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
type ComponentOverrides = StarlightConfigDefined['components'];
|
|
43
|
-
const plugins = [...config.starlightCompat.plugins];
|
|
44
|
-
|
|
45
50
|
const componentOverrides: ComponentOverrides = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
PageFrame: resolveSrcFile(COMPONENTS_FOLDER, './PageFrame.astro'),
|
|
52
|
+
|
|
53
|
+
Head: resolveSrcFile(COMPONENTS_FOLDER, './Head.astro'),
|
|
54
|
+
Header: resolveSrcFile(COMPONENTS_FOLDER, './Header.astro'),
|
|
55
|
+
ThemeSelect: resolveSrcFile(COMPONENTS_FOLDER, './ThemeSelect.astro'),
|
|
56
|
+
|
|
57
|
+
Sidebar: resolveSrcFile(COMPONENTS_FOLDER, './sidebars/BaseSidebar.astro'),
|
|
58
|
+
ContentPanel: resolveSrcFile(COMPONENTS_FOLDER, './content-panel/ContentPanel.astro'),
|
|
59
|
+
TableOfContents: resolveSrcFile(COMPONENTS_FOLDER, './TableOfContents.astro'),
|
|
60
|
+
|
|
61
|
+
PageTitle: resolveSrcFile(COMPONENTS_FOLDER, './PageTitle.astro'),
|
|
62
|
+
Pagination: resolveSrcFile(COMPONENTS_FOLDER, './pagination/Pagination.astro'),
|
|
50
63
|
};
|
|
51
64
|
|
|
65
|
+
const plugins: StarlightPlugin[] = [
|
|
66
|
+
// Disable starlight callout syntax in favor of our own component
|
|
67
|
+
disableCalloutSyntaxStarlightPlugin,
|
|
68
|
+
];
|
|
69
|
+
|
|
52
70
|
if (config.apiReference !== null) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
plugins.push(
|
|
72
|
+
stainlessStarlight({
|
|
73
|
+
...config.apiReference,
|
|
74
|
+
contextMenu: config.contextMenu,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
componentOverrides.Sidebar = resolveSrcFile(COMPONENTS_FOLDER, './sidebars/SDKSelectSidebar.astro');
|
|
78
|
+
componentOverrides.Search = resolveSrcFile('/plugin/components/search/Search.astro');
|
|
56
79
|
}
|
|
57
80
|
|
|
81
|
+
plugins.push(...config.starlightCompat.plugins, ...config.plugins.map((p) => p(config)));
|
|
82
|
+
|
|
58
83
|
// TODO: re-add once we figure out what to do with the client router
|
|
59
84
|
// if (config.enableClientRouter) {
|
|
60
85
|
// // logger.info(`Client router is enabled`);
|
|
@@ -63,6 +88,8 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
|
|
|
63
88
|
// // logger.info(`Client router is disabled`);
|
|
64
89
|
// }
|
|
65
90
|
|
|
91
|
+
const userExpressiveCode = typeof config.expressiveCode === 'object' ? config.expressiveCode : {};
|
|
92
|
+
|
|
66
93
|
return starlight({
|
|
67
94
|
...config.starlightPassThrough,
|
|
68
95
|
sidebar,
|
|
@@ -86,68 +113,100 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
|
|
|
86
113
|
setupNavLinksInitial();
|
|
87
114
|
`,
|
|
88
115
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
],
|
|
117
|
+
routeMiddleware: [
|
|
118
|
+
...config.starlightCompat.routeMiddleware,
|
|
119
|
+
resolveSrcFile('/stl-docs/tabsMiddleware.ts'),
|
|
120
|
+
],
|
|
121
|
+
customCss: [resolveSrcFile('/theme.css'), ...config.customCss],
|
|
122
|
+
|
|
123
|
+
expressiveCode: {
|
|
124
|
+
...userExpressiveCode,
|
|
125
|
+
themes: userExpressiveCode.themes ?? ['github-light', 'github-dark'],
|
|
126
|
+
styleOverrides: {
|
|
127
|
+
...userExpressiveCode.styleOverrides,
|
|
128
|
+
textMarkers: {
|
|
129
|
+
insBackground: 'var(--stl-color-green-muted-background)',
|
|
130
|
+
insBorderColor: 'var(--stl-color-green-border)',
|
|
131
|
+
insDiffIndicatorColor: 'var(--stl-color-green-foreground-reduced)',
|
|
132
|
+
|
|
133
|
+
delBackground: 'var(--stl-color-red-muted-background)',
|
|
134
|
+
delBorderColor: 'var(--stl-color-red-border)',
|
|
135
|
+
delDiffIndicatorColor: 'var(--stl-color-red-foreground-reduced)',
|
|
136
|
+
|
|
137
|
+
markBackground: 'var(--stl-color-blue-muted-background)',
|
|
138
|
+
markBorderColor: 'var(--stl-color-blue-border)',
|
|
139
|
+
...userExpressiveCode.styleOverrides?.textMarkers,
|
|
99
140
|
},
|
|
100
141
|
},
|
|
101
|
-
|
|
102
|
-
routeMiddleware: [...config.starlightCompat.routeMiddleware, '@stainless-api/docs/tabsMiddleware'],
|
|
103
|
-
customCss: ['@stainless-api/docs/theme', ...config.customCss],
|
|
142
|
+
},
|
|
104
143
|
plugins,
|
|
105
144
|
});
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
function stainlessDocsIntegration(
|
|
109
|
-
|
|
147
|
+
function stainlessDocsIntegration(
|
|
148
|
+
config: NormalizedStainlessDocsConfig,
|
|
149
|
+
apiReferenceBasePath: string | null,
|
|
150
|
+
): AstroIntegration {
|
|
151
|
+
const virtualModules = new Map(
|
|
152
|
+
Object.entries({
|
|
153
|
+
'virtual:stl-docs-virtual-module': buildVirtualModuleString({
|
|
154
|
+
TABS: config.tabs,
|
|
155
|
+
SPLIT_TABS_ENABLED: config.splitTabsEnabled,
|
|
156
|
+
HEADER_LINKS: config.header.links,
|
|
157
|
+
HEADER_LAYOUT: config.header.layout,
|
|
158
|
+
ENABLE_CLIENT_ROUTER: config.enableClientRouter,
|
|
159
|
+
API_REFERENCE_BASE_PATH: apiReferenceBasePath,
|
|
160
|
+
ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
|
|
161
|
+
// TODO: do not duplicate this between both virtual modules
|
|
162
|
+
ENABLE_CONTEXT_MENU: config.contextMenu,
|
|
163
|
+
RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
|
|
164
|
+
} satisfies typeof StlDocsVirtualModule),
|
|
165
|
+
|
|
166
|
+
'virtual:stl-docs/components/AiChat.tsx': config.aiChat
|
|
167
|
+
? `export { default } from ${JSON.stringify(config.aiChat.chatComponentPath)};`
|
|
168
|
+
: // export null when no AI chat component is provided
|
|
169
|
+
`export default null;`,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
|
|
110
173
|
// The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
|
|
111
|
-
const
|
|
174
|
+
const resolveVirtualModuleId = (id: string) => `\0${id}`;
|
|
112
175
|
let redirects: NormalizedRedirectConfig | null = null;
|
|
113
176
|
|
|
114
177
|
return {
|
|
115
|
-
name: 'stl-docs-
|
|
178
|
+
name: 'stl-docs-astro',
|
|
116
179
|
hooks: {
|
|
117
180
|
'astro:config:setup': ({ updateConfig, command, config: astroConfig }) => {
|
|
118
|
-
//
|
|
119
|
-
//
|
|
181
|
+
// we only handle redirects for builds
|
|
182
|
+
// in dev, Astro handles them for us
|
|
120
183
|
if (command === 'build' && astroConfig.redirects) {
|
|
121
184
|
redirects = normalizeRedirects(astroConfig.redirects);
|
|
122
185
|
}
|
|
123
186
|
|
|
124
187
|
updateConfig({
|
|
125
188
|
vite: {
|
|
126
|
-
ssr: {
|
|
127
|
-
noExternal: ['@stainless-api/ui-primitives'],
|
|
128
|
-
},
|
|
129
|
-
optimizeDeps: { include: ['@stainless-api/ui-primitives'] },
|
|
130
189
|
plugins: [
|
|
131
190
|
{
|
|
132
191
|
name: 'stl-docs-vite',
|
|
133
192
|
resolveId(id) {
|
|
134
|
-
if (id
|
|
135
|
-
return resolvedId;
|
|
136
|
-
}
|
|
193
|
+
if (virtualModules.has(id)) return resolveVirtualModuleId(id);
|
|
137
194
|
},
|
|
138
195
|
load(id) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
TABS: config.tabs,
|
|
142
|
-
SPLIT_TABS_ENABLED: config.splitTabsEnabled,
|
|
143
|
-
HEADER_LINKS: config.header.links,
|
|
144
|
-
HEADER_LAYOUT: config.header.layout,
|
|
145
|
-
ENABLE_CLIENT_ROUTER: config.enableClientRouter,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
196
|
+
const bare = id.replace(/^\0/, '');
|
|
197
|
+
if (virtualModules.has(bare)) return virtualModules.get(bare);
|
|
148
198
|
},
|
|
149
199
|
},
|
|
150
200
|
],
|
|
201
|
+
optimizeDeps: {
|
|
202
|
+
include: config.aiChat
|
|
203
|
+
? [
|
|
204
|
+
'@stainless-api/docs-ai-chat > motion',
|
|
205
|
+
'@stainless-api/docs-ai-chat > react-markdown',
|
|
206
|
+
'@stainless-api/docs-ai-chat > react-syntax-highlighter',
|
|
207
|
+
]
|
|
208
|
+
: [],
|
|
209
|
+
},
|
|
151
210
|
},
|
|
152
211
|
build: {
|
|
153
212
|
...astroConfig.build,
|
|
@@ -158,7 +217,7 @@ function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroI
|
|
|
158
217
|
'astro:build:done': ({ dir }) => {
|
|
159
218
|
if (redirects !== null) {
|
|
160
219
|
const stainlessDir = join(dir.pathname, '_stainless');
|
|
161
|
-
mkdirSync(stainlessDir);
|
|
220
|
+
mkdirSync(stainlessDir, { recursive: true });
|
|
162
221
|
const outputPath = join(stainlessDir, 'redirects.json');
|
|
163
222
|
writeFileSync(outputPath, JSON.stringify(redirects, null, 2), {
|
|
164
223
|
encoding: 'utf-8',
|
|
@@ -169,6 +228,17 @@ function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroI
|
|
|
169
228
|
};
|
|
170
229
|
}
|
|
171
230
|
|
|
231
|
+
function sharedLoggerIntegration(): AstroIntegration {
|
|
232
|
+
return {
|
|
233
|
+
name: 'stainless',
|
|
234
|
+
hooks: {
|
|
235
|
+
'astro:config:setup': ({ logger }) => {
|
|
236
|
+
setSharedLogger(logger);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
172
242
|
export function stainlessDocs(config: StainlessDocsUserConfig) {
|
|
173
243
|
const normalizedConfigResult = parseStlDocsConfig(config);
|
|
174
244
|
if (normalizedConfigResult.result === 'error') {
|
|
@@ -178,9 +248,21 @@ export function stainlessDocs(config: StainlessDocsUserConfig) {
|
|
|
178
248
|
}
|
|
179
249
|
const normalizedConfig = normalizedConfigResult.config;
|
|
180
250
|
|
|
251
|
+
// TODO: need to refactor this, but this allows us to get the base path for the API reference _if_ it exists
|
|
252
|
+
// if it doesn't exist, the value of basePath is null.
|
|
253
|
+
// the stl-starlight virtual module has base path, but it's not available when there's no API reference
|
|
254
|
+
const hasApiReference = normalizedConfig.apiReference !== null;
|
|
255
|
+
let apiReferenceBasePath: string | null = null;
|
|
256
|
+
if (hasApiReference) {
|
|
257
|
+
apiReferenceBasePath = normalizedConfig.apiReference?.basePath ?? '/api';
|
|
258
|
+
}
|
|
259
|
+
|
|
181
260
|
return [
|
|
261
|
+
sharedLoggerIntegration(), // this **must** be first so it can set the shared logger used by our other integrations
|
|
182
262
|
react(),
|
|
183
263
|
stainlessDocsStarlightIntegration(normalizedConfig),
|
|
184
|
-
stainlessDocsIntegration(normalizedConfig),
|
|
264
|
+
stainlessDocsIntegration(normalizedConfig, apiReferenceBasePath),
|
|
265
|
+
stainlessDocsMarkdownRenderer({ enabled: normalizedConfig.enableProseMarkdownRendering }),
|
|
266
|
+
stainlessDocsProseIndexing(),
|
|
185
267
|
];
|
|
186
268
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StainlessStarlightUserConfig } from '../plugin/loadPluginConfig';
|
|
2
|
-
import type { StarlightUserConfig } from '@astrojs/starlight/types';
|
|
2
|
+
import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types';
|
|
3
3
|
import type { ButtonVariant } from '@stainless-api/ui-primitives';
|
|
4
4
|
import type { AnchorHTMLAttributes } from 'react';
|
|
5
5
|
import type starlight from '@astrojs/starlight';
|
|
@@ -32,6 +32,7 @@ type PassThroughStarlightConfigOptions = Pick<
|
|
|
32
32
|
| 'lastUpdated'
|
|
33
33
|
| 'pagination'
|
|
34
34
|
| 'sidebar'
|
|
35
|
+
| 'expressiveCode'
|
|
35
36
|
>;
|
|
36
37
|
|
|
37
38
|
type ExperimentalStarlightCompatibilityConfig = Pick<
|
|
@@ -45,7 +46,8 @@ type Tabs = {
|
|
|
45
46
|
sidebar?: SidebarEntry[];
|
|
46
47
|
/**
|
|
47
48
|
* Whether to hide the tab in the tab bar.
|
|
48
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* @default false
|
|
49
51
|
*/
|
|
50
52
|
hidden?: boolean;
|
|
51
53
|
}[];
|
|
@@ -67,7 +69,32 @@ export type StainlessDocsUserConfig = {
|
|
|
67
69
|
experimental?: {
|
|
68
70
|
starlightCompat?: ExperimentalStarlightCompatibilityConfig;
|
|
69
71
|
enableClientRouter?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Disable markdown rendering for prose content. Only disable this if it is causing issues.
|
|
74
|
+
*
|
|
75
|
+
* @default false
|
|
76
|
+
*/
|
|
77
|
+
disableProseMarkdownRendering?: boolean;
|
|
78
|
+
aiChat?: { chatComponentPath: string };
|
|
70
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Whether to show the context menu with options like "Copy as Markdown" and "Open in ChatGPT".
|
|
82
|
+
*
|
|
83
|
+
* @default true
|
|
84
|
+
*/
|
|
85
|
+
contextMenu?: boolean;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether to render page descriptions in prose page headers
|
|
89
|
+
*
|
|
90
|
+
* @default true
|
|
91
|
+
*/
|
|
92
|
+
renderPageDescriptions?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Stainless Docs plugins.
|
|
95
|
+
* Each plugin is a function that receives the normalized config and returns a Starlight plugin.
|
|
96
|
+
*/
|
|
97
|
+
plugins?: ((config: Exclude<NormalizedStainlessDocsConfig, 'plugins'>) => StarlightPlugin)[];
|
|
71
98
|
} & PassThroughStarlightConfigOptions;
|
|
72
99
|
|
|
73
100
|
type HeaderLayout = 'default' | 'stacked';
|
|
@@ -119,6 +146,12 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
119
146
|
description: userConfig.description,
|
|
120
147
|
tagline: userConfig.tagline,
|
|
121
148
|
logo: userConfig.logo,
|
|
149
|
+
favicon: userConfig.favicon,
|
|
150
|
+
disable404Route: userConfig.disable404Route,
|
|
151
|
+
editLink: userConfig.editLink,
|
|
152
|
+
locales: userConfig.locales,
|
|
153
|
+
lastUpdated: userConfig.lastUpdated,
|
|
154
|
+
pagination: userConfig.pagination,
|
|
122
155
|
},
|
|
123
156
|
starlightCompat: {
|
|
124
157
|
components: userConfig.experimental?.starlightCompat?.components ?? {},
|
|
@@ -128,6 +161,13 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
128
161
|
enableClientRouter: userConfig.experimental?.enableClientRouter ?? false,
|
|
129
162
|
apiReference: userConfig.apiReference ?? null,
|
|
130
163
|
sidebar: userConfig.sidebar,
|
|
164
|
+
enableProseMarkdownRendering:
|
|
165
|
+
userConfig.experimental?.disableProseMarkdownRendering === true ? false : true,
|
|
166
|
+
contextMenu: userConfig.contextMenu ?? true,
|
|
167
|
+
expressiveCode: userConfig.expressiveCode,
|
|
168
|
+
renderPageDescriptions: userConfig.renderPageDescriptions ?? true,
|
|
169
|
+
plugins: userConfig.plugins ?? [],
|
|
170
|
+
aiChat: userConfig.experimental?.aiChat,
|
|
131
171
|
};
|
|
132
172
|
|
|
133
173
|
return configWithDefaults;
|
|
@@ -136,11 +176,11 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
136
176
|
export type NormalizedStainlessDocsConfig = ReturnType<typeof normalizeConfig>;
|
|
137
177
|
|
|
138
178
|
/*
|
|
139
|
-
The goal of the code in this file is to take a user's config and normalize it.
|
|
179
|
+
The goal of the code in this file is to take a user's config and normalize it.
|
|
140
180
|
Specifically: we want a single complete config format used throughout the internals of the plugin.
|
|
141
181
|
|
|
142
182
|
We've tried to avoid any config values being optional/undefined. To accomplish this:
|
|
143
|
-
- Any optional config values should have their defaults set here: eg. basePath defaults to /api
|
|
183
|
+
- Any optional config values should have their defaults set here: eg. basePath defaults to /api
|
|
144
184
|
- If a field is only used in certain contexts, we make each context a discriminated union (see SpecRetrieverConfig)
|
|
145
185
|
- We prefer empty arrays over undefined/null
|
|
146
186
|
*/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
3
|
+
import { toMarkdown } from './toMarkdown';
|
|
4
|
+
import { resolveSrcFile } from '../../resolveSrcFile';
|
|
5
|
+
import { getSharedLogger } from '../../shared/getSharedLogger';
|
|
6
|
+
import { bold } from '../../shared/terminalUtils';
|
|
7
|
+
|
|
8
|
+
export function stainlessDocsMarkdownRenderer({ enabled }: { enabled: boolean }): AstroIntegration {
|
|
9
|
+
return {
|
|
10
|
+
name: 'stl-docs-md',
|
|
11
|
+
hooks: {
|
|
12
|
+
'astro:config:setup': ({ addMiddleware }) => {
|
|
13
|
+
if (enabled) {
|
|
14
|
+
addMiddleware({
|
|
15
|
+
entrypoint: resolveSrcFile('/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts'),
|
|
16
|
+
order: 'post',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
'astro:build:done': async ({ assets, logger: localLogger, dir }) => {
|
|
21
|
+
const logger = getSharedLogger({ fallback: localLogger });
|
|
22
|
+
if (!enabled) {
|
|
23
|
+
logger.info('Stainless Docs prose Markdown rendering is disabled, skipping...');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const starlightPagePatterns = ['/[...slug]'];
|
|
27
|
+
const pagesToRender = Array.from(assets.entries())
|
|
28
|
+
.filter(([k]) => {
|
|
29
|
+
if (starlightPagePatterns.includes(k)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
})
|
|
34
|
+
.map(([, v]) => v)
|
|
35
|
+
.flat()
|
|
36
|
+
.map((v) => v.pathname);
|
|
37
|
+
|
|
38
|
+
logger.info(bold(`Building ${pagesToRender.length} Markdown pages for prose content`));
|
|
39
|
+
|
|
40
|
+
const outputBasePath = dir.pathname;
|
|
41
|
+
|
|
42
|
+
let completedCount = 0;
|
|
43
|
+
for (const absHtmlPath of pagesToRender) {
|
|
44
|
+
const txt = await readFile(absHtmlPath, 'utf-8');
|
|
45
|
+
const md = await toMarkdown(txt);
|
|
46
|
+
if (md) {
|
|
47
|
+
const absMdPath = absHtmlPath.replace('.html', '.md');
|
|
48
|
+
await writeFile(absMdPath, md, 'utf-8');
|
|
49
|
+
|
|
50
|
+
completedCount++;
|
|
51
|
+
|
|
52
|
+
const relHtmlPath = absHtmlPath.replace(outputBasePath, '');
|
|
53
|
+
const relMdPath = absMdPath.replace(outputBasePath, '');
|
|
54
|
+
|
|
55
|
+
logger.info(`(${completedCount}/${pagesToRender.length}) ${relHtmlPath} -> ${relMdPath}`);
|
|
56
|
+
} else {
|
|
57
|
+
logger.error(`Failed to render ${absHtmlPath} as Markdown`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
2
|
+
import { toMarkdown } from './toMarkdown';
|
|
3
|
+
import { API_REFERENCE_BASE_PATH } from 'virtual:stl-docs-virtual-module';
|
|
4
|
+
|
|
5
|
+
// this is only run in `astro dev` for rendering prose content as Markdown on the fly.
|
|
6
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
7
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
8
|
+
if (!import.meta.env.DEV) {
|
|
9
|
+
return next();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (API_REFERENCE_BASE_PATH && context.url.pathname.startsWith(API_REFERENCE_BASE_PATH)) {
|
|
13
|
+
// handled by the API reference API route in stl-starlight plugin
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!context.url.pathname.endsWith('/index.md')) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const htmlUrl = new URL(context.url.pathname.replace('index.md', ''), context.url);
|
|
22
|
+
const resp = await fetch(htmlUrl);
|
|
23
|
+
if (!resp.ok) {
|
|
24
|
+
return new Response('Failed to fetch HTML', { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
const html = await resp.text();
|
|
27
|
+
const md = await toMarkdown(html);
|
|
28
|
+
|
|
29
|
+
if (!md) {
|
|
30
|
+
return new Response('Failed to render Markdown', { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Response(md, { status: 200 });
|
|
34
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import rehypeParse from 'rehype-parse';
|
|
3
|
+
import rehypeRemark from 'rehype-remark';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import remarkStringify from 'remark-stringify';
|
|
6
|
+
import { HTMLElement, parse } from 'node-html-parser';
|
|
7
|
+
|
|
8
|
+
type PaginationLink = {
|
|
9
|
+
href: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PaginationItems = {
|
|
14
|
+
prev: PaginationLink | null;
|
|
15
|
+
next: PaginationLink | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function parsePaginationLink(footer: HTMLElement, rel: 'next' | 'prev'): PaginationLink | null {
|
|
19
|
+
const link = footer.querySelector(`.pagination-links a[rel="${rel}"]`);
|
|
20
|
+
if (!link) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const title = link.querySelector('.link-title');
|
|
25
|
+
if (!title) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const href = link.getAttribute('href');
|
|
30
|
+
if (!href) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
href,
|
|
36
|
+
label: title.text,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRelativeLink(href: string) {
|
|
41
|
+
return href.startsWith('/');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasExtension(href: string) {
|
|
45
|
+
return href.includes('.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeTrailingSlash(href: string) {
|
|
49
|
+
return href.replace(/\/$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeMarkdownLinks(el: HTMLElement) {
|
|
53
|
+
el.querySelectorAll('a').forEach((a) => {
|
|
54
|
+
const href = a.getAttribute('href');
|
|
55
|
+
if (!href) {
|
|
56
|
+
return a;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isRelativeLink(href) && !hasExtension(href)) {
|
|
60
|
+
if (href === '/') {
|
|
61
|
+
a.setAttribute('href', '/index.md');
|
|
62
|
+
} else {
|
|
63
|
+
a.setAttribute('href', `${removeTrailingSlash(href)}/index.md`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return a;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeHiddenElements(el: HTMLElement) {
|
|
71
|
+
const hiddenSelectors = ['.sl-anchor-link'];
|
|
72
|
+
for (const selector of hiddenSelectors) {
|
|
73
|
+
const hiddenElements = el.querySelectorAll(selector);
|
|
74
|
+
for (const hiddenElement of hiddenElements) {
|
|
75
|
+
hiddenElement.remove();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function toMarkdown(html: string) {
|
|
81
|
+
const root = parse(html);
|
|
82
|
+
|
|
83
|
+
const mainEl = root.querySelector('main');
|
|
84
|
+
|
|
85
|
+
if (!mainEl) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
makeMarkdownLinks(mainEl);
|
|
90
|
+
|
|
91
|
+
const footer = mainEl.querySelector('footer');
|
|
92
|
+
|
|
93
|
+
const markdownContentEl = mainEl.querySelector('.sl-markdown-content');
|
|
94
|
+
if (!markdownContentEl) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
removeHiddenElements(markdownContentEl);
|
|
99
|
+
|
|
100
|
+
const articleContent = markdownContentEl.innerHTML;
|
|
101
|
+
|
|
102
|
+
const paginationLinks: PaginationItems = {
|
|
103
|
+
prev: null,
|
|
104
|
+
next: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (footer) {
|
|
108
|
+
paginationLinks.prev = parsePaginationLink(footer, 'prev');
|
|
109
|
+
paginationLinks.next = parsePaginationLink(footer, 'next');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let md = (
|
|
113
|
+
await unified()
|
|
114
|
+
.use(rehypeParse, { fragment: true }) // parse HTML
|
|
115
|
+
.use(rehypeRemark) // rehype (HTML) -> remark (MD AST)
|
|
116
|
+
.use(remarkGfm) // tables, strikethrough, autolinks, etc.
|
|
117
|
+
.use(remarkStringify, {
|
|
118
|
+
fences: true,
|
|
119
|
+
bullet: '-',
|
|
120
|
+
listItemIndent: 'one',
|
|
121
|
+
rule: '-',
|
|
122
|
+
})
|
|
123
|
+
.process(articleContent)
|
|
124
|
+
).toString();
|
|
125
|
+
|
|
126
|
+
const title = root.querySelector('title')?.textContent;
|
|
127
|
+
const description = root.querySelector('meta[name="description"]')?.attributes.content;
|
|
128
|
+
const lastUpdated = root.querySelector('.meta time')?.attributes.datetime;
|
|
129
|
+
|
|
130
|
+
// let htmlUrl = url.toString().replace('.md', '');
|
|
131
|
+
// if (htmlUrl.endsWith('/index')) {
|
|
132
|
+
// htmlUrl = htmlUrl.replace('/index', '');
|
|
133
|
+
// }
|
|
134
|
+
|
|
135
|
+
md = [
|
|
136
|
+
'---',
|
|
137
|
+
`title: ${title}`,
|
|
138
|
+
description ? `description: ${description}` : [],
|
|
139
|
+
lastUpdated ? `lastUpdated: ${lastUpdated}` : [],
|
|
140
|
+
// `source_url:`,
|
|
141
|
+
// ` html: ${htmlUrl}`,
|
|
142
|
+
// ` md: ${url.toString()}`,
|
|
143
|
+
'---\n',
|
|
144
|
+
md,
|
|
145
|
+
]
|
|
146
|
+
.flat()
|
|
147
|
+
.join('\n');
|
|
148
|
+
|
|
149
|
+
if (paginationLinks.prev) {
|
|
150
|
+
md += `\n\n[Previous](${paginationLinks.prev.href})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (paginationLinks.next) {
|
|
154
|
+
md += `\n\n[Next](${paginationLinks.next.href})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return md;
|
|
158
|
+
}
|