@clidey/dory 0.9.0 → 0.11.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.
@@ -0,0 +1,369 @@
1
+ import { motion } from "motion/react"
2
+ import { useState } from "preact/hooks"
3
+ import { APIGroup } from "./api"
4
+ import { PlayButton } from "./api-playground"
5
+ import { Fence } from "./fence"
6
+ import { Properties, Property } from "./open-api"
7
+ import { Tag } from "./tag"
8
+ import { Col, Row } from "./text"
9
+ import { WebSocketPlayground, type WebSocketServer } from "./websocket-playground"
10
+
11
+ interface AsyncAPIMessage {
12
+ name?: string
13
+ title?: string
14
+ summary?: string
15
+ description?: string
16
+ payload?: Schema
17
+ headers?: Schema
18
+ correlationId?: {
19
+ description?: string
20
+ location?: string
21
+ }
22
+ schemaFormat?: string
23
+ contentType?: string
24
+ examples?: any[]
25
+ }
26
+
27
+ interface AsyncAPIOperation {
28
+ operationId?: string
29
+ summary?: string
30
+ description?: string
31
+ tags?: Array<{
32
+ name: string
33
+ description?: string
34
+ }>
35
+ message?: AsyncAPIMessage | {
36
+ oneOf?: AsyncAPIMessage[]
37
+ }
38
+ bindings?: {
39
+ ws?: {
40
+ method?: string
41
+ query?: Schema
42
+ headers?: Schema
43
+ bindingVersion?: string
44
+ }
45
+ }
46
+ }
47
+
48
+ interface AsyncAPIChannel {
49
+ address?: string
50
+ title?: string
51
+ summary?: string
52
+ description?: string
53
+ subscribe?: AsyncAPIOperation
54
+ publish?: AsyncAPIOperation
55
+ parameters?: Record<string, Parameter>
56
+ messages?: Record<string, AsyncAPIMessage>
57
+ bindings?: {
58
+ ws?: {
59
+ method?: string
60
+ query?: Schema
61
+ headers?: Schema
62
+ bindingVersion?: string
63
+ }
64
+ }
65
+ }
66
+
67
+ interface Schema {
68
+ $ref?: string
69
+ type?: string
70
+ items?: Schema
71
+ properties?: Record<string, Schema>
72
+ required?: string[]
73
+ description?: string
74
+ default?: any
75
+ example?: any
76
+ format?: string
77
+ enum?: any[]
78
+ oneOf?: Schema[]
79
+ anyOf?: Schema[]
80
+ allOf?: Schema[]
81
+ }
82
+
83
+ interface Parameter {
84
+ description?: string
85
+ schema?: Schema
86
+ location?: string
87
+ default?: any
88
+ }
89
+
90
+ interface Components {
91
+ schemas?: Record<string, Schema>
92
+ messages?: Record<string, AsyncAPIMessage>
93
+ parameters?: Record<string, Parameter>
94
+ }
95
+
96
+ interface Info {
97
+ title: string
98
+ version: string
99
+ description?: string
100
+ termsOfService?: string
101
+ contact?: {
102
+ name?: string
103
+ url?: string
104
+ email?: string
105
+ }
106
+ license?: {
107
+ name: string
108
+ url?: string
109
+ }
110
+ }
111
+
112
+ export interface AsyncAPIJson {
113
+ asyncapi: string
114
+ info: Info
115
+ servers?: Record<string, WebSocketServer & {
116
+ host?: string
117
+ pathname?: string
118
+ protocol?: string
119
+ protocolVersion?: string
120
+ description?: string
121
+ title?: string
122
+ summary?: string
123
+ variables?: Record<string, any>
124
+ security?: any[]
125
+ tags?: Array<{
126
+ name: string
127
+ description?: string
128
+ }>
129
+ bindings?: any
130
+ }>
131
+ channels: Record<string, AsyncAPIChannel>
132
+ components?: Components
133
+ defaultContentType?: string
134
+ }
135
+
136
+ function resolveSchema(schema: Schema | undefined, components: Components): Schema {
137
+ if (!schema) return {}
138
+
139
+ if (schema.$ref) {
140
+ const refPath = schema.$ref.replace(/^#\/components\/schemas\//, '')
141
+ return resolveSchema(components.schemas?.[refPath], components)
142
+ }
143
+
144
+ if (schema.type === 'array' && schema.items) {
145
+ return {
146
+ ...schema,
147
+ items: resolveSchema(schema.items, components)
148
+ }
149
+ }
150
+
151
+ if (schema.properties) {
152
+ const resolvedProps: Record<string, Schema> = {}
153
+ for (const [key, value] of Object.entries(schema.properties)) {
154
+ resolvedProps[key] = resolveSchema(value, components)
155
+ }
156
+ return {
157
+ ...schema,
158
+ properties: resolvedProps
159
+ }
160
+ }
161
+
162
+ return schema
163
+ }
164
+
165
+ function isDirectMessage(message: AsyncAPIMessage | { oneOf?: AsyncAPIMessage[] } | undefined): message is AsyncAPIMessage {
166
+ return message != null && !('oneOf' in message);
167
+ }
168
+
169
+ export function AsyncAPI({
170
+ asyncAPIJson,
171
+ channel: selectedChannel,
172
+ operation: selectedOperation
173
+ }: {
174
+ asyncAPIJson: AsyncAPIJson
175
+ channel: string
176
+ operation: 'subscribe' | 'publish'
177
+ }) {
178
+ const { channels, components, servers } = asyncAPIJson;
179
+ const [showPlayground, setShowPlayground] = useState(false)
180
+
181
+ const handleShowPlayground = () => {
182
+ setShowPlayground(true)
183
+ }
184
+
185
+ // Convert AsyncAPI servers to WebSocket servers
186
+ const wsServers: WebSocketServer[] = servers ? Object.entries(servers).map(([key, server]) => ({
187
+ url: `${server.protocol}://${server.host}${server.pathname || ''}`,
188
+ description: server.description || server.title || key,
189
+ protocol: server.protocol
190
+ })) : [];
191
+
192
+ return (
193
+ <div className="max-w-none w-full">
194
+ {showPlayground && (
195
+ <motion.div
196
+ initial={{ x: "100%" }}
197
+ animate={{ x: 0 }}
198
+ exit={{ x: "100%" }}
199
+ transition={{ type: "spring", damping: 20 }}
200
+ className="fixed inset-y-0 right-0 w-full max-w-3xl bg-white dark:bg-[#1e1e1e] shadow-xl z-50 overflow-y-auto border-l border-zinc-200 dark:border-zinc-800"
201
+ >
202
+ <div className="p-6">
203
+ <div className="flex justify-between items-center mb-6">
204
+ <h2 className="text-xl font-semibold">WebSocket Playground</h2>
205
+ <button
206
+ onClick={() => setShowPlayground(false)}
207
+ className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
208
+ >
209
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
210
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
211
+ </svg>
212
+ </button>
213
+ </div>
214
+ <WebSocketPlayground
215
+ url={selectedChannel}
216
+ servers={wsServers}
217
+ />
218
+ </div>
219
+ </motion.div>
220
+ )}
221
+
222
+ {Object.entries(channels).map(([channelPath, channelDetails]) => {
223
+ if (channelPath !== selectedChannel) {
224
+ return null
225
+ }
226
+
227
+ const operation = channelDetails[selectedOperation]
228
+ if (!operation) {
229
+ return null
230
+ }
231
+
232
+ const { summary, description, message } = operation
233
+ const resolvedMessage = isDirectMessage(message) ? message :
234
+ (message?.oneOf && message.oneOf.length > 0 ? message.oneOf[0] : undefined)
235
+
236
+ return (
237
+ <div key={channelPath} className="my-8">
238
+ <Row cols={2}>
239
+ <Col>
240
+ <h2 className="flex items-center mt-4">
241
+ <Tag variant="medium" className="mr-2 py-1 px-4 rounded-lg">
242
+ {selectedOperation.toUpperCase()}
243
+ </Tag>
244
+ <span className="ml-2 font-mono">{channelPath}</span>
245
+ </h2>
246
+
247
+ {(summary || description) && (
248
+ <p>{summary || description}</p>
249
+ )}
250
+
251
+ {resolvedMessage && (
252
+ <div className="mt-4">
253
+ <h3>Message</h3>
254
+ {resolvedMessage.title && <h4>{resolvedMessage.title}</h4>}
255
+ {resolvedMessage.description && <p>{resolvedMessage.description}</p>}
256
+
257
+ {resolvedMessage.payload && (
258
+ <div className="mt-4">
259
+ <h4>Payload</h4>
260
+ {(() => {
261
+ const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
262
+ return (
263
+ <Properties>
264
+ {Object.entries(resolvedPayload.properties || {}).map(([propName, prop]) => (
265
+ <Property
266
+ key={propName}
267
+ name={propName}
268
+ type={prop.type || 'string'}
269
+ required={resolvedPayload.required?.includes(propName)}
270
+ >
271
+ {prop.description}
272
+ {prop.default !== undefined && (
273
+ <span className="text-gray-500 ml-1">(default: {JSON.stringify(prop.default)})</span>
274
+ )}
275
+ </Property>
276
+ ))}
277
+ </Properties>
278
+ )
279
+ })()}
280
+ </div>
281
+ )}
282
+
283
+ {resolvedMessage.headers && (
284
+ <div className="mt-4">
285
+ <h4>Headers</h4>
286
+ {(() => {
287
+ const resolvedHeaders = resolveSchema(resolvedMessage.headers, components || {})
288
+ return (
289
+ <Properties>
290
+ {Object.entries(resolvedHeaders.properties || {}).map(([headerName, header]) => (
291
+ <Property
292
+ key={headerName}
293
+ name={headerName}
294
+ type={header.type || 'string'}
295
+ required={resolvedHeaders.required?.includes(headerName)}
296
+ >
297
+ {header.description}
298
+ </Property>
299
+ ))}
300
+ </Properties>
301
+ )
302
+ })()}
303
+ </div>
304
+ )}
305
+
306
+ {resolvedMessage?.examples && resolvedMessage.examples.length > 0 && (
307
+ <div className="mt-4">
308
+ <h4>Examples</h4>
309
+ {resolvedMessage.examples.map((example: any, idx: number) => (
310
+ <Fence key={idx} language="json">
311
+ {JSON.stringify(example, null, 2)}
312
+ </Fence>
313
+ ))}
314
+ </div>
315
+ )}
316
+ </div>
317
+ )}
318
+
319
+ {components?.schemas && (
320
+ <div className="mt-4">
321
+ <h3>Schemas</h3>
322
+ {Object.entries(components.schemas).filter(([, schema]) => schema.properties).map(([name, schema]) => (
323
+ <div key={name} className="mt-4">
324
+ <h4>{name}</h4>
325
+ <Properties>
326
+ {Object.entries(schema.properties || {}).map(([propName, prop]) => (
327
+ <Property
328
+ key={propName}
329
+ name={propName}
330
+ type={prop.type || 'string'}
331
+ required={schema.required?.includes(propName)}
332
+ >
333
+ {prop.description}
334
+ </Property>
335
+ ))}
336
+ </Properties>
337
+ </div>
338
+ ))}
339
+ </div>
340
+ )}
341
+ </Col>
342
+ <Col sticky>
343
+ <button
344
+ onClick={handleShowPlayground}
345
+ className='ml-auto mt-3 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition bg-sky-600 text-white hover:bg-sky-700'
346
+ >
347
+ Try
348
+ <PlayButton className="h-4 w-4" />
349
+ </button>
350
+ <APIGroup title="Message" tag={selectedOperation.toUpperCase()} label={channelPath}>
351
+ <>
352
+ {resolvedMessage?.payload && (() => {
353
+ const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
354
+ return (
355
+ <Fence language="json">
356
+ {JSON.stringify(resolvedPayload, null, 2)}
357
+ </Fence>
358
+ )
359
+ })()}
360
+ </>
361
+ </APIGroup>
362
+ </Col>
363
+ </Row>
364
+ </div>
365
+ )
366
+ })}
367
+ </div>
368
+ )
369
+ }
package/src/mdx/mdx.tsx CHANGED
@@ -18,6 +18,8 @@ export { Steps, Step } from './step';
18
18
  export { Table as table, Th as th, Td as td } from './table';
19
19
  export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
20
20
  export { APIPlayground } from './api-playground';
21
+ export { WebSocketPlayground } from './websocket-playground';
22
+ export { AsyncAPI } from './async-api';
21
23
  export { Source } from './source';
22
24
 
23
25
  export const wrapper = ({ children }: { children: preact.ComponentChildren }) => {
@@ -40,4 +42,6 @@ export const wrapper = ({ children }: { children: preact.ComponentChildren }) =>
40
42
  };
41
43
 
42
44
 
43
- export const hr = () => <hr className="my-4 text-gray-500/25 dark:text-gray-200/25" />;
45
+ export const hr = () => <hr className="my-4 text-gray-500/25 dark:text-gray-200/25" />;
46
+
47
+ export const br = () => <br />;
@@ -2,6 +2,20 @@ import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
2
2
  import classNames from 'classnames'
3
3
  import { useEffect, useMemo, useRef, useState } from 'preact/compat'
4
4
 
5
+ function fixNodeLabelsWithSpecialChars(content: string): string {
6
+ // Pattern to match node definitions with brackets containing special characters
7
+ // Matches: NodeId[text content] where text content has special chars and isn't already quoted
8
+ const nodePattern = /(\w+)\[([^\]"]*[(),\s][^\]"]*)\]/g;
9
+
10
+ return content.replace(nodePattern, (match, nodeId, labelContent) => {
11
+ // Only wrap in quotes if not already quoted and contains special characters
12
+ if (!labelContent.startsWith('"') || !labelContent.endsWith('"')) {
13
+ return `${nodeId}["${labelContent}"]`;
14
+ }
15
+ return match;
16
+ });
17
+ }
18
+
5
19
  export function MermaidRenderer({ content }: { content: string }) {
6
20
  const mermaidRef = useRef<HTMLDivElement>(null)
7
21
  const [isFullscreen, setIsFullscreen] = useState(false)
@@ -47,9 +61,12 @@ export function MermaidRenderer({ content }: { content: string }) {
47
61
  .replace(/^\s*classDef .*$/gm, '')
48
62
  .replace(/^\s*class .*$/gm, '');
49
63
 
64
+ // Fix node labels with special characters by wrapping them in quotes
65
+ const processedContent = fixNodeLabelsWithSpecialChars(contentWithoutStyle);
66
+
50
67
  // Validate the mermaid syntax first
51
68
  try {
52
- await mermaid.parse(contentWithoutStyle)
69
+ await mermaid.parse(processedContent)
53
70
  setIsValid(true)
54
71
  } catch (parseError) {
55
72
  setIsValid(false)
@@ -59,7 +76,7 @@ export function MermaidRenderer({ content }: { content: string }) {
59
76
  return
60
77
  }
61
78
 
62
- const { svg } = await mermaid.render(diagramId, contentWithoutStyle);
79
+ const { svg } = await mermaid.render(diagramId, processedContent);
63
80
 
64
81
  if (mounted && mermaidRef.current) {
65
82
  mermaidRef.current.innerHTML = svg