@stainless-api/docs 0.1.0-beta.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/.env.example +1 -0
- package/CHANGELOG.md +13 -0
- package/README.md +11 -0
- package/components/variables.css +139 -0
- package/eslint.config.js +10 -0
- package/package.json +74 -0
- package/plugin/assets/fonts/geist/OFL.txt +93 -0
- package/plugin/assets/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-italic-latin.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-latin-ext.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-latin.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
- package/plugin/assets/fonts/geist/geist-mono-latin.woff2 +0 -0
- package/plugin/assets/languages/curl.svg +10 -0
- package/plugin/assets/languages/go.svg +4 -0
- package/plugin/assets/languages/java.svg +7 -0
- package/plugin/assets/languages/kotlin.svg +10 -0
- package/plugin/assets/languages/powershell.svg +3 -0
- package/plugin/assets/languages/python.svg +19 -0
- package/plugin/assets/languages/ruby.svg +125 -0
- package/plugin/assets/languages/terraform.svg +5 -0
- package/plugin/assets/languages/typescript.svg +11 -0
- package/plugin/assets/stainless-logo-dark.png +0 -0
- package/plugin/assets/stainless-logo.png +0 -0
- package/plugin/buildAlgoliaIndex.ts +72 -0
- package/plugin/cms/client.ts +62 -0
- package/plugin/cms/server.ts +268 -0
- package/plugin/cms/sidebar-builder.ts +420 -0
- package/plugin/cms/worker.ts +122 -0
- package/plugin/components/SDKSelect.astro +154 -0
- package/plugin/components/SnippetCode.tsx +212 -0
- package/plugin/components/search/Search.astro +6 -0
- package/plugin/components/search/SearchAlgolia.astro +87 -0
- package/plugin/components/search/SearchIsland.tsx +100 -0
- package/plugin/generateAPIReferenceLink.ts +71 -0
- package/plugin/globalJs/ai-dropdown.ts +57 -0
- package/plugin/globalJs/code-snippets.ts +87 -0
- package/plugin/globalJs/copy.ts +37 -0
- package/plugin/globalJs/navigation.ts +81 -0
- package/plugin/globalJs/tooltip.ts +32 -0
- package/plugin/helpers/getPageLoadEvent.ts +8 -0
- package/plugin/index.ts +308 -0
- package/plugin/languages.ts +67 -0
- package/plugin/loadPluginConfig.ts +273 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +5 -0
- package/plugin/middlewareBuilder/stlStarlightMiddleware.ts +5 -0
- package/plugin/react/Routing.tsx +435 -0
- package/plugin/referencePlaceholderUtils.ts +82 -0
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +50 -0
- package/plugin/routes/Docs.astro +171 -0
- package/plugin/routes/DocsStatic.astro +14 -0
- package/plugin/routes/Overview.astro +67 -0
- package/plugin/routes/markdown.ts +58 -0
- package/plugin/vendor/preview.worker.docs.js +21657 -0
- package/plugin/vendor/templates/go.md +314 -0
- package/plugin/vendor/templates/java.md +87 -0
- package/plugin/vendor/templates/kotlin.md +87 -0
- package/plugin/vendor/templates/node.md +233 -0
- package/plugin/vendor/templates/python.md +249 -0
- package/plugin/vendor/templates/ruby.md +145 -0
- package/plugin/vendor/templates/terraform.md +60 -0
- package/plugin/vendor/templates/typescript.md +317 -0
- package/scripts/vendor_deps.ts +50 -0
- package/shared/virtualModule.ts +7 -0
- package/stl-docs/components/APIReferenceAIDropdown.tsx +86 -0
- package/stl-docs/components/ClientRouterHead.astro +41 -0
- package/stl-docs/components/Header.astro +91 -0
- package/stl-docs/components/Sidebar.astro +11 -0
- package/stl-docs/components/ThemeSelect.astro +225 -0
- package/stl-docs/components/content-panel/ContentBreadcrumbs.tsx +84 -0
- package/stl-docs/components/content-panel/ContentPanel.astro +72 -0
- package/stl-docs/components/content-panel/ProseAIDropdown.tsx +64 -0
- package/stl-docs/components/headers/DefaultHeader.astro +36 -0
- package/stl-docs/components/headers/HeaderLinks.astro +16 -0
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +49 -0
- package/stl-docs/components/headers/StackedHeader.astro +75 -0
- package/stl-docs/components/mintlify-compat/Accordion.astro +46 -0
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +25 -0
- package/stl-docs/components/mintlify-compat/Card.tsx +32 -0
- package/stl-docs/components/mintlify-compat/Columns.astro +66 -0
- package/stl-docs/components/mintlify-compat/Frame.astro +37 -0
- package/stl-docs/components/mintlify-compat/Step.astro +58 -0
- package/stl-docs/components/mintlify-compat/Steps.astro +17 -0
- package/stl-docs/components/mintlify-compat/Tab.astro +13 -0
- package/stl-docs/components/mintlify-compat/Tabs.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +7 -0
- package/stl-docs/components/mintlify-compat/callouts/index.ts +9 -0
- package/stl-docs/components/mintlify-compat/card.css +44 -0
- package/stl-docs/components/mintlify-compat/index.ts +15 -0
- package/stl-docs/components/nav-tabs/NavDropdown.astro +106 -0
- package/stl-docs/components/nav-tabs/NavTabs.astro +165 -0
- package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +62 -0
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +14 -0
- package/stl-docs/index.ts +174 -0
- package/stl-docs/loadStlDocsConfig.ts +160 -0
- package/stl-docs/redirects.ts +33 -0
- package/stl-docs/tabsMiddleware.ts +183 -0
- package/styles/code.css +189 -0
- package/styles/fonts.css +68 -0
- package/styles/links.css +51 -0
- package/styles/mintlify-compat.css +1 -0
- package/styles/overrides.css +79 -0
- package/styles/page.css +76 -0
- package/styles/sdk_select.css +11 -0
- package/styles/search.css +85 -0
- package/styles/sidebar.css +168 -0
- package/styles/toc.css +42 -0
- package/styles/variables.css +18 -0
- package/theme.css +15 -0
- package/tsconfig.json +18 -0
- package/virtual-module.d.ts +43 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SnippetCodeProps,
|
|
3
|
+
SnippetContainerProps,
|
|
4
|
+
SnippetRequestContainerProps,
|
|
5
|
+
} from '@stainless-api/docs-ui/src/components';
|
|
6
|
+
import { useHighlight, useLanguage } from '@stainless-api/docs-ui/src/contexts';
|
|
7
|
+
import style from '@stainless-api/docs-ui/src/style';
|
|
8
|
+
import * as cheerio from 'cheerio/slim';
|
|
9
|
+
import { EXPERIMENTAL_COLLAPSIBLE_SNIPPETS } from 'virtual:stl-starlight-virtual-module';
|
|
10
|
+
import clsx from 'clsx';
|
|
11
|
+
/*
|
|
12
|
+
* This may be replaced by additional data from the sdk.
|
|
13
|
+
* Without information from the sdk, we use simple heuristics per language.
|
|
14
|
+
*/
|
|
15
|
+
function getCollapsedRanges(content: string, language: string, signature?: string): [number, number][] {
|
|
16
|
+
if (!signature) return [];
|
|
17
|
+
const raw = content.split(/\r?\n/);
|
|
18
|
+
|
|
19
|
+
const sigIdx = raw.findIndex((l) => l.includes(signature));
|
|
20
|
+
if (sigIdx < 0) return [];
|
|
21
|
+
|
|
22
|
+
let finalIndex = undefined;
|
|
23
|
+
if (language === 'kotlin' || language === 'java') {
|
|
24
|
+
finalIndex = raw.findIndex((l, i) => i > sigIdx && l.trim() === '}');
|
|
25
|
+
} else if (language === 'python') {
|
|
26
|
+
finalIndex = raw.findIndex((l, i) => i > sigIdx && l.trim().startsWith('print'));
|
|
27
|
+
} else if (language === 'go') {
|
|
28
|
+
finalIndex = raw.findIndex((l, i) => i > sigIdx && l.trim().startsWith('if err'));
|
|
29
|
+
} else {
|
|
30
|
+
// Fallback to empty line
|
|
31
|
+
finalIndex = raw.findIndex((l, i) => i > sigIdx && l.trim() === '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (finalIndex > 0) {
|
|
35
|
+
return [
|
|
36
|
+
[0, sigIdx],
|
|
37
|
+
[finalIndex, raw.length],
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return [[0, sigIdx]];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* While collapsed, we may need to remove indentation.
|
|
46
|
+
* This function allows us to remove any level of indentation from a given line.
|
|
47
|
+
*/
|
|
48
|
+
function wrapFirstNSpaces($line: cheerio.Cheerio<any>, n: number) {
|
|
49
|
+
const $firstSpan = $line.children('span').first();
|
|
50
|
+
if ($firstSpan.length === 0) return;
|
|
51
|
+
|
|
52
|
+
const inner = $firstSpan.html() ?? '';
|
|
53
|
+
const m = inner.match(new RegExp(`^( {1,${n}})`));
|
|
54
|
+
if (!m) return;
|
|
55
|
+
|
|
56
|
+
const lead = m[1];
|
|
57
|
+
$firstSpan.html(`<span class="leading-ws">${lead}</span>${inner.slice(lead.length)}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* This function calculates the counter offset for the given ranges.
|
|
62
|
+
* This only works when there is between 0 and 2 ranges. If we add support
|
|
63
|
+
* for more than 2 ranges, we will need to re-work how we calculate offsets.
|
|
64
|
+
* It may be that we will no longer be able to use css counters, and will need
|
|
65
|
+
* to find a different approach.
|
|
66
|
+
*/
|
|
67
|
+
function getCounterOffset(ranges: [number, number][]) {
|
|
68
|
+
let offset = 0;
|
|
69
|
+
const firstRange = ranges.length > 0 ? ranges[0] : null;
|
|
70
|
+
if (firstRange && firstRange[0] === 0) {
|
|
71
|
+
offset = firstRange[1];
|
|
72
|
+
}
|
|
73
|
+
return offset;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function condensedShikiHtmlFull(docHtml: string, language: string, ranges: [number, number][] = []) {
|
|
77
|
+
const $ = cheerio.load(docHtml, null, false);
|
|
78
|
+
|
|
79
|
+
const counterOffset = getCounterOffset(ranges);
|
|
80
|
+
|
|
81
|
+
$('.shiki').attr('style', `counter-reset: codeblock-line ${Math.max(counterOffset - 1, 0)}`);
|
|
82
|
+
|
|
83
|
+
const $code = $('pre code').first();
|
|
84
|
+
if ($code.length === 0) return docHtml;
|
|
85
|
+
|
|
86
|
+
const lines = $code.find('span.line').toArray();
|
|
87
|
+
|
|
88
|
+
const out: string[] = [];
|
|
89
|
+
let didPushEllipsis = false;
|
|
90
|
+
|
|
91
|
+
lines.forEach((lineEl, idx) => {
|
|
92
|
+
const $line = $(lineEl);
|
|
93
|
+
const existsInRange = ranges.some(([start, end]) => idx >= start && idx < end);
|
|
94
|
+
if (existsInRange) {
|
|
95
|
+
$line.addClass('hidden');
|
|
96
|
+
} else {
|
|
97
|
+
didPushEllipsis = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (language === 'java') {
|
|
101
|
+
wrapFirstNSpaces($line, 8);
|
|
102
|
+
} else if (language === 'kotlin') {
|
|
103
|
+
wrapFirstNSpaces($line, 4);
|
|
104
|
+
} else if (language === 'go') {
|
|
105
|
+
wrapFirstNSpaces($line, 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$line.append('<span class="nl">\n</span>');
|
|
109
|
+
if (!didPushEllipsis && existsInRange) {
|
|
110
|
+
out.push('<span class="line ellipsis">…\n</span>');
|
|
111
|
+
didPushEllipsis = true;
|
|
112
|
+
}
|
|
113
|
+
out.push($.html(lineEl)!);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
$code.html(out.join(''));
|
|
117
|
+
return $.html();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function useIsCollapsible({ signature }: { signature?: string }): boolean {
|
|
121
|
+
const language = useLanguage();
|
|
122
|
+
|
|
123
|
+
return Boolean(EXPERIMENTAL_COLLAPSIBLE_SNIPPETS && signature && language);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function SnippetRequestContainer({ children, signature }: SnippetRequestContainerProps) {
|
|
127
|
+
const isCollapsible = useIsCollapsible({ signature });
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="stl-snippet-request-container">
|
|
131
|
+
{children}
|
|
132
|
+
{signature && isCollapsible && (
|
|
133
|
+
<button
|
|
134
|
+
className={clsx(style.Button, style.ButtonSecondary, 'stl-snippet-expand-button')}
|
|
135
|
+
id="stl-snippet-expand-button"
|
|
136
|
+
>
|
|
137
|
+
Show more
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function SnippetContainer({ children, signature }: SnippetContainerProps) {
|
|
145
|
+
const isCollapsible = useIsCollapsible({ signature });
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
className={clsx(
|
|
150
|
+
style.Snippet,
|
|
151
|
+
isCollapsible ? 'stl-snippet-collapsible' : 'stl-snippet-non-collapsible',
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function CondensibleSnippetCode({
|
|
160
|
+
content,
|
|
161
|
+
language,
|
|
162
|
+
signature,
|
|
163
|
+
highlighted,
|
|
164
|
+
}: SnippetCodeProps & { signature: string; highlighted: string; language: string }) {
|
|
165
|
+
const ranges = getCollapsedRanges(content, language, signature);
|
|
166
|
+
const html = condensedShikiHtmlFull(highlighted, language, ranges);
|
|
167
|
+
const offset = getCounterOffset(ranges);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className={clsx(style.SnippetCode, 'stl-snippet-code-is-collapsed')}
|
|
172
|
+
data-snippet-expanded-offset={offset}
|
|
173
|
+
>
|
|
174
|
+
<pre className={style.SnippetCodeContent} data-stldocs-copy-content>
|
|
175
|
+
<code
|
|
176
|
+
className={language === 'json' ? 'snippet-json' : 'snippet'}
|
|
177
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
178
|
+
/>
|
|
179
|
+
</pre>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function SnippetCode({ content, signature, language: forcedLanguage }: SnippetCodeProps) {
|
|
185
|
+
const lang = useLanguage();
|
|
186
|
+
const language = forcedLanguage || lang;
|
|
187
|
+
let highlighted = useHighlight(content, language);
|
|
188
|
+
|
|
189
|
+
const isCollapsible = useIsCollapsible({ signature });
|
|
190
|
+
|
|
191
|
+
let offset = 0;
|
|
192
|
+
|
|
193
|
+
if (isCollapsible && language) {
|
|
194
|
+
const ranges = getCollapsedRanges(content, language, signature);
|
|
195
|
+
highlighted = condensedShikiHtmlFull(highlighted, language, ranges);
|
|
196
|
+
offset = getCounterOffset(ranges);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div
|
|
201
|
+
className={clsx(style.SnippetCode, isCollapsible && 'stl-snippet-code-is-collapsed')}
|
|
202
|
+
data-snippet-expanded-offset={offset}
|
|
203
|
+
>
|
|
204
|
+
<pre className={style.SnippetCodeContent} data-stldocs-copy-content>
|
|
205
|
+
<code
|
|
206
|
+
className={(language as string) === 'json' ? 'snippet-json' : 'snippet'}
|
|
207
|
+
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
208
|
+
/>
|
|
209
|
+
</pre>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from '@astrojs/starlight/components';
|
|
3
|
+
import { DocsSearch } from './SearchIsland';
|
|
4
|
+
import { SEARCH } from 'virtual:stl-starlight-virtual-module';
|
|
5
|
+
|
|
6
|
+
import '@stainless-api/docs-ui/src/styles/resets.css';
|
|
7
|
+
import '@stainless-api/docs-ui/src/styles/primitives.css';
|
|
8
|
+
import '@stainless-api/docs-ui/src/styles/main.css';
|
|
9
|
+
import '@stainless-api/docs-ui/src/styles/snippets.css';
|
|
10
|
+
import '@stainless-api/docs-ui/src/styles/search.css';
|
|
11
|
+
import '../../../components/variables.css';
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<site-search>
|
|
15
|
+
<button
|
|
16
|
+
popovertarget="stldocs-search"
|
|
17
|
+
data-open-modal
|
|
18
|
+
aria-label={Astro.locals.t('search.label')}
|
|
19
|
+
aria-keyshortcuts="Control+K"
|
|
20
|
+
class="stldocs-button stldocs-button-secondary"
|
|
21
|
+
>
|
|
22
|
+
<Icon name="magnifier" />
|
|
23
|
+
<span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
|
|
24
|
+
<kbd class="sl-hidden md:sl-flex" style="display: none;">
|
|
25
|
+
<kbd>{Astro.locals.t('search.ctrlKey')}</kbd><kbd>K</kbd>
|
|
26
|
+
</kbd>
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<DocsSearch
|
|
30
|
+
client:only="react"
|
|
31
|
+
currentPath={Astro.url.pathname}
|
|
32
|
+
settings={{
|
|
33
|
+
searchKey: import.meta.env['PUBLIC_ALGOLIA_SEARCH_KEY'],
|
|
34
|
+
indexName: import.meta.env['PUBLIC_ALGOLIA_INDEX'],
|
|
35
|
+
assistant: import.meta.env['PUBLIC_ALGOLIA_ASSISTANT'],
|
|
36
|
+
appId: import.meta.env['PUBLIC_ALGOLIA_APP_ID'],
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
</site-search>
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
SEARCH?.enableAISearch === true && (
|
|
43
|
+
<button id="chat-button" popovertarget="stldocs-chat" data-open-modal>
|
|
44
|
+
<Icon name="comment" />
|
|
45
|
+
</button>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
#chat-button {
|
|
51
|
+
background: var(--sl-color-bg-ui);
|
|
52
|
+
border: 1px solid var(--sl-color-hairline);
|
|
53
|
+
height: 2.25rem;
|
|
54
|
+
width: 2.25rem;
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
border: 1px solid rgb(64, 64, 64);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
61
|
+
|
|
62
|
+
<script is:inline>
|
|
63
|
+
function setupShortcut() {
|
|
64
|
+
const openBtn = document.querySelector('button[data-open-modal]');
|
|
65
|
+
const shortcut = openBtn?.querySelector('kbd');
|
|
66
|
+
|
|
67
|
+
if (!openBtn || !(shortcut instanceof HTMLElement)) return;
|
|
68
|
+
|
|
69
|
+
const platformKey = shortcut.querySelector('kbd');
|
|
70
|
+
if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
|
|
71
|
+
platformKey.textContent = '⌘';
|
|
72
|
+
openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
shortcut.style.display = '';
|
|
76
|
+
}
|
|
77
|
+
document.addEventListener('astro:page-load', setupShortcut);
|
|
78
|
+
setupShortcut();
|
|
79
|
+
|
|
80
|
+
// important: this fn should not be called inside astro:page-load
|
|
81
|
+
window.addEventListener('keydown', (e) => {
|
|
82
|
+
if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'k') {
|
|
83
|
+
document.getElementById('stldocs-search')?.togglePopover();
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
</script>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { BASE_PATH, SEARCH } from 'virtual:stl-starlight-virtual-module';
|
|
3
|
+
import { parseRoute, generateRoute } from '@stainless-api/docs-ui/src/routing';
|
|
4
|
+
import { SearchModal } from '@stainless-api/docs-ui/src/search/index';
|
|
5
|
+
import { ChatModal } from '@stainless-api/docs-ui/src/components/chat';
|
|
6
|
+
import * as Markdoc from '@markdoc/markdoc';
|
|
7
|
+
import { createHighlighter } from 'shiki';
|
|
8
|
+
import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DocsProvider,
|
|
12
|
+
MarkdownProvider,
|
|
13
|
+
NavigationProvider,
|
|
14
|
+
SearchProvider,
|
|
15
|
+
} from '@stainless-api/docs-ui/src/contexts';
|
|
16
|
+
import type { SearchSettings } from '@stainless-api/docs-ui/src/search/types';
|
|
17
|
+
|
|
18
|
+
let $$highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null = null;
|
|
19
|
+
async function getHighlighter() {
|
|
20
|
+
if ($$highlighter === null) {
|
|
21
|
+
$$highlighter = await createHighlighter({
|
|
22
|
+
themes: ['github-dark'],
|
|
23
|
+
langs: ['typescript', 'python', 'go', 'java', 'kotlin', 'ruby'],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return $$highlighter;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function createMarkdownRenderer() {
|
|
31
|
+
const highlighter = await getHighlighter();
|
|
32
|
+
const markdocConfig: Markdoc.Config = {
|
|
33
|
+
nodes: {
|
|
34
|
+
document: { ...Markdoc.nodes.document, render: '' },
|
|
35
|
+
fence: {
|
|
36
|
+
...Markdoc.nodes.fence,
|
|
37
|
+
transform(node, config) {
|
|
38
|
+
const attributes = node.transformAttributes(config);
|
|
39
|
+
const lang = node.attributes.language === 'node' ? 'typescript' : node.attributes.language;
|
|
40
|
+
const code = highlighter.codeToTokens(node.attributes.content, { lang, theme: 'github-dark' });
|
|
41
|
+
|
|
42
|
+
return new Markdoc.Tag(
|
|
43
|
+
'pre',
|
|
44
|
+
attributes,
|
|
45
|
+
code.tokens.map(
|
|
46
|
+
(line) =>
|
|
47
|
+
new Markdoc.Tag('span', { class: 'line' }, [
|
|
48
|
+
...line.map(
|
|
49
|
+
(token) => new Markdoc.Tag('span', { style: `color:${token.color}` }, [token.content]),
|
|
50
|
+
),
|
|
51
|
+
new Markdoc.Tag('span', { class: 'nl' }, ['\n']),
|
|
52
|
+
]),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
link: {
|
|
58
|
+
...Markdoc.nodes.link,
|
|
59
|
+
transform(node, config) {
|
|
60
|
+
const children = node.transformChildren(config);
|
|
61
|
+
const attrs = node.transformAttributes(config);
|
|
62
|
+
const href = attrs['href'].replace('docs://BASE_PATH', BASE_PATH);
|
|
63
|
+
return new Markdoc.Tag(this.render, { ...attrs, href }, children);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (content: string) => {
|
|
70
|
+
const ast = Markdoc.parse(content);
|
|
71
|
+
const transformed = Markdoc.transform(ast, markdocConfig);
|
|
72
|
+
return Markdoc.renderers.html(transformed);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function DocsSearch({ settings, currentPath }: { settings: SearchSettings; currentPath: string }) {
|
|
77
|
+
const renderMarkdown = React.use(createMarkdownRenderer());
|
|
78
|
+
const { stainlessPath, language } = parseRoute(BASE_PATH, currentPath);
|
|
79
|
+
const pageFind = import.meta.env.DEV ? undefined : '/pagefind/pagefind.js';
|
|
80
|
+
|
|
81
|
+
function handleSelect(path: string) {
|
|
82
|
+
const url = path.startsWith('/') ? path : generateRoute(BASE_PATH, language, path);
|
|
83
|
+
if (url) window.location.href = url;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<DocsProvider spec={null} language={language}>
|
|
88
|
+
<NavigationProvider basePath="/" selectedPath={stainlessPath}>
|
|
89
|
+
<MarkdownProvider render={renderMarkdown}>
|
|
90
|
+
<SearchProvider onSelect={handleSelect} pageFind={pageFind} settings={settings}>
|
|
91
|
+
<div className="stldocs-root">
|
|
92
|
+
<SearchModal id="stldocs-search" />
|
|
93
|
+
{SEARCH?.enableAISearch === true && <ChatModal id="stldocs-chat" />}
|
|
94
|
+
</div>
|
|
95
|
+
</SearchProvider>
|
|
96
|
+
</MarkdownProvider>
|
|
97
|
+
</NavigationProvider>
|
|
98
|
+
</DocsProvider>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// This is probably temporary, but it fills in functionality needed for Mintlify imports
|
|
2
|
+
|
|
3
|
+
import type { StarlightRouteData } from '@astrojs/starlight/route-data';
|
|
4
|
+
import type * as SDKJSON from '~/lib/json-spec-v2/types';
|
|
5
|
+
import { walkTree } from '@stainless-api/docs-ui/src/routing';
|
|
6
|
+
|
|
7
|
+
const INTERNAL_REFERENCE_ENTRY_MARKER = 'STL_STARLIGHT_API_REFERENCE_METHOD_LINK_PLACEHOLDER';
|
|
8
|
+
|
|
9
|
+
type SidebarEntry = StarlightRouteData['sidebar'][number];
|
|
10
|
+
|
|
11
|
+
type SidebarLink = Extract<SidebarEntry, { href: string }>;
|
|
12
|
+
|
|
13
|
+
export function getMethodFromSDKJSON(spec: SDKJSON.Spec, endpoint: string) {
|
|
14
|
+
for (const entry of walkTree(spec)) {
|
|
15
|
+
if (entry.data.kind === 'http_method' && entry.data.endpoint === endpoint) {
|
|
16
|
+
return entry.data;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Endpoint ${endpoint} not found in API`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function recursiveReplacePlaceholderItems(
|
|
23
|
+
sidebar: SidebarEntry[],
|
|
24
|
+
modifyFn: (entry: SidebarLink, props: { endpoint: string; label?: string }) => void,
|
|
25
|
+
) {
|
|
26
|
+
for (const entry of sidebar) {
|
|
27
|
+
const endpoint = 'attrs' in entry && entry.attrs?.['data-stldocs-endpoint'];
|
|
28
|
+
if (
|
|
29
|
+
'attrs' in entry &&
|
|
30
|
+
entry.attrs?.about === INTERNAL_REFERENCE_ENTRY_MARKER &&
|
|
31
|
+
endpoint &&
|
|
32
|
+
typeof endpoint === 'string'
|
|
33
|
+
) {
|
|
34
|
+
modifyFn(entry, {
|
|
35
|
+
endpoint,
|
|
36
|
+
label: entry.attrs?.['data-stldocs-label'],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if ('entries' in entry) {
|
|
40
|
+
recursiveReplacePlaceholderItems(entry.entries, modifyFn);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type GenerateProps = string | { label?: string; endpoint: string };
|
|
46
|
+
|
|
47
|
+
function normalizeGenerateProps(generateProps: GenerateProps) {
|
|
48
|
+
if (typeof generateProps === 'string') {
|
|
49
|
+
return {
|
|
50
|
+
label: undefined,
|
|
51
|
+
endpoint: generateProps,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
endpoint: generateProps.endpoint,
|
|
56
|
+
label: generateProps.label,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function generateAPILink(generateProps: GenerateProps) {
|
|
61
|
+
const { label, endpoint } = normalizeGenerateProps(generateProps);
|
|
62
|
+
return {
|
|
63
|
+
label: label ?? endpoint,
|
|
64
|
+
link: `/`,
|
|
65
|
+
attrs: {
|
|
66
|
+
about: INTERNAL_REFERENCE_ENTRY_MARKER,
|
|
67
|
+
'data-stldocs-endpoint': endpoint,
|
|
68
|
+
'data-stldocs-label': label,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const copyCurrentPageAsMarkdown = async () => {
|
|
2
|
+
const currentUrl = new URL(window.location.href);
|
|
3
|
+
const markdownUrl = `${currentUrl.origin}${currentUrl.pathname}.md`;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch(markdownUrl);
|
|
7
|
+
if (!response.ok) {
|
|
8
|
+
throw new Error('Network response was not ok');
|
|
9
|
+
}
|
|
10
|
+
const markdown = await response.text();
|
|
11
|
+
await navigator.clipboard.writeText(markdown);
|
|
12
|
+
console.log('Markdown copied to clipboard');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('There has been a problem with your fetch operation:', error);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function onSelectAIOption(value: string, makeMarkdownLink: boolean = true) {
|
|
19
|
+
if (value === 'copy-markdown') {
|
|
20
|
+
copyCurrentPageAsMarkdown();
|
|
21
|
+
}
|
|
22
|
+
if (value === 'view-as-markdown') {
|
|
23
|
+
const currentUrl = new URL(window.location.href);
|
|
24
|
+
const markdownUrl = `${currentUrl.origin}${currentUrl.pathname}.md`;
|
|
25
|
+
window.open(markdownUrl, '_blank');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (value === 'chat-gpt') {
|
|
29
|
+
const currentUrl = new URL(window.location.href);
|
|
30
|
+
const llmUrl = makeMarkdownLink ? `${currentUrl.origin}${currentUrl.pathname}.md` : currentUrl.href;
|
|
31
|
+
|
|
32
|
+
const prompt = `Read from ${llmUrl} so I can ask questions about it.`;
|
|
33
|
+
|
|
34
|
+
const chatGptUrl = `https://chatgpt.com/?hints=search&prompt=${encodeURIComponent(prompt)}`;
|
|
35
|
+
window.open(chatGptUrl, '_blank');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (value === 'claude') {
|
|
39
|
+
const currentUrl = new URL(window.location.href);
|
|
40
|
+
const llmUrl = makeMarkdownLink ? `${currentUrl.origin}${currentUrl.pathname}.md` : currentUrl.href;
|
|
41
|
+
|
|
42
|
+
const prompt = `Read from ${llmUrl} so I can ask questions about it.`;
|
|
43
|
+
|
|
44
|
+
const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`;
|
|
45
|
+
window.open(claudeUrl, '_blank');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (value === 'cursor') {
|
|
49
|
+
const currentUrl = new URL(window.location.href);
|
|
50
|
+
const llmUrl = makeMarkdownLink ? `${currentUrl.origin}${currentUrl.pathname}.md` : currentUrl.href;
|
|
51
|
+
|
|
52
|
+
const prompt = `Read from ${llmUrl} so I can ask questions about it.`;
|
|
53
|
+
|
|
54
|
+
const cursorUrl = `https://www.cursor.so/?prompt=${encodeURIComponent(prompt)}`;
|
|
55
|
+
window.open(cursorUrl, '_blank');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getPageLoadEvent } from '../helpers/getPageLoadEvent.ts';
|
|
2
|
+
|
|
3
|
+
document.addEventListener(getPageLoadEvent(), () => {
|
|
4
|
+
document.querySelectorAll('.stldocs-snippet-response-tab-item').forEach((tab) => {
|
|
5
|
+
tab.addEventListener('click', () => {
|
|
6
|
+
document
|
|
7
|
+
.querySelectorAll('.stldocs-snippet-response-tab-item')
|
|
8
|
+
.forEach((t) => t.classList.remove('stldocs-snippet-response-tab-item-active'));
|
|
9
|
+
document
|
|
10
|
+
.querySelectorAll('.stldocs-snippet-response-pane')
|
|
11
|
+
.forEach((p) => p.classList.remove('stldocs-snippet-response-pane-active'));
|
|
12
|
+
|
|
13
|
+
const panelId = (tab as HTMLButtonElement).dataset.snippetResponseTabId;
|
|
14
|
+
|
|
15
|
+
if (!panelId) {
|
|
16
|
+
console.error(`No panel found for tab: ${tab}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
document
|
|
20
|
+
.querySelectorAll(`[data-snippet-response-pane-id="${panelId}"]`)
|
|
21
|
+
.forEach((p) => p.classList.add('stldocs-snippet-response-pane-active'));
|
|
22
|
+
document
|
|
23
|
+
.querySelectorAll(`[data-snippet-response-tab-id="${panelId}"]`)
|
|
24
|
+
.forEach((p) => p.classList.add('stldocs-snippet-response-tab-item-active'));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
document.getElementById('stl-snippet-expand-button')?.addEventListener('click', (e) => {
|
|
29
|
+
const btn = e.currentTarget as HTMLButtonElement;
|
|
30
|
+
|
|
31
|
+
const collapsedDiv = document.querySelector('.stl-snippet-code-is-collapsed') as HTMLElement | null;
|
|
32
|
+
const expandedDiv = document.querySelector('.stl-snippet-code-is-expanded') as HTMLElement | null;
|
|
33
|
+
|
|
34
|
+
// We need to use javascript for height animations due to having dynamic heights based on snippet size.
|
|
35
|
+
const animateHeight = (el: HTMLElement, expand: boolean) => {
|
|
36
|
+
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
37
|
+
if (prefersReduced) {
|
|
38
|
+
el.classList.toggle('stl-snippet-code-is-expanded', expand);
|
|
39
|
+
el.classList.toggle('stl-snippet-code-is-collapsed', !expand);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// This is a bit funky, but it animates pretty smooth.
|
|
44
|
+
// 1. Toggle to new state so we can measure the target height
|
|
45
|
+
// 2. Reset to starting height to trigger transition
|
|
46
|
+
const startHeight = el.scrollHeight;
|
|
47
|
+
el.classList.toggle('stl-snippet-code-is-expanded', expand);
|
|
48
|
+
el.classList.toggle('stl-snippet-code-is-collapsed', !expand);
|
|
49
|
+
|
|
50
|
+
const endHeight = el.scrollHeight;
|
|
51
|
+
|
|
52
|
+
el.style.height = `${startHeight}px`;
|
|
53
|
+
|
|
54
|
+
requestAnimationFrame(() => {
|
|
55
|
+
el.style.height = `${endHeight}px`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const onEnd = (e: TransitionEvent) => {
|
|
59
|
+
if (e.propertyName !== 'height') return;
|
|
60
|
+
el.style.height = '';
|
|
61
|
+
el.removeEventListener('transitionend', onEnd);
|
|
62
|
+
};
|
|
63
|
+
el.addEventListener('transitionend', onEnd);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (collapsedDiv) {
|
|
67
|
+
if (collapsedDiv) {
|
|
68
|
+
collapsedDiv.querySelector('.shiki')?.setAttribute('style', `counter-reset: codeblock-line 0`);
|
|
69
|
+
}
|
|
70
|
+
animateHeight(collapsedDiv, true);
|
|
71
|
+
btn.textContent = 'Show less';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (expandedDiv) {
|
|
76
|
+
const dataSnippetExpandedOffset = expandedDiv?.dataset.snippetExpandedOffset;
|
|
77
|
+
if (dataSnippetExpandedOffset) {
|
|
78
|
+
const offset = parseInt(dataSnippetExpandedOffset, 10);
|
|
79
|
+
expandedDiv
|
|
80
|
+
.querySelector('.shiki')
|
|
81
|
+
?.setAttribute('style', `counter-reset: codeblock-line ${Math.max(offset - 1, 0)}`);
|
|
82
|
+
}
|
|
83
|
+
animateHeight(expandedDiv, false);
|
|
84
|
+
btn.textContent = 'Show more';
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EXPERIMENTAL_COLLAPSIBLE_SNIPPETS } from 'virtual:stl-starlight-virtual-module';
|
|
2
|
+
const copyIcon = `<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>`;
|
|
3
|
+
const circleAlertIcon = `<circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/>`;
|
|
4
|
+
const checkIcon = `<path d="M20 6 9 17l-5-5"/>`;
|
|
5
|
+
|
|
6
|
+
addEventListener('click', async (event) => {
|
|
7
|
+
const copyButton = (event.target as HTMLElement).closest('[data-stldocs-snippet-copy]') as HTMLElement;
|
|
8
|
+
if (copyButton) {
|
|
9
|
+
const iconElement = copyButton.querySelector('.stldocs-icon') as SVGElement;
|
|
10
|
+
clearTimeout(copyButton.dataset.__stldocsCopyTimeout);
|
|
11
|
+
try {
|
|
12
|
+
const isContentCollapsed = !!document.querySelector(
|
|
13
|
+
'.stldocs-snippet-code.stl-snippet-code-is-collapsed',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const content = copyButton
|
|
17
|
+
.closest('[data-stldocs-copy-parent]')!
|
|
18
|
+
.querySelector('[data-stldocs-copy-content]')!;
|
|
19
|
+
|
|
20
|
+
const contentCopy = content.cloneNode(true) as HTMLElement;
|
|
21
|
+
|
|
22
|
+
contentCopy.querySelectorAll('.ellipsis').forEach((el) => el.remove());
|
|
23
|
+
if (EXPERIMENTAL_COLLAPSIBLE_SNIPPETS && isContentCollapsed) {
|
|
24
|
+
contentCopy.querySelectorAll('.hidden').forEach((el) => el.remove());
|
|
25
|
+
contentCopy.querySelectorAll('.leading-ws').forEach((el) => el.remove());
|
|
26
|
+
}
|
|
27
|
+
await navigator.clipboard.writeText(contentCopy.textContent!);
|
|
28
|
+
iconElement.innerHTML = checkIcon;
|
|
29
|
+
} catch {
|
|
30
|
+
iconElement.innerHTML = circleAlertIcon;
|
|
31
|
+
}
|
|
32
|
+
copyButton.dataset.__stldocsCopyTimeout =
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
iconElement.innerHTML = copyIcon;
|
|
35
|
+
}, 1000) + '';
|
|
36
|
+
}
|
|
37
|
+
});
|