@stainless-api/playgrounds 0.0.1-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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +23 -0
  3. package/eslint.config.js +2 -0
  4. package/package.json +69 -0
  5. package/src/Logs.tsx +216 -0
  6. package/src/Panel.tsx +21 -0
  7. package/src/PlaygroundPanelWrapper.tsx +5 -0
  8. package/src/build-py-types.ts +152 -0
  9. package/src/build-ts-types.ts +70 -0
  10. package/src/build.ts +97 -0
  11. package/src/codemirror/comlink.ts +698 -0
  12. package/src/codemirror/curl/curlconverter.vendor.js +7959 -0
  13. package/src/codemirror/curl.ts +108 -0
  14. package/src/codemirror/deps.ts +12 -0
  15. package/src/codemirror/fix-lsp-markdown.ts +50 -0
  16. package/src/codemirror/lsp.ts +87 -0
  17. package/src/codemirror/python/anser.ts +398 -0
  18. package/src/codemirror/python/pyodide.ts +180 -0
  19. package/src/codemirror/python.ts +160 -0
  20. package/src/codemirror/react.tsx +615 -0
  21. package/src/codemirror/sanitize-html.ts +12 -0
  22. package/src/codemirror/shiki.ts +65 -0
  23. package/src/codemirror/typescript/cdn-typescript.d.ts +1 -0
  24. package/src/codemirror/typescript/cdn-typescript.js +1 -0
  25. package/src/codemirror/typescript/console.ts +590 -0
  26. package/src/codemirror/typescript/get-signature.ts +94 -0
  27. package/src/codemirror/typescript/prettier-plugin-external-typescript.vendor.js +4968 -0
  28. package/src/codemirror/typescript/runner.ts +396 -0
  29. package/src/codemirror/typescript/special-info.ts +171 -0
  30. package/src/codemirror/typescript/worker.ts +292 -0
  31. package/src/codemirror/typescript.tsx +198 -0
  32. package/src/create.tsx +44 -0
  33. package/src/icon.tsx +21 -0
  34. package/src/index.ts +6 -0
  35. package/src/logs-context.ts +5 -0
  36. package/src/playground.css +359 -0
  37. package/src/sandbox-worker/in-frame.js +179 -0
  38. package/src/sandbox-worker/index.ts +202 -0
  39. package/src/use-storage.ts +54 -0
  40. package/src/util.ts +29 -0
  41. package/src/virtual-module.d.ts +45 -0
  42. package/src/vite-env.d.ts +1 -0
  43. package/test/get-signature.test.ts +73 -0
  44. package/test/use-storage.test.ts +60 -0
  45. package/tsconfig.json +11 -0
