@djangocfg/ui-tools 2.1.287 → 2.1.290

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 (105) hide show
  1. package/README.md +14 -3
  2. package/dist/DocsLayout-IKH7BLSU.cjs +3464 -0
  3. package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
  4. package/dist/DocsLayout-JPXFUKAR.mjs +3457 -0
  5. package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
  6. package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
  7. package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
  8. package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
  9. package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
  10. package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
  11. package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
  12. package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
  13. package/dist/chunk-EFWOJPA6.mjs.map +1 -0
  14. package/dist/index.cjs +10 -10
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +34 -0
  17. package/dist/index.d.ts +34 -0
  18. package/dist/index.mjs +5 -5
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +21 -14
  21. package/src/components/markdown/MarkdownMessage.tsx +46 -0
  22. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +93 -157
  23. package/src/tools/OpenapiViewer/README.md +114 -6
  24. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
  25. package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +331 -53
  26. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
  27. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
  28. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
  29. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
  30. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
  31. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
  32. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
  33. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
  34. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
  35. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
  36. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
  37. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
  38. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
  39. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
  40. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
  41. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
  42. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
  43. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
  44. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
  45. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
  46. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
  47. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
  48. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
  49. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
  50. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
  51. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
  52. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
  53. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
  54. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +40 -11
  55. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
  56. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
  57. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
  58. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
  59. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
  60. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
  61. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
  62. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
  63. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
  64. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
  65. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
  66. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
  67. package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
  68. package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
  69. package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +19 -2
  70. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +38 -21
  71. package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +168 -50
  72. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
  73. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
  74. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
  75. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
  76. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
  77. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
  78. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
  79. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
  80. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
  81. package/src/tools/OpenapiViewer/hooks/index.ts +3 -1
  82. package/src/tools/OpenapiViewer/hooks/useDocsUrlSync.ts +119 -0
  83. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +164 -74
  84. package/src/tools/OpenapiViewer/types.ts +46 -1
  85. package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
  86. package/src/tools/OpenapiViewer/utils/index.ts +3 -0
  87. package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
  88. package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
  89. package/src/tools/OpenapiViewer/utils/scrollParent.ts +68 -0
  90. package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
  91. package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
  92. package/src/tools/PrettyCode/index.tsx +13 -0
  93. package/src/tools/PrettyCode/lazy.tsx +5 -0
  94. package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
  95. package/dist/DocsLayout-BCVU6TTX.cjs +0 -2027
  96. package/dist/DocsLayout-BCVU6TTX.cjs.map +0 -1
  97. package/dist/DocsLayout-ERETJLLV.mjs +0 -2020
  98. package/dist/DocsLayout-ERETJLLV.mjs.map +0 -1
  99. package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
  100. package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
  101. package/dist/chunk-IULI4XII.cjs.map +0 -1
  102. package/dist/chunk-VZGQC3NG.mjs.map +0 -1
  103. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -268
  104. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -211
  105. package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Code sample generation.
