@usecross/docs 0.11.0 → 0.12.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,257 @@
1
+ import { useState } from 'react'
2
+ import type { GriffeClass, GriffeFunction, GriffeAttribute, GriffeExpression } from '../../types'
3
+ import { Docstring } from './Docstring'
4
+ import { FunctionDoc } from './FunctionDoc'
5
+ import { CodeSpan } from './CodeSpan'
6
+
7
+ /**
8
+ * Render a type annotation expression or value to string
9
+ */
10
+ function renderExpression(expr: GriffeExpression | string | undefined): string {
11
+ if (!expr) return ''
12
+ if (typeof expr === 'string') return expr
13
+ if (expr.str) return expr.str
14
+ if (expr.canonical) return expr.canonical
15
+
16
+ const exprAny = expr as any
17
+
18
+ // Handle ExprName with member reference
19
+ if (expr.name && typeof expr.name === 'string') return expr.name
20
+
21
+ // Handle ExprBoolOp (like `config or StrawberryConfig()`)
22
+ if (exprAny.cls === 'ExprBoolOp' && exprAny.operator && Array.isArray(exprAny.values)) {
23
+ return exprAny.values.map((v: any) => renderExpression(v)).join(` ${exprAny.operator} `)
24
+ }
25
+
26
+ // Handle ExprBinOp (like `type | None`)
27
+ if (exprAny.cls === 'ExprBinOp' && exprAny.left && exprAny.right) {
28
+ const left = renderExpression(exprAny.left)
29
+ const right = renderExpression(exprAny.right)
30
+ const op = exprAny.operator || '|'
31
+ return `${left} ${op} ${right}`
32
+ }
33
+
34
+ // Handle ExprCall (like `StrawberryConfig()`)
35
+ if (exprAny.cls === 'ExprCall' && exprAny.function) {
36
+ const funcName = renderExpression(exprAny.function)
37
+ const args = Array.isArray(exprAny.arguments)
38
+ ? exprAny.arguments.map((a: any) => renderExpression(a)).join(', ')
39
+ : ''
40
+ return `${funcName}(${args})`
41
+ }
42
+
43
+ // Handle ExprAttribute (like contextlib.asynccontextmanager)
44
+ if (exprAny.cls === 'ExprAttribute' && Array.isArray(exprAny.values)) {
45
+ return exprAny.values.map((v: any) => renderExpression(v)).join('.')
46
+ }
47
+
48
+ // Handle ExprList and ExprTuple
49
+ if ('elements' in exprAny && Array.isArray(exprAny.elements)) {
50
+ const inner = exprAny.elements.map((el: any) => renderExpression(el)).join(', ')
51
+ return exprAny.cls === 'ExprTuple' ? `(${inner})` : `[${inner}]`
52
+ }
53
+
54
+ // Handle ExprDict
55
+ if (exprAny.cls === 'ExprDict' && Array.isArray(exprAny.keys) && Array.isArray(exprAny.values)) {
56
+ const pairs = exprAny.keys.map((k: any, i: number) =>
57
+ `${renderExpression(k)}: ${renderExpression(exprAny.values[i])}`
58
+ ).join(', ')
59
+ return `{${pairs}}`
60
+ }
61
+
62
+ // Handle ExprSubscript (like Dict[str, int])
63
+ if (exprAny.left && exprAny.slice) {
64
+ const left = renderExpression(exprAny.left)
65
+ const slice = renderExpression(exprAny.slice)
66
+ return `${left}[${slice}]`
67
+ }
68
+
69
+ // Handle slice expressions
70
+ if ('slice' in exprAny && exprAny.slice && !exprAny.left) {
71
+ return renderExpression(exprAny.slice)
72
+ }
73
+
74
+ // Fallback for unknown expressions - try to avoid [object Object]
75
+ if (typeof expr === 'object') {
76
+ return JSON.stringify(expr)
77
+ }
78
+
79
+ return String(expr)
80
+ }
81
+
82
+ /**
83
+ * Collapsible method item with arrow indicator (strawberry.rocks style)
84
+ */
85
+ function CollapsibleMethod({ method }: { method: GriffeFunction }) {
86
+ const [expanded, setExpanded] = useState(false)
87
+
88
+ return (
89
+ <div className="border-b border-gray-200 dark:border-gray-700 last:border-b-0">
90
+ <button
91
+ onClick={() => setExpanded(!expanded)}
92
+ className="w-full flex items-center gap-2 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
93
+ >
94
+ <span className="font-mono text-base font-semibold text-gray-900 dark:text-white">
95
+ {method.name}
96
+ </span>
97
+ <span className="text-gray-400 text-sm">
98
+ {expanded ? '▲' : '▼'}
99
+ </span>
100
+ </button>
101
+ {expanded && (
102
+ <div className="pb-6">
103
+ <FunctionDoc fn={method} isMethod showName={false} />
104
+ </div>
105
+ )}
106
+ </div>
107
+ )
108
+ }
109
+
110
+ interface ClassDocProps {
111
+ cls: GriffeClass
112
+ /** URL prefix for links */
113
+ prefix?: string
114
+ /** Current path for breadcrumb */
115
+ currentPath?: string
116
+ /** GitHub repo URL for source links */
117
+ githubUrl?: string
118
+ /** Additional CSS class */
119
+ className?: string
120
+ /** Override display path (e.g., for aliases to show alias name instead of target path) */
121
+ displayPath?: string
122
+ }
123
+
124
+ /**
125
+ * Renders documentation for a class matching strawberry.rocks design.
126
+ * Includes: Title, Constructor, Methods (collapsible), Attributes, and footer.
127
+ */
128
+ export function ClassDoc({ cls, prefix: _prefix = '/api', currentPath: _currentPath, githubUrl, className = '', displayPath }: ClassDocProps) {
129
+ const members = cls.members ?? {}
130
+
131
+ // Separate members by type
132
+ const methods: GriffeFunction[] = []
133
+ const attributes: GriffeAttribute[] = []
134
+
135
+ for (const member of Object.values(members)) {
136
+ // Skip private members
137
+ if (member.name.startsWith('_') && !member.name.startsWith('__')) continue
138
+
139
+ if (member.kind === 'function') {
140
+ methods.push(member as GriffeFunction)
141
+ } else if (member.kind === 'attribute') {
142
+ attributes.push(member as GriffeAttribute)
143
+ }
144
+ }
145
+
146
+ // Sort methods: __init__ first, then public methods alphabetically, skip other dunders
147
+ const initMethod = methods.find(m => m.name === '__init__')
148
+ const publicMethods = methods
149
+ .filter(m => m.name !== '__init__' && !m.name.startsWith('_'))
150
+ .sort((a, b) => a.name.localeCompare(b.name))
151
+
152
+ // Sort attributes alphabetically, skip private ones
153
+ const publicAttributes = attributes
154
+ .filter(a => !a.name.startsWith('_'))
155
+ .sort((a, b) => a.name.localeCompare(b.name))
156
+
157
+ // Get relative filepath for display (prefer package-relative path)
158
+ const relativeFilepath = cls.relative_package_filepath || cls.relative_filepath || cls.filepath
159
+
160
+ // Build GitHub URL for source link
161
+ const githubSourceUrl = githubUrl && relativeFilepath && cls.lineno
162
+ ? `${githubUrl}/blob/main/${relativeFilepath}#L${cls.lineno}-L${cls.endlineno || cls.lineno}`
163
+ : undefined
164
+
165
+ return (
166
+ <div className={className}>
167
+ {/* Title - monospace like strawberry.rocks */}
168
+ <h1 id={cls.name} className="font-mono text-2xl font-normal text-gray-900 dark:text-white mb-8">
169
+ {displayPath || cls.path || cls.name}
170
+ </h1>
171
+
172
+ {/* Constructor section */}
173
+ {initMethod && (
174
+ <section id="constructor" className="mb-8">
175
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
176
+ Constructor:
177
+ </h2>
178
+
179
+ {/* Docstring description */}
180
+ {initMethod.docstring && (
181
+ <div className="mb-6">
182
+ <Docstring docstring={initMethod.docstring} showOnlyText />
183
+ </div>
184
+ )}
185
+
186
+ <FunctionDoc fn={initMethod} isMethod showName={false} />
187
+ </section>
188
+ )}
189
+
190
+ {/* Class docstring if no __init__ */}
191
+ {!initMethod && cls.docstring && (
192
+ <section className="mb-8">
193
+ <Docstring docstring={cls.docstring} />
194
+ </section>
195
+ )}
196
+
197
+ {/* Methods section - collapsible */}
198
+ {publicMethods.length > 0 && (
199
+ <section id="methods" className="mb-8">
200
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
201
+ Methods:
202
+ </h2>
203
+ <div>
204
+ {publicMethods.map((method) => (
205
+ <CollapsibleMethod key={method.name} method={method} />
206
+ ))}
207
+ </div>
208
+ </section>
209
+ )}
210
+
211
+ {/* Attributes section */}
212
+ {publicAttributes.length > 0 && (
213
+ <section id="attributes" className="mb-8">
214
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
215
+ Attributes:
216
+ </h2>
217
+ <div className="space-y-2">
218
+ {publicAttributes.map((attr) => (
219
+ <div key={attr.name} className="flex items-baseline gap-2">
220
+ <CodeSpan>{attr.name}:</CodeSpan>
221
+ {attr.annotation && (
222
+ <span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
223
+ {renderExpression(attr.annotation)}
224
+ </span>
225
+ )}
226
+ </div>
227
+ ))}
228
+ </div>
229
+ </section>
230
+ )}
231
+
232
+ {/* Footer with file path and GitHub link */}
233
+ {relativeFilepath && (
234
+ <footer className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 space-y-4">
235
+ <p className="flex items-center gap-2">
236
+ <span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
237
+ File path:
238
+ </span>
239
+ <CodeSpan allowCopy>{relativeFilepath}</CodeSpan>
240
+ </p>
241
+ {githubSourceUrl && (
242
+ <p>
243
+ <a
244
+ href={githubSourceUrl}
245
+ target="_blank"
246
+ rel="noopener noreferrer"
247
+ className="text-sm font-semibold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 uppercase tracking-wide"
248
+ >
249
+ Open in GitHub
250
+ </a>
251
+ </p>
252
+ )}
253
+ </footer>
254
+ )}
255
+ </div>
256
+ )
257
+ }
@@ -0,0 +1,53 @@
1
+ import { cn } from '../../lib/utils'
2
+
3
+ interface CodeSpanProps {
4
+ children: React.ReactNode
5
+ /** Visual variant */
6
+ variant?: 'default' | 'simple'
7
+ /** Allow copy on click */
8
+ allowCopy?: boolean
9
+ /** Additional CSS class */
10
+ className?: string
11
+ }
12
+
13
+ /**
14
+ * Styled code span component matching strawberry.rocks design.
15
+ * Uses coral/pink color for code badges.
16
+ */
17
+ export function CodeSpan({ children, variant = 'default', allowCopy = false, className }: CodeSpanProps) {
18
+ const handleCopy = () => {
19
+ if (allowCopy && typeof children === 'string') {
20
+ navigator.clipboard.writeText(children)
21
+ }
22
+ }
23
+
24
+ if (variant === 'simple') {
25
+ return (
26
+ <code
27
+ onClick={allowCopy ? handleCopy : undefined}
28
+ className={cn(
29
+ 'font-mono text-[0.9em] font-semibold text-gray-900 dark:text-white',
30
+ allowCopy && 'cursor-pointer hover:text-primary-600 dark:hover:text-primary-400',
31
+ className
32
+ )}
33
+ >
34
+ {children}
35
+ </code>
36
+ )
37
+ }
38
+
39
+ return (
40
+ <code
41
+ onClick={allowCopy ? handleCopy : undefined}
42
+ className={cn(
43
+ 'inline-flex items-center px-2 py-0.5 rounded font-mono text-sm',
44
+ 'bg-red-50 text-red-600 border border-red-200',
45
+ 'dark:bg-red-900/20 dark:text-red-400 dark:border-red-800/50',
46
+ allowCopy && 'cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/30',
47
+ className
48
+ )}
49
+ >
50
+ {children}
51
+ </code>
52
+ )
53
+ }
@@ -0,0 +1,269 @@
1
+ import type { GriffeDocstring, GriffeDocstringSection, GriffeDocstringElement, GriffeExpression } from '../../types'
2
+ import { Markdown } from '../Markdown'
3
+
4
+ /**
5
+ * Render a type annotation expression to string
6
+ */
7
+ function renderExpression(expr: GriffeExpression | string | undefined): string {
8
+ if (!expr) return ''
9
+ if (typeof expr === 'string') return expr
10
+ if (expr.str) return expr.str
11
+ if (expr.canonical) return expr.canonical
12
+
13
+ const exprAny = expr as any
14
+
15
+ // Handle ExprName with member reference
16
+ if (expr.name && typeof expr.name === 'string') return expr.name
17
+
18
+ // Handle ExprBoolOp (like `config or StrawberryConfig()`)
19
+ if (exprAny.cls === 'ExprBoolOp' && exprAny.operator && Array.isArray(exprAny.values)) {
20
+ return exprAny.values.map((v: any) => renderExpression(v)).join(` ${exprAny.operator} `)
21
+ }
22
+
23
+ // Handle ExprBinOp (like `type | None`)
24
+ if (exprAny.cls === 'ExprBinOp' && exprAny.left && exprAny.right) {
25
+ const left = renderExpression(exprAny.left)
26
+ const right = renderExpression(exprAny.right)
27
+ const op = exprAny.operator || '|'
28
+ return `${left} ${op} ${right}`
29
+ }
30
+
31
+ // Handle ExprCall (like `StrawberryConfig()`)
32
+ if (exprAny.cls === 'ExprCall' && exprAny.function) {
33
+ const funcName = renderExpression(exprAny.function)
34
+ const args = Array.isArray(exprAny.arguments)
35
+ ? exprAny.arguments.map((a: any) => renderExpression(a)).join(', ')
36
+ : ''
37
+ return `${funcName}(${args})`
38
+ }
39
+
40
+ // Handle ExprAttribute (like contextlib.asynccontextmanager)
41
+ if (exprAny.cls === 'ExprAttribute' && Array.isArray(exprAny.values)) {
42
+ return exprAny.values.map((v: any) => renderExpression(v)).join('.')
43
+ }
44
+
45
+ // Handle ExprList and ExprTuple
46
+ if ('elements' in exprAny && Array.isArray(exprAny.elements)) {
47
+ const inner = exprAny.elements.map((el: any) => renderExpression(el)).join(', ')
48
+ return exprAny.cls === 'ExprTuple' ? `(${inner})` : `[${inner}]`
49
+ }
50
+
51
+ // Handle ExprDict
52
+ if (exprAny.cls === 'ExprDict' && Array.isArray(exprAny.keys) && Array.isArray(exprAny.values)) {
53
+ const pairs = exprAny.keys.map((k: any, i: number) =>
54
+ `${renderExpression(k)}: ${renderExpression(exprAny.values[i])}`
55
+ ).join(', ')
56
+ return `{${pairs}}`
57
+ }
58
+
59
+ // Handle ExprSubscript (like Dict[str, int])
60
+ if (exprAny.left && exprAny.slice) {
61
+ const left = renderExpression(exprAny.left)
62
+ const slice = renderExpression(exprAny.slice)
63
+ return `${left}[${slice}]`
64
+ }
65
+
66
+ // Handle slice expressions
67
+ if ('slice' in exprAny && exprAny.slice && !exprAny.left) {
68
+ return renderExpression(exprAny.slice)
69
+ }
70
+
71
+ // Fallback for unknown expressions
72
+ if (typeof expr === 'object') {
73
+ return JSON.stringify(expr)
74
+ }
75
+
76
+ return String(expr)
77
+ }
78
+
79
+ interface DocstringSectionProps {
80
+ section: GriffeDocstringSection
81
+ }
82
+
83
+ function DocstringSection({ section }: DocstringSectionProps) {
84
+ switch (section.kind) {
85
+ case 'text':
86
+ return (
87
+ <div className="prose prose-sm dark:prose-invert max-w-none">
88
+ <Markdown content={section.value as string} />
89
+ </div>
90
+ )
91
+
92
+ case 'parameters':
93
+ return (
94
+ <div className="mt-4">
95
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Parameters</h4>
96
+ <dl className="space-y-2">
97
+ {(section.value as GriffeDocstringElement[])?.map((param) => (
98
+ <div key={param.name} className="grid grid-cols-[auto_1fr] gap-x-3">
99
+ <dt className="font-mono text-sm">
100
+ <span className="text-orange-600 dark:text-orange-400">{param.name}</span>
101
+ {param.annotation && (
102
+ <span className="text-gray-500 dark:text-gray-400">
103
+ {' '}({renderExpression(param.annotation)})
104
+ </span>
105
+ )}
106
+ </dt>
107
+ <dd className="text-sm text-gray-600 dark:text-gray-300">
108
+ {param.description}
109
+ </dd>
110
+ </div>
111
+ ))}
112
+ </dl>
113
+ </div>
114
+ )
115
+
116
+ case 'returns':
117
+ return (
118
+ <div className="mt-4">
119
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Returns</h4>
120
+ <div className="text-sm text-gray-600 dark:text-gray-300">
121
+ {Array.isArray(section.value) ? (
122
+ (section.value as GriffeDocstringElement[]).map((ret, i) => (
123
+ <div key={i}>
124
+ {ret.annotation && (
125
+ <span className="font-mono text-green-600 dark:text-green-400">
126
+ {renderExpression(ret.annotation)}
127
+ </span>
128
+ )}
129
+ {ret.description && <span> - {ret.description}</span>}
130
+ </div>
131
+ ))
132
+ ) : (
133
+ section.value
134
+ )}
135
+ </div>
136
+ </div>
137
+ )
138
+
139
+ case 'raises':
140
+ return (
141
+ <div className="mt-4">
142
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Raises</h4>
143
+ <dl className="space-y-2">
144
+ {(section.value as GriffeDocstringElement[])?.map((exc, i) => (
145
+ <div key={i} className="grid grid-cols-[auto_1fr] gap-x-3">
146
+ <dt className="font-mono text-sm text-red-600 dark:text-red-400">
147
+ {exc.annotation ? renderExpression(exc.annotation) : exc.name}
148
+ </dt>
149
+ <dd className="text-sm text-gray-600 dark:text-gray-300">
150
+ {exc.description}
151
+ </dd>
152
+ </div>
153
+ ))}
154
+ </dl>
155
+ </div>
156
+ )
157
+
158
+ case 'examples':
159
+ return (
160
+ <div className="mt-4">
161
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Examples</h4>
162
+ <pre className="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 overflow-x-auto text-sm">
163
+ <code>{section.value as string}</code>
164
+ </pre>
165
+ </div>
166
+ )
167
+
168
+ case 'attributes':
169
+ return (
170
+ <div className="mt-4">
171
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Attributes</h4>
172
+ <dl className="space-y-2">
173
+ {(section.value as GriffeDocstringElement[])?.map((attr) => (
174
+ <div key={attr.name} className="grid grid-cols-[auto_1fr] gap-x-3">
175
+ <dt className="font-mono text-sm">
176
+ <span className="text-orange-600 dark:text-orange-400">{attr.name}</span>
177
+ {attr.annotation && (
178
+ <span className="text-gray-500 dark:text-gray-400">
179
+ {' '}({renderExpression(attr.annotation)})
180
+ </span>
181
+ )}
182
+ </dt>
183
+ <dd className="text-sm text-gray-600 dark:text-gray-300">
184
+ {attr.description}
185
+ </dd>
186
+ </div>
187
+ ))}
188
+ </dl>
189
+ </div>
190
+ )
191
+
192
+ case 'deprecated':
193
+ return (
194
+ <div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
195
+ <h4 className="text-sm font-semibold text-yellow-800 dark:text-yellow-200 mb-1">Deprecated</h4>
196
+ <p className="text-sm text-yellow-700 dark:text-yellow-300">{section.value as string}</p>
197
+ </div>
198
+ )
199
+
200
+ case 'admonition':
201
+ return (
202
+ <div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
203
+ {section.title && (
204
+ <h4 className="text-sm font-semibold text-blue-800 dark:text-blue-200 mb-1">{section.title}</h4>
205
+ )}
206
+ <p className="text-sm text-blue-700 dark:text-blue-300">{section.value as string}</p>
207
+ </div>
208
+ )
209
+
210
+ default:
211
+ if (section.title) {
212
+ return (
213
+ <div className="mt-4">
214
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">{section.title}</h4>
215
+ <div className="text-sm text-gray-600 dark:text-gray-300">
216
+ {typeof section.value === 'string' ? section.value : JSON.stringify(section.value)}
217
+ </div>
218
+ </div>
219
+ )
220
+ }
221
+ return null
222
+ }
223
+ }
224
+
225
+ interface DocstringProps {
226
+ docstring: GriffeDocstring | undefined
227
+ /** Show raw text instead of parsed sections */
228
+ raw?: boolean
229
+ /** Only show the text/description part, skip parameters/returns/etc */
230
+ showOnlyText?: boolean
231
+ /** Additional CSS class */
232
+ className?: string
233
+ }
234
+
235
+ /**
236
+ * Renders a parsed docstring with sections for parameters, returns, raises, etc.
237
+ */
238
+ export function Docstring({ docstring, raw = false, showOnlyText = false, className = '' }: DocstringProps) {
239
+ if (!docstring) return null
240
+
241
+ // If raw mode or no parsed sections, show raw value
242
+ if (raw || !docstring.parsed || docstring.parsed.length === 0) {
243
+ return (
244
+ <div className={`prose prose-sm dark:prose-invert max-w-none ${className}`}>
245
+ <p className="whitespace-pre-wrap text-gray-600 dark:text-gray-300">{docstring.value}</p>
246
+ </div>
247
+ )
248
+ }
249
+
250
+ // If showOnlyText, only render the first text section (the description)
251
+ if (showOnlyText) {
252
+ const firstTextSection = docstring.parsed.find(s => s.kind === 'text')
253
+ if (!firstTextSection) return null
254
+
255
+ return (
256
+ <div className={`prose prose-sm dark:prose-invert max-w-none ${className}`}>
257
+ <Markdown content={firstTextSection.value as string} />
258
+ </div>
259
+ )
260
+ }
261
+
262
+ return (
263
+ <div className={className}>
264
+ {docstring.parsed.map((section, i) => (
265
+ <DocstringSection key={`${section.kind}-${i}`} section={section} />
266
+ ))}
267
+ </div>
268
+ )
269
+ }