@@ -0,0 +1,615 @@
1
+ import { useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type Ref } from 'react';
2
+ import { Braces, ChevronsUpDownIcon, CopyIcon, KeyRound, Play, Square } from 'lucide-react';
3
+ import type { EditorView } from '@codemirror/view';
4
+ import type * as lsp from 'vscode-languageserver-protocol';
5
+ import type { Diagnostic } from '@codemirror/lint';
6
+ import type { EditorState, Extension, Text, Transaction } from '@codemirror/state';
7
+ import type { Transport } from '@codemirror/lsp-client';
8
+ import type { PlaygroundLanguage } from '../create.tsx';
9
+ import style from '@stainless-api/docs-ui/style';
10
+ import { LogsContext } from '../logs-context.ts';
11
+ import { Logs, type Logger, type Part, type StatePart } from '../Logs.tsx';
12
+ import type { Completion } from '@codemirror/autocomplete';
13
+ import { Button, Dropdown } from '@stainless-api/ui-primitives';
14
+ import { PlaygroundIcon } from '../icon.tsx';
15
+ import { initDropdown } from '@stainless-api/ui-primitives/scripts';
16
+ // @ts-expect-error TODO: make independent from stl-starlight
17
+ import { navigate } from 'astro:transitions/client';
18
+ // @ts-expect-error TODO: make independent from stl-starlight
19
+ import { updateSelectedLanguage } from 'virtual:stl-playground/unstable-update-language';
20
+ // @ts-expect-error TODO: make independent from stl-starlight
21
+ import { BASE_PATH } from 'virtual:stl-starlight-virtual-module';
22
+ import authData from 'virtual:stl-playground/auth.json';
23
+ import { Panel } from '../Panel.tsx';
24
+ import { PlaygroundPanelWrapper } from '../PlaygroundPanelWrapper.tsx';
25
+ import { MaskedInput } from '@stainless-api/docs-ui/components/MaskedInput';
26
+ import useStorage from '../use-storage.ts';
27
+
28
+ let codeMirrorPromise: Promise<typeof import('./deps.ts')>;
29
+ function loadCodeMirror(): Promise<typeof import('./deps.ts')> {
30
+ return codeMirrorPromise || (codeMirrorPromise = import('./deps.ts'));
31
+ }
32
+
33
+ function fromPosition(doc: Text, pos: lsp.Position): number {
34
+ const line = doc.line(pos.line + 1);
35
+ return line.from + pos.character;
36
+ }
37
+
38
+ function hydrateDetail(completion: Completion): Completion & { sortText?: string } {
39
+ if (completion.detail?.startsWith('\0{')) {
40
+ const extra = JSON.parse(completion.detail.slice(1));
41
+ completion.detail = undefined;
42
+ Object.assign(completion, extra);
43
+ }
44
+ return completion;
45
+ }
46
+
47
+ async function initCodeMirror({
48
+ signal,
49
+ container,
50
+ extensions,
51
+ fileName,
52
+ doc,
53
+ transport,
54
+ }: {
55
+ signal: AbortSignal;
56
+ container: HTMLDivElement;
57
+ transport?: Transport;
58
+ fileName: string;
59
+ extensions: Extension[];
60
+ doc: string;
61
+ }): Promise<{ editor: EditorView; fixGutters(): void }> {
62
+ const {
63
+ view: {
64
+ ViewUpdate,
65
+ EditorView,
66
+ keymap,
67
+ highlightSpecialChars,
68
+ drawSelection,
69
+ dropCursor,
70
+ rectangularSelection,
71
+ crosshairCursor,
72
+ lineNumbers,
73
+ },
74
+ language: { indentOnInput, bracketMatching, foldGutter, indentUnit },
75
+ commands: { history },
76
+ search: { highlightSelectionMatches },
77
+ autocomplete: { autocompletion, closeBrackets },
78
+ lint: { setDiagnostics },
79
+ state: { EditorState, Compartment },
80
+ lsp: { LSPClient, languageServerSupport },
81
+ vscodeKeymap,
82
+ createLSPClient,
83
+ shikiHighlighter,
84
+ } = await loadCodeMirror();
85
+
86
+ if (signal.aborted) throw new Error('unmounted');
87
+
88
+ if (transport) {
89
+ const client = await createLSPClient(
90
+ LSPClient,
91
+ {
92
+ rootUri: 'file:///play/',
93
+ onDiagnostics: (lspDiagnostics) => {
94
+ const diagnostics = lspDiagnostics.map((lsp): Diagnostic => {
95
+ return {
96
+ from: fromPosition(editor.state.doc, lsp.range.start),
97
+ to: fromPosition(editor.state.doc, lsp.range.end),
98
+ message: lsp.message,
99
+ severity:
100
+ lsp.severity === 4
101
+ ? 'hint'
102
+ : lsp.severity === 3
103
+ ? 'info'
104
+ : lsp.severity === 2
105
+ ? 'warning'
106
+ : 'error',
107
+ };
108
+ });
109
+ editor.dispatch(setDiagnostics(editor.state, diagnostics));
110
+ },
111
+ },
112
+ transport,
113
+ );
114
+ signal.addEventListener('abort', () => client.disconnect());
115
+ extensions.push(languageServerSupport(client, 'file:///play/' + fileName));
116
+ }
117
+
118
+ const editorTheme = new Compartment();
119
+ const getTheme = (): Extension[] => {
120
+ const isDark = document.documentElement.dataset.theme === 'dark';
121
+ return [EditorView.theme({}, { dark: isDark })];
122
+ };
123
+ const editor = new EditorView({
124
+ doc,
125
+ parent: container,
126
+ extensions: [
127
+ lineNumbers(),
128
+ foldGutter(),
129
+ highlightSpecialChars(),
130
+ history(),
131
+ drawSelection(),
132
+ dropCursor(),
133
+ EditorState.allowMultipleSelections.of(true),
134
+ indentOnInput(),
135
+ bracketMatching(),
136
+ closeBrackets(),
137
+ autocompletion({
138
+ compareCompletions(a, b) {
139
+ return (hydrateDetail(a).sortText ?? a.label).localeCompare(hydrateDetail(b).sortText ?? b.label);
140
+ },
141
+ optionClass(completion) {
142
+ hydrateDetail(completion);
143
+ return '';
144
+ },
145
+ }),
146
+ rectangularSelection(),
147
+ crosshairCursor(),
148
+ highlightSelectionMatches(),
149
+ keymap.of(vscodeKeymap),
150
+ shikiHighlighter,
151
+ editorTheme.of(getTheme()),
152
+ indentUnit.of(fileName.endsWith('.py') ? ' ' : ' '),
153
+ ...extensions,
154
+ ],
155
+ });
156
+ const observer = new MutationObserver(() => {
157
+ editor.dispatch({
158
+ effects: editorTheme.reconfigure(getTheme()),
159
+ });
160
+ });
161
+ observer.observe(document.documentElement, {
162
+ attributeFilter: ['data-theme'],
163
+ });
164
+ signal.addEventListener('abort', () => {
165
+ editor.destroy();
166
+ observer.disconnect();
167
+ });
168
+ return {
169
+ editor,
170
+ fixGutters() {
171
+ const update = (
172
+ ViewUpdate as unknown as {
173
+ create(view: EditorView, state: EditorState, _: []): Transaction & { flags: number };
174
+ }
175
+ ).create(editor, editor.state, []);
176
+ update.flags = update.flags | 4; /* UpdateFlag.Viewport */
177
+ editor.dispatch(update);
178
+ },
179
+ };
180
+ }
181
+
182
+ type State =
183
+ | { state: 'loading' }
184
+ | { state: 'error'; error: unknown }
185
+ | {
186
+ state: 'ready';
187
+ view: EditorView;
188
+ fixGutters(): void;
189
+ run: (signal: AbortSignal) => Promise<void>;
190
+ format(): Promise<void>;
191
+ setEnv(vars: Record<string, string>): Promise<void>;
192
+ };
193
+
194
+ export interface Language {
195
+ transport?: Transport;
196
+ fileName: string;
197
+ extensions: Extension[];
198
+ doc: string;
199
+ setLogger: (s: Logger) => void;
200
+ freeLogHandle(handle: string): void;
201
+ expandLogHandle(handle: string): Promise<Part[]>;
202
+ run: (code: string, signal: AbortSignal) => Promise<void>;
203
+ format: (code: string) => Promise<string | undefined>;
204
+ setEnv(vars: Record<string, string>): Promise<void>;
205
+ }
206
+
207
+ const useCodeMirror = (
208
+ initLanguage: (arg: { signal: AbortSignal }) => Promise<Language>,
209
+ ): {
210
+ containerRef: Ref<HTMLDivElement>;
211
+ } & State => {
212
+ const containerRef = useRef<HTMLDivElement>(null);
213
+ const [state, setState] = useState<State>({ state: 'loading' });
214
+ const logsSignal = useContext(LogsContext)!;
215
+ useEffect(() => {
216
+ const controller = new AbortController();
217
+ let run: Language['run'];
218
+ let format: Language['format'];
219
+ let setEnv: Language['setEnv'];
220
+ (async () => {
221
+ const {
222
+ transport,
223
+ fileName,
224
+ extensions,
225
+ doc,
226
+ run: run_,
227
+ format: format_,
228
+ setEnv: setEnv_,
229
+ setLogger,
230
+ freeLogHandle,
231
+ expandLogHandle,
232
+ } = await initLanguage({
233
+ signal: controller.signal,
234
+ });
235
+ setEnv = setEnv_;
236
+ run = run_;
237
+ format = format_;
238
+ if (controller.signal.aborted) throw new Error('unmounted');
239
+ setLogger((log) => {
240
+ if (log.type === 'clear') {
241
+ logsSignal.value = [];
242
+ } else {
243
+ const mapPart = (part: Part): StatePart => ({
244
+ css: part.css,
245
+ value: typeof part.value === 'string' ? part.value : part.value.map(mapPart),
246
+ handle:
247
+ part.expandHandle === undefined
248
+ ? undefined
249
+ : {
250
+ async expand() {
251
+ const e = await expandLogHandle(part.expandHandle!);
252
+ return e.map(mapPart);
253
+ },
254
+ free() {
255
+ freeLogHandle(part.expandHandle!);
256
+ },
257
+ id: part.id,
258
+ },
259
+ lowPriority: part.lowPriority,
260
+ });
261
+ logsSignal.value = [
262
+ ...logsSignal.value,
263
+ { type: log.type, parts: log.parts.map((e) => (typeof e === 'string' ? e : mapPart(e))) },
264
+ ];
265
+ }
266
+ });
267
+ return await initCodeMirror({
268
+ extensions,
269
+ transport,
270
+ fileName,
271
+ container: containerRef.current!,
272
+ signal: controller.signal,
273
+ doc,
274
+ });
275
+ })().then(
276
+ ({ editor: view, fixGutters }) => {
277
+ setState({
278
+ state: 'ready',
279
+ view,
280
+ fixGutters,
281
+ run(signal) {
282
+ return run(view.state.doc.toString(), signal);
283
+ },
284
+ setEnv,
285
+ async format() {
286
+ const newDoc = await format(view.state.doc.toString());
287
+ if (newDoc !== undefined) {
288
+ view.dispatch({
289
+ changes: {
290
+ from: 0,
291
+ to: view.state.doc.length,
292
+ insert: newDoc,
293
+ },
294
+ });
295
+ }
296
+ },
297
+ });
298
+ },
299
+ (error) => {
300
+ if (!controller.signal.aborted) {
301
+ console.error(error);
302
+ setState({ state: 'error', error });
303
+ }
304
+ },
305
+ );
306
+ return () => {
307
+ controller.abort();
308
+ };
309
+ }, []);
310
+ return { containerRef, ...state };
311
+ };
312
+
313
+ 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"/>`;
314
+ const checkIcon = `<path d="M20 6 9 17l-5-5"/>`;
315
+ function DOMNode({ node }: { node: Node }) {
316
+ const wrap = useRef<HTMLDivElement>(null);
317
+
318
+ useLayoutEffect(() => {
319
+ wrap.current!.replaceChildren(node);
320
+ }, [node]);
321
+
322
+ return <div style={{ display: 'contents' }} ref={wrap} />;
323
+ }
324
+ function prefixIds(root: Element | DocumentFragment, idPrefix: string) {
325
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
326
+
327
+ let node;
328
+ while ((node = walker.nextNode() as Element | null)) {
329
+ if (node.id) {
330
+ node.id = idPrefix + node.id;
331
+ }
332
+ if (node.hasAttribute('aria-labelledby')) {
333
+ node.setAttribute('aria-labelledby', idPrefix + node.getAttribute('aria-labelledby'));
334
+ }
335
+ for (const attr of node.getAttributeNames()) {
336
+ node.setAttribute(attr, node.getAttribute(attr)!.replaceAll('url(#', 'url(#' + idPrefix));
337
+ }
338
+
339
+ if (node instanceof HTMLTemplateElement && node.content) {
340
+ prefixIds(node.content, idPrefix);
341
+ }
342
+ }
343
+ }
344
+ export const Editor = ({
345
+ lang,
346
+ doc,
347
+ container,
348
+ onLoad,
349
+ unmount,
350
+ }: {
351
+ lang: PlaygroundLanguage;
352
+ doc: string;
353
+ container: HTMLElement;
354
+ onLoad: (onShow: () => void) => void;
355
+ unmount: () => void;
356
+ }) => {
357
+ const titleNode = useMemo(
358
+ () => container.querySelector('.stldocs-snippet-request-title-method')!.cloneNode(true),
359
+ [container],
360
+ );
361
+ const idPrefix = useId();
362
+ const menuNode = useMemo(() => {
363
+ const clone = container
364
+ .querySelector('.stldocs-snippet-request-title-content')!
365
+ .cloneNode(true) as HTMLElement;
366
+ prefixIds(clone, idPrefix);
367
+ initDropdown({
368
+ root: clone.querySelector('#' + idPrefix + 'stldocs-snippet-select')!,
369
+ onSelect: (value) => {
370
+ const originalLanguage = document.getElementById(idPrefix + 'stldocs-snippet-select')?.dataset
371
+ .currentValue;
372
+ const path: string = updateSelectedLanguage(BASE_PATH, originalLanguage, value);
373
+ navigate(path.replace(/(\?.+)?($|#)/, (_, str, end) => (str ? str + '&play' : '?play') + end));
374
+ },
375
+ });
376
+ return clone;
377
+ }, [container]);
378
+ const editor = useCodeMirror(async ({ signal }) => {
379
+ let _never: never;
380
+ void _never!;
381
+ switch (lang) {
382
+ case 'python': {
383
+ const { createPyright } = await import('./python');
384
+ if (signal.aborted) throw new Error('unmounted');
385
+ return await createPyright(signal, doc);
386
+ }
387
+ case 'typescript': {
388
+ const { createTypescript } = await import('./typescript');
389
+ if (signal.aborted) throw new Error('unmounted');
390
+ return await createTypescript(signal, doc);
391
+ }
392
+ case 'http': {
393
+ const { createCurl } = await import('./curl');
394
+ if (signal.aborted) throw new Error('unmounted');
395
+ return await createCurl(signal, doc);
396
+ }
397
+ default:
398
+ throw (_never = lang);
399
+ }
400
+ });
401
+ const logs = useContext(LogsContext);
402
+ useEffect(() => {
403
+ if (editor.state !== 'loading') {
404
+ onLoad(editor.state === 'ready' ? () => editor.fixGutters() : () => {});
405
+ }
406
+ }, [editor.state]);
407
+ const [controller, setController] = useState<AbortController | null>(null);
408
+ return (
409
+ <div className="stldocs-snippet stl-snippet-non-collapsible stldocs-snippet-playground">
410
+ <div className="stldocs-snippet-request">
411
+ <div className="stldocs-snippet-request-title">
412
+ <DOMNode node={titleNode} />
413
+ <DOMNode node={menuNode} />
414
+ <Button
415
+ variant="outline"
416
+ onClick={async (e) => {
417
+ if (editor.state !== 'ready') return;
418
+ const copyButton = e.currentTarget;
419
+ if (copyButton.dataset.__stldocsCopyTimeout) return;
420
+ const iconElement = copyButton.querySelector('.stldocs-icon') as SVGElement;
421
+ const originalIcon = iconElement.innerHTML;
422
+ try {
423
+ await navigator.clipboard.writeText(editor.view.state.doc.toString());
424
+ iconElement.innerHTML = checkIcon;
425
+ } catch {
426
+ iconElement.innerHTML = circleAlertIcon;
427
+ }
428
+ copyButton.dataset.__stldocsCopyTimeout =
429
+ setTimeout(() => {
430
+ copyButton.dataset.__stldocsCopyTimeout = '';
431
+ iconElement.innerHTML = originalIcon;
432
+ }, 1000) + '';
433
+ }}
434
+ title={'Copy Code'}
435
+ >
436
+ <CopyIcon size={16} className={style.Icon} />
437
+ </Button>
438
+ <Button
439
+ variant="accent-muted"
440
+ border
441
+ title="Back to snippet"
442
+ onClick={() => {
443
+ unmount();
444
+ }}
445
+ >
446
+ <PlaygroundIcon />
447
+ </Button>
448
+ </div>
449
+ <div className="playground-editor-wrap">
450
+ {editor.state === 'error' && (
451
+ <pre className="playground-editor-load-error">Failed to load editor:{'\n' + editor.error}</pre>
452
+ )}
453
+ <div
454
+ className="playground-editor-container"
455
+ ref={editor.containerRef}
456
+ hidden={editor.state !== 'ready'}
457
+ />
458
+ </div>
459
+ <div className="playground-footer">
460
+ {lang !== 'http' && (
461
+ <Button
462
+ variant="outline"
463
+ onClick={async (e) => {
464
+ if (editor.state !== 'ready') return;
465
+ const button = e.currentTarget;
466
+ if (button.dataset.__stldocsFormatTimeout) return;
467
+ if (button.getAttribute('aria-disabled') === 'true') return;
468
+ const iconElement = button.querySelector('.stldocs-icon') as SVGElement;
469
+ const originalIcon = iconElement.innerHTML;
470
+ // use aria-disabled, not disabled, to avoid losing focus
471
+ button.setAttribute('aria-disabled', 'true');
472
+ button.setAttribute('aria-label', 'Formatting...');
473
+ button.classList.add('stl-ui-button--loading');
474
+ try {
475
+ await editor.format();
476
+ iconElement.innerHTML = checkIcon;
477
+ } catch {
478
+ iconElement.innerHTML = circleAlertIcon;
479
+ }
480
+ button.dataset.__stldocsFormatTimeout =
481
+ setTimeout(() => {
482
+ button.dataset.__stldocsFormatTimeout = '';
483
+ iconElement.innerHTML = originalIcon;
484
+ }, 1000) + '';
485
+ button.removeAttribute('aria-disabled');
486
+ button.removeAttribute('aria-label');
487
+ button.classList.remove('stl-ui-button--loading');
488
+ }}
489
+ title={'Format Code'}
490
+ aria-label={'Format Code'}
491
+ >
492
+ <Braces size={16} className={style.Icon} />
493
+ </Button>
494
+ )}
495
+ <span className="playground-spacer" />
496
+ <Button
497
+ variant="accent"
498
+ onClick={async () => {
499
+ if (controller) {
500
+ controller.abort();
501
+ return;
502
+ }
503
+ if (editor.state !== 'ready') return;
504
+ try {
505
+ const controller = new AbortController();
506
+ setController(controller);
507
+ await editor.run(controller.signal);
508
+ } catch (e) {
509
+ logs!.value = [...logs!.value, { type: 'error', parts: ['Uncaught ' + e] }];
510
+ } finally {
511
+ setController(null);
512
+ }
513
+ }}
514
+ style={{ minWidth: '6em' }}
515
+ >
516
+ {controller ? (
517
+ <>
518
+ Stop <Square size={16} className={style.Icon} style={{ marginLeft: '0.5em' }} />
519
+ </>
520
+ ) : (
521
+ <>
522
+ Run <Play size={16} className={style.Icon} style={{ marginLeft: '0.5em' }} />
523
+ </>
524
+ )}
525
+ </Button>
526
+ </div>
527
+ </div>
528
+ <PlaygroundPanelWrapper>
529
+ <Auth setEnv={editor.state === 'ready' ? editor.setEnv : undefined} />
530
+ <Logs />
531
+ </PlaygroundPanelWrapper>
532
+ </div>
533
+ );
534
+ };
535
+
536
+ function Auth(props: { setEnv?: (vars: Record<string, string>) => Promise<void> }) {
537
+ const [env, setEnv] = useStorage<Record<string, string>>(sessionStorage, 'stl-playground-env', {});
538
+ const id = useId();
539
+ const [selected, setSelected] = useState(authData![0]!.name);
540
+ const current = authData!.find((e) => e.name === selected)!;
541
+ useEffect(() => {
542
+ props.setEnv?.(env);
543
+ }, [props.setEnv, env, current]);
544
+ return (
545
+ <Panel title="Authentication" icon={KeyRound}>
546
+ <div className="playground-auth-panel playground-panel-content">
547
+ {authData!.length > 1 && (
548
+ <>
549
+ <label htmlFor={id + '-trigger'}>Method</label>
550
+ <div>
551
+ <Dropdown
552
+ id={id}
553
+ data-current-value={selected}
554
+ className={''}
555
+ ref={(root) => {
556
+ initDropdown({
557
+ root,
558
+ onSelect(value) {
559
+ setSelected(value);
560
+ },
561
+ });
562
+ }}
563
+ >
564
+ <Dropdown.Trigger
565
+ className="dropdown-toggle"
566
+ type="button"
567
+ id={id + '-trigger'}
568
+ aria-expanded="false"
569
+ >
570
+ <Dropdown.TriggerSelectedItem>
571
+ <span>{current.title}</span>
572
+ </Dropdown.TriggerSelectedItem>
573
+ <Dropdown.TriggerIcon>
574
+ <ChevronsUpDownIcon size={16} />
575
+ </Dropdown.TriggerIcon>
576
+ </Dropdown.Trigger>
577
+ <Dropdown.Menu aria-labelledby={id + '-trigger'}>
578
+ {authData!.map((item) => (
579
+ <Dropdown.MenuItem key={item.name} value={item.name} isSelected={item.name === selected}>
580
+ <div>{item.title}</div>
581
+ </Dropdown.MenuItem>
582
+ ))}
583
+ </Dropdown.Menu>
584
+ </Dropdown>
585
+ </div>
586
+ </>
587
+ )}
588
+ {...current.opts
589
+ .filter((e) => e.read_env)
590
+ .map((e, i) => {
591
+ return (
592
+ <>
593
+ {(i !== 0 || authData!.length > 1) && <hr />}
594
+ <label htmlFor={id + '-var-' + i}>
595
+ <code>{e.read_env} =</code>
596
+ </label>
597
+ <MaskedInput
598
+ type="text"
599
+ key={e.name}
600
+ defaultValue={env[e.read_env!]}
601
+ onInput={(evt) => {
602
+ const input = (evt.target as HTMLElement).closest('input') as HTMLInputElement;
603
+ setEnv({
604
+ ...env,
605
+ [e.read_env!]: input.value,
606
+ });
607
+ }}
608
+ />
609
+ </>
610
+ );
611
+ })}
612
+ </div>
613
+ </Panel>
614
+ );
615
+ }
@@ -0,0 +1,12 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ export function sanitizeHTML(html: string) {
4
+ const dom = DOMPurify.sanitize(html, {
5
+ RETURN_DOM: true,
6
+ }) as HTMLBodyElement;
7
+ for (const link of dom.querySelectorAll('a, area')) {
8
+ link.setAttribute('target', '_blank');
9
+ link.setAttribute('rel', 'noopener');
10
+ }
11
+ return dom.innerHTML;
12
+ }
@@ -0,0 +1,65 @@
1
+ import { language } from '@codemirror/language';
2
+ import { Prec, RangeSetBuilder } from '@codemirror/state';
3
+ import {
4
+ Decoration,
5
+ type DecorationSet,
6
+ type EditorView,
7
+ ViewPlugin,
8
+ type ViewUpdate,
9
+ } from '@codemirror/view';
10
+ import { SupportedLanguageSyntaxes } from '@stainless-api/docs-ui/routing';
11
+ import { createHighlighter } from 'shiki';
12
+ // @ts-expect-error TODO: make independent from stl-starlight
13
+ import { HIGHLIGHT_THEMES } from 'virtual:stl-starlight-virtual-module';
14
+ export const highlighter = await createHighlighter({
15
+ themes: [HIGHLIGHT_THEMES?.dark ?? 'github-dark', HIGHLIGHT_THEMES?.light ?? 'github-light'],
16
+ langs: SupportedLanguageSyntaxes,
17
+ });
18
+ const span = document.createElement('span');
19
+ class TreeHighlighter {
20
+ decorations: DecorationSet;
21
+
22
+ constructor(view: EditorView) {
23
+ this.decorations = this.buildDeco(view);
24
+ }
25
+
26
+ update(update: ViewUpdate) {
27
+ if (update.docChanged) {
28
+ this.decorations = this.buildDeco(update.view);
29
+ }
30
+ }
31
+
32
+ buildDeco(view: EditorView) {
33
+ const builder = new RangeSetBuilder<Decoration>();
34
+ const { tokens } = highlighter.codeToTokens(view.state.doc.toString(), {
35
+ themes: {
36
+ dark: HIGHLIGHT_THEMES?.dark ?? 'github-dark',
37
+ light: HIGHLIGHT_THEMES?.light ?? 'github-light',
38
+ },
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ lang: view.state.facet(language)!.name as any,
41
+ });
42
+ for (const token of tokens.flat()) {
43
+ span.style.cssText = '';
44
+ for (const k in token.htmlStyle) {
45
+ span.style.setProperty(k, token.htmlStyle[k]!);
46
+ }
47
+ builder.add(
48
+ token.offset,
49
+ token.offset + token.content.length,
50
+ Decoration.mark({
51
+ attributes: {
52
+ style: span.style.cssText,
53
+ },
54
+ }),
55
+ );
56
+ }
57
+ return builder.finish();
58
+ }
59
+ }
60
+
61
+ export const shikiHighlighter = Prec.high(
62
+ ViewPlugin.fromClass(TreeHighlighter, {
63
+ decorations: (v) => v.decorations,
64
+ }),
65
+ );
@@ -0,0 +1 @@
1
+ export { default } from 'typescript';
@@ -0,0 +1 @@
1
+ export default (importScripts('https://cdn.jsdelivr.net/npm/typescript@5.9.3/lib/typescript.js'), ts);