3
+ *
4
+ * In-house, browser-safe generator for a small catalogue of languages
5
+ * (curl / fetch / Node axios / Python requests / Go / PHP / Ruby /
6
+ * Java OkHttp). We used to wrap Kong's ``httpsnippet`` here but it is
7
+ * Node-only — the bundle referenced ``global`` / ``stream`` /
8
+ * ``string_decoder`` and crashed in Vite dev. Writing our own keeps the
9
+ * chunk small (a few kB vs ~1.5 MB) and avoids runtime polyfills.
10
+ *
11
+ * Adding more targets later is one function plus a row in the catalogue.
12
+ */
13
+
14
+ import type { HarRequest } from './operationToHar';
15
+
16
+ // Prism language ids used by ``prism-react-renderer``. ``bash``,
17
+ // ``ruby``, ``java`` and ``php`` aren't in the library's default
18
+ // bundle, but our ``registerPrismLanguages`` module pulls their
19
+ // grammars from ``prismjs`` at load time so they work as first-class
20
+ // languages here.
21
+ export const CODE_SAMPLE_TARGETS = [
22
+ { id: 'curl', label: 'cURL', prism: 'bash' },
23
+ { id: 'fetch', label: 'JavaScript', prism: 'javascript' },
24
+ { id: 'axios', label: 'Node (axios)', prism: 'javascript' },
25
+ { id: 'python', label: 'Python', prism: 'python' },
26
+ { id: 'go', label: 'Go', prism: 'go' },
27
+ { id: 'php', label: 'PHP', prism: 'php' },
28
+ { id: 'ruby', label: 'Ruby', prism: 'ruby' },
29
+ { id: 'java', label: 'Java', prism: 'java' },
30
+ ] as const;
31
+
32
+ export type CodeSampleTargetId = typeof CODE_SAMPLE_TARGETS[number]['id'];
33
+
34
+ // ─── Body literal helpers ─────────────────────────────────────────────────────
35
+ //
36
+ // Pre-formatted JSON bodies (``JSON.stringify(value, null, 2)``) carry
37
+ // real newlines that we want the snippet to preserve — a one-line
38
+ // escaped string like ``"{\n \"id\": 10,\n …}"`` reads as a wall of
39
+ // escape sequences. Each helper below returns a multi-line string
40
+ // literal in the target language's native syntax so the snippet reads
41
+ // like hand-written code.
42
+
43
+ /** Go raw string literal — uses backticks. Falls back to a double-
44
+ * quoted escaped literal when the body itself contains a backtick. */
45
+ function goRawString(s: string): string {
46
+ return s.includes('`') ? JSON.stringify(s) : `\`${s}\``;
47
+ }
48
+
49
+ /** Python triple-quoted string — uses ``"""``. Falls back to escaped
50
+ * form when the body already contains that sequence. */
51
+ function pythonTripleQuote(s: string): string {
52
+ return s.includes('"""') ? JSON.stringify(s) : `"""${s}"""`;
53
+ }
54
+
55
+ /** Ruby squiggly heredoc — ``<<~JSON`` style strips common leading
56
+ * whitespace. Safe unless the body literally contains ``JSON`` on a
57
+ * line by itself, which is vanishingly rare. */
58
+ function rubyHeredoc(s: string): string {
59
+ return `<<~JSON\n${s}\nJSON`;
60
+ }
61
+
62
+ /** PHP heredoc — ``<<<JSON`` style. Same caveat as Ruby's. Trailing
63
+ * ``JSON;`` must sit at column 0 (enforced by this template). */
64
+ function phpHeredoc(s: string): string {
65
+ return `<<<JSON\n${s}\nJSON`;
66
+ }
67
+
68
+ /** Java 15+ text block — ``"""``. Falls back to an escaped literal
69
+ * when the body contains that sequence. */
70
+ function javaTextBlock(s: string): string {
71
+ if (s.includes('"""')) return JSON.stringify(s);
72
+ // Text blocks need the opening ``"""`` on its own line — the
73
+ // parser strips the newline that immediately follows the delimiter.
74
+ return `"""\n${s}\n"""`;
75
+ }
76
+
77
+ function fullUrl(har: HarRequest): string {
78
+ if (!har.queryString.length) return har.url;
79
+ const qs = har.queryString
80
+ .map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
81
+ .join('&');
82
+ const sep = har.url.includes('?') ? '&' : '?';
83
+ return `${har.url}${sep}${qs}`;
84
+ }
85
+
86
+ function shellEscape(value: string): string {
87
+ return `'${value.replace(/'/g, `'\\''`)}'`;
88
+ }
89
+
90
+ function renderCurl(har: HarRequest): string {
91
+ const lines: string[] = [`curl -X ${har.method} ${shellEscape(fullUrl(har))}`];
92
+ for (const h of har.headers) {
93
+ lines.push(` -H ${shellEscape(`${h.name}: ${h.value}`)}`);
94
+ }
95
+ if (har.postData?.text) {
96
+ lines.push(` -d ${shellEscape(har.postData.text)}`);
97
+ }
98
+ return lines.join(' \\\n');
99
+ }
100
+
101
+ function jsHeadersLiteral(har: HarRequest, indent: string): string {
102
+ if (!har.headers.length) return '{}';
103
+ const entries = har.headers
104
+ .map((h) => `${indent} ${JSON.stringify(h.name)}: ${JSON.stringify(h.value)}`)
105
+ .join(',\n');
106
+ return `{\n${entries},\n${indent}}`;
107
+ }
108
+
109
+ /** JS template literal — backtick string that preserves newlines and
110
+ * escapes backticks / ``${`` sequences. Makes multi-line JSON body
111
+ * readable inside the fetch() options. */
112
+ function jsTemplateLiteral(s: string): string {
113
+ if (/[`$]/.test(s)) return JSON.stringify(s);
114
+ return `\`${s}\``;
115
+ }
116
+
117
+ function renderFetch(har: HarRequest): string {
118
+ const url = fullUrl(har);
119
+ const options: string[] = [` method: ${JSON.stringify(har.method)}`];
120
+ if (har.headers.length) {
121
+ options.push(` headers: ${jsHeadersLiteral(har, ' ')}`);
122
+ }
123
+ if (har.postData?.text) {
124
+ options.push(` body: ${jsTemplateLiteral(har.postData.text)}`);
125
+ }
126
+ return `const response = await fetch(${JSON.stringify(url)}, {\n${options.join(',\n')},\n});\nconst data = await response.json();`;
127
+ }
128
+
129
+ function renderAxios(har: HarRequest): string {
130
+ const url = fullUrl(har);
131
+ const config: string[] = [
132
+ ` method: ${JSON.stringify(har.method.toLowerCase())}`,
133
+ ` url: ${JSON.stringify(url)}`,
134
+ ];
135
+ if (har.headers.length) {
136
+ config.push(` headers: ${jsHeadersLiteral(har, ' ')}`);
137
+ }
138
+ if (har.postData?.text) {
139
+ // Axios auto-serializes objects; we pass the raw JSON string as data.
140
+ config.push(` data: ${har.postData.text}`);
141
+ }
142
+ return `import axios from 'axios';\n\nconst { data } = await axios({\n${config.join(',\n')},\n});`;
143
+ }
144
+
145
+ function renderPython(har: HarRequest): string {
146
+ const lines: string[] = [`import requests`, ``];
147
+ lines.push(`url = ${JSON.stringify(fullUrl(har))}`);
148
+ if (har.headers.length) {
149
+ const headerEntries = har.headers
150
+ .map((h) => ` ${JSON.stringify(h.name)}: ${JSON.stringify(h.value)}`)
151
+ .join(',\n');
152
+ lines.push(`headers = {\n${headerEntries},\n}`);
153
+ }
154
+ if (har.postData?.text) {
155
+ // ``json=payload`` on ``requests`` expects a Python object, not
156
+ // a JSON string — so we leave the body as a raw dict literal
157
+ // (which Python parses identically to the JSON source for the
158
+ // shapes we generate). No wrapping helper needed here.
159
+ lines.push(`payload = ${har.postData.text}`);
160
+ }
161
+ const args = [`url`];
162
+ if (har.headers.length) args.push(`headers=headers`);
163
+ if (har.postData?.text) args.push(`json=payload`);
164
+ lines.push(``, `response = requests.${har.method.toLowerCase()}(${args.join(', ')})`);
165
+ lines.push(`data = response.json()`);
166
+ return lines.join('\n');
167
+ }
168
+
169
+ function renderGo(har: HarRequest): string {
170
+ const url = fullUrl(har);
171
+ const lines: string[] = [
172
+ `package main`,
173
+ ``,
174
+ `import (`,
175
+ ` "fmt"`,
176
+ ` "io"`,
177
+ ];
178
+ if (har.postData?.text) lines.push(` "strings"`);
179
+ lines.push(` "net/http"`);
180
+ lines.push(`)`, ``, `func main() {`);
181
+ if (har.postData?.text) {
182
+ lines.push(` payload := strings.NewReader(${goRawString(har.postData.text)})`);
183
+ lines.push(` req, _ := http.NewRequest(${JSON.stringify(har.method)}, ${JSON.stringify(url)}, payload)`);
184
+ } else {
185
+ lines.push(` req, _ := http.NewRequest(${JSON.stringify(har.method)}, ${JSON.stringify(url)}, nil)`);
186
+ }
187
+ for (const h of har.headers) {
188
+ lines.push(` req.Header.Add(${JSON.stringify(h.name)}, ${JSON.stringify(h.value)})`);
189
+ }
190
+ lines.push(
191
+ ``,
192
+ ` res, _ := http.DefaultClient.Do(req)`,
193
+ ` defer res.Body.Close()`,
194
+ ` body, _ := io.ReadAll(res.Body)`,
195
+ ` fmt.Println(string(body))`,
196
+ `}`,
197
+ );
198
+ return lines.join('\n');
199
+ }
200
+
201
+ function renderPhp(har: HarRequest): string {
202
+ const lines: string[] = [`<?php`, ``, `$curl = curl_init();`, ``, `curl_setopt_array($curl, [`];
203
+ lines.push(` CURLOPT_URL => ${JSON.stringify(fullUrl(har))},`);
204
+ lines.push(` CURLOPT_RETURNTRANSFER => true,`);
205
+ lines.push(` CURLOPT_CUSTOMREQUEST => ${JSON.stringify(har.method)},`);
206
+ if (har.postData?.text) {
207
+ lines.push(` CURLOPT_POSTFIELDS => ${phpHeredoc(har.postData.text)},`);
208
+ }
209
+ if (har.headers.length) {
210
+ const headerList = har.headers
211
+ .map((h) => ` ${JSON.stringify(`${h.name}: ${h.value}`)}`)
212
+ .join(',\n');
213
+ lines.push(` CURLOPT_HTTPHEADER => [\n${headerList},\n ],`);
214
+ }
215
+ lines.push(`]);`, ``, `$response = curl_exec($curl);`, `curl_close($curl);`, `echo $response;`);
216
+ return lines.join('\n');
217
+ }
218
+
219
+ function renderRuby(har: HarRequest): string {
220
+ const lines: string[] = [
221
+ `require 'net/http'`,
222
+ `require 'uri'`,
223
+ `require 'json'`,
224
+ ``,
225
+ `uri = URI(${JSON.stringify(fullUrl(har))})`,
226
+ `http = Net::HTTP.new(uri.host, uri.port)`,
227
+ `http.use_ssl = uri.scheme == 'https'`,
228
+ ``,
229
+ ];
230
+ const methodClass = har.method.charAt(0) + har.method.slice(1).toLowerCase();
231
+ lines.push(`request = Net::HTTP::${methodClass}.new(uri)`);
232
+ for (const h of har.headers) {
233
+ lines.push(`request[${JSON.stringify(h.name)}] = ${JSON.stringify(h.value)}`);
234
+ }
235
+ if (har.postData?.text) {
236
+ lines.push(`request.body = ${rubyHeredoc(har.postData.text)}`);
237
+ }
238
+ lines.push(``, `response = http.request(request)`, `puts response.body`);
239
+ return lines.join('\n');
240
+ }
241
+
242
+ function renderJava(har: HarRequest): string {
243
+ const lines: string[] = [
244
+ `OkHttpClient client = new OkHttpClient();`,
245
+ ``,
246
+ ];
247
+ if (har.postData?.text) {
248
+ lines.push(
249
+ `MediaType mediaType = MediaType.parse("application/json");`,
250
+ `RequestBody body = RequestBody.create(mediaType, ${javaTextBlock(har.postData.text)});`,
251
+ ``,
252
+ );
253
+ }
254
+ lines.push(`Request request = new Request.Builder()`);
255
+ lines.push(` .url(${JSON.stringify(fullUrl(har))})`);
256
+ if (har.postData?.text) {
257
+ lines.push(` .method(${JSON.stringify(har.method)}, body)`);
258
+ } else {
259
+ lines.push(` .method(${JSON.stringify(har.method)}, null)`);
260
+ }
261
+ for (const h of har.headers) {
262
+ lines.push(` .addHeader(${JSON.stringify(h.name)}, ${JSON.stringify(h.value)})`);
263
+ }
264
+ lines.push(` .build();`, ``, `Response response = client.newCall(request).execute();`);
265
+ return lines.join('\n');
266
+ }
267
+
268
+ const RENDERERS: Record<CodeSampleTargetId, (har: HarRequest) => string> = {
269
+ curl: renderCurl,
270
+ fetch: renderFetch,
271
+ axios: renderAxios,
272
+ python: renderPython,
273
+ go: renderGo,
274
+ php: renderPhp,
275
+ ruby: renderRuby,
276
+ java: renderJava,
277
+ };
278
+
279
+ export function renderSnippet(har: HarRequest, targetId: CodeSampleTargetId): string | null {
280
+ const renderer = RENDERERS[targetId];
281
+ if (!renderer) return null;
282
+ try {
283
+ return renderer(har);
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
@@ -5,7 +5,10 @@
5
5
  */
6
6
 
7
7
  export * from './apiKeyManager';
8
+ export * from './codeSamples';
9
+ export * from './operationToHar';
8
10
  export * from './versionManager';
9
11
  export * from './formatters';
12
+ export * from './sampler';
10
13
  export * from './schemaExport';
11
14
  export * from './url';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * OpenAPI endpoint → HAR request.
3
+ *
4
+ * Our in-house code-sample renderers consume a HAR 1.2-shaped request.
5
+ * This module shapes an ``ApiEndpoint`` + user-provided parameter
6
+ * values into that form — path substitution, query-string assembly,
7
+ * header inference, body formatting.
8
+ *
9
+ * We keep the HAR intermediate (rather than a bespoke type) so the
10
+ * renderers match well-known HAR semantics and stay easy to extend.
11
+ */
12
+
13
+ import type { ApiEndpoint } from '../types';
14
+
15
+ /** HAR 1.2 request subset that our renderers consume. We don't ship
16
+ * the full HAR type surface — just the fields that snippet generators
17
+ * actually read. */
18
+ export interface HarRequest {
19
+ method: string;
20
+ url: string;
21
+ httpVersion: string;
22
+ headers: Array<{ name: string; value: string }>;
23
+ queryString: Array<{ name: string; value: string }>;
24
+ cookies: Array<{ name: string; value: string }>;
25
+ headersSize: number;
26
+ bodySize: number;
27
+ postData?: {
28
+ mimeType: string;
29
+ text: string;
30
+ };
31
+ }
32
+
33
+ export interface BuildHarInput {
34
+ endpoint: ApiEndpoint;
35
+ /** Raw body string — whatever the user typed in the playground or
36
+ * the pre-sampled example. Passed through verbatim; the caller
37
+ * decides the content shape. */
38
+ body?: string;
39
+ /** Path-param + query-param values, keyed by name. We look up each
40
+ * parameter on ``endpoint`` to decide whether it slots into the
41
+ * path or the query string. */
42
+ parameters?: Record<string, string>;
43
+ /** Extra headers to merge in (e.g. ``X-API-Key``, ``Authorization``).
44
+ * Later entries with the same name override earlier ones. */
45
+ headers?: Record<string, string>;
46
+ /** Override the request URL's base. When absent we use
47
+ * ``endpoint.path`` as-is (extractor already prepended base URL). */
48
+ baseUrl?: string;
49
+ }
50
+
51
+ /** Split a template path into path vs query. Substitutes ``{id}``-style
52
+ * placeholders using the provided parameter map; unused entries flow
53
+ * into the query string. */
54
+ function buildUrl(
55
+ endpoint: ApiEndpoint,
56
+ parameters: Record<string, string>,
57
+ baseUrl?: string,
58
+ ): { url: string; queryString: HarRequest['queryString'] } {
59
+ const pathParamNames = new Set(
60
+ (endpoint.parameters ?? [])
61
+ .filter((p) => endpoint.path.includes(`{${p.name}}`))
62
+ .map((p) => p.name),
63
+ );
64
+
65
+ let path = endpoint.path;
66
+ for (const name of pathParamNames) {
67
+ const value = parameters[name] ?? `{${name}}`;
68
+ path = path.replaceAll(`{${name}}`, encodeURIComponent(value));
69
+ }
70
+
71
+ const queryString: HarRequest['queryString'] = [];
72
+ for (const param of endpoint.parameters ?? []) {
73
+ if (pathParamNames.has(param.name)) continue;
74
+ const value = parameters[param.name];
75
+ if (value === undefined || value === '') continue;
76
+ queryString.push({ name: param.name, value });
77
+ }
78
+
79
+ const url = baseUrl ? `${baseUrl.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}` : path;
80
+ return { url, queryString };
81
+ }
82
+
83
+ /** Build a HAR 1.2 request from an API endpoint + current form values. */
84
+ export function buildHarRequest(input: BuildHarInput): HarRequest {
85
+ const { endpoint, body, parameters = {}, headers = {}, baseUrl } = input;
86
+ const { url, queryString } = buildUrl(endpoint, parameters, baseUrl);
87
+
88
+ const hasBody = Boolean(body && body.trim().length > 0);
89
+ const bodyMime = hasBody
90
+ ? 'application/json'
91
+ : undefined;
92
+
93
+ // Merge headers. Caller wins, but we default Content-Type for bodies
94
+ // and Accept for JSON endpoints — snippet consumers expect these.
95
+ const mergedHeaders: Record<string, string> = {};
96
+ if (hasBody && bodyMime) mergedHeaders['Content-Type'] = bodyMime;
97
+ mergedHeaders['Accept'] = 'application/json';
98
+ for (const [k, v] of Object.entries(headers)) {
99
+ if (v === undefined || v === '') continue;
100
+ mergedHeaders[k] = v;
101
+ }
102
+
103
+ const har: HarRequest = {
104
+ method: endpoint.method.toUpperCase(),
105
+ url,
106
+ httpVersion: 'HTTP/1.1',
107
+ headers: Object.entries(mergedHeaders).map(([name, value]) => ({ name, value })),
108
+ queryString,
109
+ cookies: [],
110
+ headersSize: -1,
111
+ bodySize: hasBody ? new TextEncoder().encode(body!).length : 0,
112
+ };
113
+
114
+ if (hasBody && bodyMime) {
115
+ har.postData = { mimeType: bodyMime, text: body! };
116
+ }
117
+
118
+ return har;
119
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * JSON Schema → example JSON.
3
+ *
4
+ * Thin wrapper over ``openapi-sampler`` (by Redocly — same library Redoc
5
+ * uses internally). Picks deterministic, realistic values from a
6
+ * dereferenced schema: honours ``const`` / ``examples`` / ``enum`` /
7
+ * ``default``, unpacks ``allOf`` / ``oneOf`` / ``anyOf``, respects
8
+ * string ``format`` (email/uuid/date-time/…).
9
+ *
10
+ * Replaces the previous hand-rolled ``exampleFromSchema`` — stricter on
11
+ * spec semantics and cheaper to maintain.
12
+ */
13
+
14
+ import consola from 'consola';
15
+ import { sample as openapiSample } from 'openapi-sampler';
16
+
17
+ type JsonSchemaLike = Record<string, unknown>;
18
+
19
+ export interface SampleOptions {
20
+ /** Skip properties marked ``readOnly`` — useful when the body will
21
+ * be sent in a request (request body editor). Default ``false``. */
22
+ skipReadOnly?: boolean;
23
+ /** Skip properties marked ``writeOnly`` — useful when rendering a
24
+ * response body (passwords etc. must not leak). Default ``false``. */
25
+ skipWriteOnly?: boolean;
26
+ /** Skip non-required properties. Rarely useful for a viewer — we
27
+ * want to show the full shape. Default ``false``. */
28
+ skipNonRequired?: boolean;
29
+ }
30
+
31
+ /** Sample a JSON schema into a plain value. Returns ``null`` on failure
32
+ * (malformed schema, circular refs the sampler can't unwind) so the
33
+ * caller can show "no example" instead of crashing the page.
34
+ *
35
+ * ``spec`` must be the root OpenAPI document whenever ``schema`` may
36
+ * contain ``$ref`` nodes — ``openapi-sampler`` walks the tree and
37
+ * resolves refs against it. Our own ``dereferenceSchema`` only inlines
38
+ * refs up to a depth limit; anything deeper comes through here as a
39
+ * live ``$ref`` and the sampler throws if ``spec`` is missing. */
40
+ export function sampleSchema(
41
+ schema: JsonSchemaLike | undefined,
42
+ options: SampleOptions = {},
43
+ spec?: unknown,
44
+ ): unknown {
45
+ if (!schema) return null;
46
+ try {
47
+ return openapiSample(schema as never, options, spec as never);
48
+ } catch (err) {
49
+ // Sampler failures used to be silent, which meant "no example"
50
+ // in the UI looked like a missing spec entry. Log so we can see
51
+ // the real cause in dev (circular refs, missing type, etc.).
52
+ consola.warn('[OpenapiViewer] sampleSchema failed:', err, { schema });
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** Same as ``sampleSchema`` but returns a pre-stringified JSON payload
58
+ * ready to drop into a textarea / code block. Returns ``undefined``
59
+ * when sampling fails so the UI can conditionally hide the example. */
60
+ export function sampleSchemaJson(
61
+ schema: JsonSchemaLike | undefined,
62
+ options: SampleOptions = {},
63
+ spec?: unknown,
64
+ ): string | undefined {
65
+ const value = sampleSchema(schema, options, spec);
66
+ if (value === null || value === undefined) return undefined;
67
+ try {
68
+ return JSON.stringify(value, null, 2);
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Find the nearest ancestor that actually scrolls vertically, or
3
+ * ``window`` when none exists. Handles the common embed scenarios:
4
+ *
5
+ * - Standalone page: nothing in the chain scrolls → caller scrolls
6
+ * ``window`` and listens to ``window.scroll``.
7
+ * - Dev playground / modal shell: an intermediate ``overflow-auto``
8
+ * container scrolls → caller scrolls that element and listens to
9
+ * *its* ``scroll`` events (which do NOT bubble to window).
10
+ *
11
+ * A "scrollable" ancestor is one whose computed ``overflow-y`` is
12
+ * ``auto`` or ``scroll`` AND whose content actually overflows. We bail
13
+ * before ``document.body`` — ``documentElement`` is represented by
14
+ * ``window`` in the caller's hot path, so returning the body itself
15
+ * would double-count the scroll surface.
16
+ */
17
+ export type ScrollTarget = HTMLElement | Window;
18
+
19
+ export function getScrollParent(el: HTMLElement | null): ScrollTarget {
20
+ if (typeof window === 'undefined') return (null as unknown) as Window;
21
+ if (!el) return window;
22
+
23
+ let cur: HTMLElement | null = el.parentElement;
24
+ while (cur && cur !== document.body && cur !== document.documentElement) {
25
+ const style = getComputedStyle(cur);
26
+ const overflowY = style.overflowY;
27
+ const canScroll =
28
+ (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
29
+ cur.scrollHeight > cur.clientHeight;
30
+ if (canScroll) return cur;
31
+ cur = cur.parentElement;
32
+ }
33
+ return window;
34
+ }
35
+
36
+ /** Top-relative scroll position of the target. */
37
+ export function getScrollTop(target: ScrollTarget): number {
38
+ return target === window ? window.scrollY : (target as HTMLElement).scrollTop;
39
+ }
40
+
41
+ /** Visible viewport height of the target. */
42
+ export function getViewportHeight(target: ScrollTarget): number {
43
+ return target === window ? window.innerHeight : (target as HTMLElement).clientHeight;
44
+ }
45
+
46
+ /** Y coordinate of the target's top edge, in viewport space. Used to
47
+ * translate ``getBoundingClientRect().top`` into target-relative
48
+ * coordinates. For ``window`` this is always ``0``. */
49
+ export function getTargetTop(target: ScrollTarget): number {
50
+ return target === window ? 0 : (target as HTMLElement).getBoundingClientRect().top;
51
+ }
52
+
53
+ /** Scroll the target so that the given absolute Y lands at its top.
54
+ *
55
+ * For ``window`` we ask the browser to animate smoothly — every engine
56
+ * honours that path. Nested ``overflow-auto`` elements are a different
57
+ * story: some layouts (e.g. dev-playground shells with flex parents)
58
+ * silently drop the animation, leaving the user stuck. Direct
59
+ * ``scrollTop`` writes always work, so we use them there — loss of
60
+ * animation beats broken navigation. Consumers who want animated
61
+ * scrolling inside a custom shell can wrap this function themselves. */
62
+ export function scrollTargetTo(target: ScrollTarget, top: number) {
63
+ if (target === window) {
64
+ window.scrollTo({ top, behavior: 'smooth' });
65
+ return;
66
+ }
67
+ (target as HTMLElement).scrollTop = top;
68
+ }
@@ -8,6 +8,13 @@ import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
8
8
  import { FloatingToolbar } from '../../components/FloatingToolbar';
9
9
  import { CopyAction } from '../../components/FloatingToolbar/actions';
10
10
 
11
+ // Load extra Prism grammars (``bash``, ``ruby``, ``java``, ``php``)
12
+ // that aren't in ``prism-react-renderer``'s default bundle. The hook
13
+ // below subscribes to its ready state and re-renders this component
14
+ // once loading completes, so the first tab click gets highlighted
15
+ // correctly even if it happened before the dynamic import resolved.
16
+ import { useEnsurePrismLanguages } from './registerPrismLanguages';
17
+
11
18
  interface PrettyCodeProps {
12
19
  data: string | object;
13
20
  language: Language;
@@ -23,13 +30,24 @@ interface PrettyCodeProps {
23
30
  * Set e.g. ``50`` to cap short snippets inline and scroll long ones.
24
31
  */
25
32
  maxLines?: number;
33
+ /**
34
+ * Visual variant. ``"card"`` (default) ships full chrome (border,
35
+ * background, hover toolbar, optional internal scroll). ``"plain"``
36
+ * is chrome-less — use when embedding inside another scroll
37
+ * container so the surface manages its own chrome and scroll. */
38
+ variant?: 'card' | 'plain';
26
39
  }
27
40
 
28
- const PrettyCode = ({ data, language, className, mode, inline = false, customBg, isCompact = false, scrollIsolation, maxLines }: PrettyCodeProps) => {
41
+ const PrettyCode = ({ data, language, className, mode, inline = false, customBg, isCompact = false, scrollIsolation, maxLines, variant = 'card' }: PrettyCodeProps) => {
29
42
  const containerRef = useRef<HTMLDivElement>(null);
30
43
  const t = useAppT();
31
44
  const detectedTheme = useResolvedTheme();
32
45
 
46
+ // Subscribe to the extra-grammars ready state. When ``bash`` /
47
+ // ``ruby`` / ``java`` / ``php`` finish loading, this hook triggers a
48
+ // re-render so the code block picks up the new grammar.
49
+ useEnsurePrismLanguages();
50
+
33
51
  const labels = useMemo(() => ({
34
52
  copyCode: t('tools.code.copyCode'),
35
53
  noContent: t('tools.code.noContent'),
@@ -110,6 +128,16 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
110
128
  return 'Text';
111
129
  case 'mermaid':
112
130
  return 'Mermaid';
131
+ case 'ruby':
132
+ case 'rb':
133
+ return 'Ruby';
134
+ case 'java':
135
+ return 'Java';
136
+ case 'php':
137
+ return 'PHP';
138
+ case 'go':
139
+ case 'golang':
140
+ return 'Go';
113
141
  default:
114
142
  return lang.charAt(0).toUpperCase() + lang.slice(1);
115
143
  }
@@ -140,7 +168,18 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
140
168
  return 'markup';
141
169
  case 'bash':
142
170
  case 'shell':
171
+ case 'sh':
143
172
  return 'bash';
173
+ case 'ruby':
174
+ case 'rb':
175
+ return 'ruby';
176
+ case 'java':
177
+ return 'java';
178
+ case 'php':
179
+ return 'php';
180
+ case 'go':
181
+ case 'golang':
182
+ return 'go';
144
183
  case 'sql':
145
184
  return 'sql';
146
185
  case 'yaml':
@@ -160,6 +199,54 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
160
199
 
161
200
  const displayLanguage = getLanguageDisplayName(language);
162
201
 
202
+ // Plain variant — chrome-less render for embedding inside other
203
+ // scroll containers. No border, no background, no hover toolbar, no
204
+ // internal scroll. The caller's ScrollArea/panel owns the chrome
205
+ // and scroll responsibilities.
206
+ if (variant === 'plain') {
207
+ return (
208
+ <Highlight theme={prismTheme} code={contentJson} language={normalizedLanguage as Language}>
209
+ {({ className: prismClassName, style, tokens, getLineProps, getTokenProps }) => {
210
+ const { backgroundColor: _bg, ...restStyle } = style;
211
+ return (
212
+ <pre
213
+ className={`${prismClassName} ${className || ''}`}
214
+ style={{
215
+ ...restStyle,
216
+ background: 'transparent',
217
+ margin: 0,
218
+ padding: isCompact ? '0.75rem 1rem' : '1rem',
219
+ fontSize,
220
+ lineHeight: lineHeightRatio,
221
+ fontFamily: 'monospace',
222
+ // ``break-all`` (not ``break-word``) so long unbroken
223
+ // strings without whitespace — typical of escaped
224
+ // JSON bodies in generated code samples — wrap at any
225
+ // character rather than overflowing the container.
226
+ whiteSpace: 'pre-wrap',
227
+ wordBreak: 'break-all',
228
+ overflowWrap: 'anywhere',
229
+ // Hard cap on width — parents (grid cells, panels)
230
+ // should constrain us but some callers render inside
231
+ // ``flex: 1 1 auto`` which lets the pre grow past
232
+ // intended bounds. ``max-width: 100%`` pins to parent.
233
+ maxWidth: '100%',
234
+ }}
235
+ >
236
+ {tokens.map((line, i) => (
237
+ <div key={i} {...getLineProps({ line })}>
238
+ {line.map((token, key) => (
239
+ <span key={key} {...getTokenProps({ token })} />
240
+ ))}
241
+ </div>
242
+ ))}
243
+ </pre>
244
+ );
245
+ }}
246
+ </Highlight>
247
+ );
248
+ }
249
+
163
250
  if (inline) {
164
251
  const inlineBgClass = customBg || (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-100');
165
252
  return (