@powerduck/tree 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +556 -0
- package/dist/adapters/index.cjs +1 -0
- package/dist/adapters/index.d.cts +161 -0
- package/dist/adapters/index.d.ts +161 -0
- package/dist/adapters/index.js +1 -0
- package/dist/chunk-5KF7QUTD.js +1 -0
- package/dist/chunk-NHPRTISE.js +0 -0
- package/dist/chunk-YSSQNJPQ.js +1 -0
- package/dist/core/index.cjs +1 -0
- package/dist/core/index.d.cts +107 -0
- package/dist/core/index.d.ts +107 -0
- package/dist/core/index.js +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/react/index.cjs +1 -0
- package/dist/react/index.css +592 -0
- package/dist/react/index.d.cts +213 -0
- package/dist/react/index.d.ts +213 -0
- package/dist/react/index.js +1 -0
- package/dist/types-DVz8c2Yd.d.cts +51 -0
- package/dist/types-DVz8c2Yd.d.ts +51 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 powerduck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# @powerduck/tree
|
|
2
|
+
|
|
3
|
+
Extensible tree component for API navigation, schema exploration, and documentation. Built on a generic core with adapters for OpenAPI, JSON Schema, and custom data sources.
|
|
4
|
+
|
|
5
|
+
[Website](https://www.powerduck.com) · [Documentation](https://www.powerduck.com/docs/tree) · [GitHub](https://github.com/powerducklab/tree) · [npm](https://www.npmjs.com/package/@powerduck/tree)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Generic core** — `TreeNode<TMetadata>` works with any data source
|
|
10
|
+
- **OpenAPI adapter** — tag-based navigation with OAS 3.2 native parent nesting, `x-tagGroups`, `x-order`, `x-displayName`, `x-internal`
|
|
11
|
+
- **JSON Schema adapter** — deep expansion of nested properties, arrays, combinators with `jsonPath` metadata for editor line jumping
|
|
12
|
+
- **Doc adapter** — Stripe-style documentation tree with operation count badges, parameters, request body, and responses
|
|
13
|
+
- **React component** — search, expand/collapse toggle (Material Design unfold icons), keyboard navigation, method badges, custom render props, `searchRowExtra` for inline toolbar extensions
|
|
14
|
+
- **Drag and drop** — reorder within the same parent **and cross-level move into folders**, with three-zone drop detection, `canDrag`/`canDrop` predicates, `dragGroupKey` isolation, `onReorder`/`onMove` callbacks, and circular reference prevention
|
|
15
|
+
- **Context menu** — right-click or the more (…) button for per-node actions, with custom item definitions, icons, separators, danger styling, and two-step confirm for destructive actions
|
|
16
|
+
- **Imperative locate** — `locateNode(predicate)` finds a node, expands all ancestors, selects it, and scrolls into view (ideal for "jump to API" features)
|
|
17
|
+
- **JSON Patch integration** — `onPatch` callback emits RFC 6902 operations for reorder/move/delete, compatible with [`@powerduck/conf-patch`](https://www.npmjs.com/package/@powerduck/conf-patch) for applying to the original document
|
|
18
|
+
- **High-quality icons** — powered by [react-icons](https://react-icons.github.io/react-icons/) (Lucide + Material Design icon sets)
|
|
19
|
+
- **CSS variable theming** — compatible with powerduck `tokens.css`, light/dark mode
|
|
20
|
+
- **Zero hard dependencies** — core and adapters have no runtime dependencies; React layer requires `react`, `react-dom`, and `react-icons`
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @powerduck/tree
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Peer Dependencies
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install react react-dom react-icons
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### CSS Import
|
|
35
|
+
|
|
36
|
+
Import the component styles in your app entry point:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import "@powerduck/tree/react/index.css";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### API Debug Tree
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { useMemo } from "react";
|
|
48
|
+
import { buildOpenApiTree } from "@powerduck/tree";
|
|
49
|
+
import { Tree } from "@powerduck/tree/react";
|
|
50
|
+
|
|
51
|
+
function ApiNav({ document }) {
|
|
52
|
+
const { root } = useMemo(() => buildOpenApiTree(document), [document]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div style={{ width: 320, height: "100%" }}>
|
|
56
|
+
<Tree
|
|
57
|
+
nodes={root.children ?? []}
|
|
58
|
+
onSelect={(node) => console.log(node.metadata?.method, node.metadata?.path)}
|
|
59
|
+
searchable
|
|
60
|
+
showExpandAll
|
|
61
|
+
defaultExpandDepth={2}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Schema Navigation Tree
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useMemo } from "react";
|
|
72
|
+
import { buildSchemaTree } from "@powerduck/tree";
|
|
73
|
+
import { Tree } from "@powerduck/tree/react";
|
|
74
|
+
|
|
75
|
+
function SchemaNav({ schema, editorRef }) {
|
|
76
|
+
const { root } = useMemo(() => buildSchemaTree(schema), [schema]);
|
|
77
|
+
|
|
78
|
+
const handleSelect = (node) => {
|
|
79
|
+
const jsonPath = node.metadata?.jsonPath;
|
|
80
|
+
if (jsonPath) {
|
|
81
|
+
editorRef.current?.revealLine(jsonPath);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Tree
|
|
87
|
+
nodes={root.children ?? []}
|
|
88
|
+
onSelect={handleSelect}
|
|
89
|
+
searchable
|
|
90
|
+
defaultExpandDepth={3}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Custom Data Source
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { Tree } from "@powerduck/tree/react";
|
|
100
|
+
import type { TreeNode } from "@powerduck/tree/core";
|
|
101
|
+
|
|
102
|
+
interface FileMeta {
|
|
103
|
+
type: "folder" | "file";
|
|
104
|
+
size?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const files: TreeNode<FileMeta>[] = [
|
|
108
|
+
{
|
|
109
|
+
id: "src",
|
|
110
|
+
name: "src",
|
|
111
|
+
metadata: { type: "folder" },
|
|
112
|
+
children: [
|
|
113
|
+
{ id: "src/index.ts", name: "index.ts", metadata: { type: "file", size: 512 } },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
function FileTree() {
|
|
119
|
+
return <Tree nodes={files} searchable defaultExpandDepth={1} />;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Drag and Drop
|
|
124
|
+
|
|
125
|
+
Enable drag and drop with the `draggable` prop. Nodes can be reordered within the same parent **or moved into a different parent** (cross-level) by dragging the grip handle.
|
|
126
|
+
|
|
127
|
+
### Reordering (same level)
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { useState } from "react";
|
|
131
|
+
import { Tree } from "@powerduck/tree/react";
|
|
132
|
+
import type { ReorderResult } from "@powerduck/tree/core";
|
|
133
|
+
|
|
134
|
+
function SortableTree({ initialNodes }) {
|
|
135
|
+
const [nodes, setNodes] = useState(initialNodes);
|
|
136
|
+
|
|
137
|
+
const handleReorder = (result: ReorderResult) => {
|
|
138
|
+
console.log(`Moved "${result.moved.name}" from index ${result.fromIndex} to ${result.toIndex}`);
|
|
139
|
+
setNodes(result.nodes);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Tree
|
|
144
|
+
nodes={nodes}
|
|
145
|
+
draggable
|
|
146
|
+
onReorder={handleReorder}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Cross-level move (drag into folder)
|
|
153
|
+
|
|
154
|
+
When dragging over a branch node, the middle 50% of the row highlights the node as a drop target (folder). Dropping there moves the node to become a child of that folder.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { useState } from "react";
|
|
158
|
+
import { Tree } from "@powerduck/tree/react";
|
|
159
|
+
import type { TreeNode } from "@powerduck/tree/core";
|
|
160
|
+
|
|
161
|
+
function MoveableTree({ initialNodes }) {
|
|
162
|
+
const [nodes, setNodes] = useState(initialNodes);
|
|
163
|
+
|
|
164
|
+
const handleMove = (newNodes: TreeNode[], movedNode, targetParentId) => {
|
|
165
|
+
console.log(`Moved "${movedNode.name}" into folder "${targetParentId}"`);
|
|
166
|
+
setNodes(newNodes);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Tree
|
|
171
|
+
nodes={nodes}
|
|
172
|
+
draggable
|
|
173
|
+
onReorder={(result) => setNodes(result.nodes)}
|
|
174
|
+
onMove={handleMove}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Drop zones on a branch node:
|
|
181
|
+
- **Top 25%** — insert before (reorder)
|
|
182
|
+
- **Middle 50%** — move into this folder (cross-level)
|
|
183
|
+
- **Bottom 25%** — insert after (reorder)
|
|
184
|
+
|
|
185
|
+
Circular references are automatically prevented: a node cannot be dropped into its own descendant.
|
|
186
|
+
|
|
187
|
+
### Control which nodes can be dragged
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
<Tree
|
|
191
|
+
nodes={nodes}
|
|
192
|
+
draggable
|
|
193
|
+
canDrag={(node) => node.metadata?.type !== "folder"}
|
|
194
|
+
onReorder={handleReorder}
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Control where nodes can be dropped
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<Tree
|
|
202
|
+
nodes={nodes}
|
|
203
|
+
draggable
|
|
204
|
+
canDrop={(dragged, target, position) => {
|
|
205
|
+
/* position is "before" | "after" | "child" */
|
|
206
|
+
return position !== "child" || target.metadata?.type === "folder";
|
|
207
|
+
}}
|
|
208
|
+
onReorder={handleReorder}
|
|
209
|
+
onMove={(nodes) => setNodes(nodes)}
|
|
210
|
+
/>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Imperative Handle
|
|
214
|
+
|
|
215
|
+
Access tree methods via `ref`:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { useRef } from "react";
|
|
219
|
+
import { Tree } from "@powerduck/tree/react";
|
|
220
|
+
import type { TreeHandle } from "@powerduck/tree/react";
|
|
221
|
+
|
|
222
|
+
function TreeWithLocate() {
|
|
223
|
+
const treeRef = useRef<TreeHandle>(null);
|
|
224
|
+
|
|
225
|
+
const locateByOperationId = (operationId: string) => {
|
|
226
|
+
const found = treeRef.current?.locateNode((node) =>
|
|
227
|
+
node.metadata?.operationId === operationId
|
|
228
|
+
);
|
|
229
|
+
/* found is the matched TreeNode, or undefined if not found */
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
<button onClick={() => locateByOperationId("getPet")}>Locate getPet</button>
|
|
235
|
+
<Tree ref={treeRef} nodes={nodes} />
|
|
236
|
+
</>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Handle methods
|
|
242
|
+
|
|
243
|
+
| Method | Description |
|
|
244
|
+
| --- | --- |
|
|
245
|
+
| `expandAll()` | Expand all nodes |
|
|
246
|
+
| `collapseAll()` | Collapse all nodes |
|
|
247
|
+
| `expandToDepth(depth)` | Expand nodes to a specific depth |
|
|
248
|
+
| `getExpandedIds()` | Get currently expanded node IDs |
|
|
249
|
+
| `getSelectedNode()` | Get the currently selected node |
|
|
250
|
+
| `scrollToNode(id)` | Scroll a node into view by ID |
|
|
251
|
+
| `locateNode(predicate)` | Find a node by predicate, expand ancestors, select, and scroll into view |
|
|
252
|
+
|
|
253
|
+
## Context Menu
|
|
254
|
+
|
|
255
|
+
Enable per-node actions via right-click or the more (…) button that appears on row hover. Define menu items with the `contextMenuItems` prop.
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { LuCopy, LuTrash2 } from "react-icons/lu";
|
|
259
|
+
import { Tree } from "@powerduck/tree/react";
|
|
260
|
+
import type { ContextMenuItem } from "@powerduck/tree/react";
|
|
261
|
+
|
|
262
|
+
function ApiTree({ nodes }) {
|
|
263
|
+
const buildMenu = (node): ContextMenuItem[] => [
|
|
264
|
+
{
|
|
265
|
+
label: "Copy operationId",
|
|
266
|
+
icon: <LuCopy size={14} />,
|
|
267
|
+
onClick: (n) => navigator.clipboard.writeText(n.metadata?.operationId ?? ""),
|
|
268
|
+
},
|
|
269
|
+
{ separator: true, label: "", onClick: () => undefined },
|
|
270
|
+
{
|
|
271
|
+
label: "Delete",
|
|
272
|
+
icon: <LuTrash2 size={14} />,
|
|
273
|
+
danger: true,
|
|
274
|
+
onClick: (n) => console.log("Delete", n.id),
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
return <Tree nodes={nodes} contextMenuItems={buildMenu} />;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### ContextMenuItem properties
|
|
283
|
+
|
|
284
|
+
| Property | Type | Description |
|
|
285
|
+
| --- | --- | --- |
|
|
286
|
+
| `label` | `string` | Display text |
|
|
287
|
+
| `icon` | `ReactNode` | Optional icon before label |
|
|
288
|
+
| `onClick` | `(node) => void` | Click handler |
|
|
289
|
+
| `disabled` | `boolean` | Disable the item |
|
|
290
|
+
| `danger` | `boolean` | Red text for destructive actions |
|
|
291
|
+
| `separator` | `boolean` | Render a horizontal line above this item |
|
|
292
|
+
|
|
293
|
+
## JSON Patch & conf-patch Integration
|
|
294
|
+
|
|
295
|
+
When nodes carry a `jsonPath` in their metadata, the tree emits RFC 6902 JSON Patch operations via the `onPatch` callback. Apply these to the original document with [`@powerduck/conf-patch`](https://www.npmjs.com/package/@powerduck/conf-patch).
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { patchContent } from "@powerduck/conf-patch/core";
|
|
299
|
+
import { Tree } from "@powerduck/tree/react";
|
|
300
|
+
import type { JsonPatchOp } from "@powerduck/tree/react";
|
|
301
|
+
|
|
302
|
+
function EditableTree({ initialDocument, format }) {
|
|
303
|
+
const [document, setDocument] = useState(initialDocument);
|
|
304
|
+
|
|
305
|
+
const handlePatch = (ops: JsonPatchOp[]) => {
|
|
306
|
+
/* Apply patch operations to the document string. */
|
|
307
|
+
const updated = patchContent(document, ops, format);
|
|
308
|
+
setDocument(updated);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<Tree
|
|
313
|
+
nodes={nodes}
|
|
314
|
+
draggable
|
|
315
|
+
onReorder={(result) => setNodes(result.nodes)}
|
|
316
|
+
onPatch={handlePatch}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Patch operations are currently emitted for **reorder** (array `move`). Nodes must have `metadata.jsonPath` as an array of path segments for patch generation.
|
|
323
|
+
|
|
324
|
+
## API Reference
|
|
325
|
+
|
|
326
|
+
### Core
|
|
327
|
+
|
|
328
|
+
#### `TreeNode<TMetadata>`
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
interface TreeNode<TMetadata = unknown> {
|
|
332
|
+
id: string;
|
|
333
|
+
name: string;
|
|
334
|
+
order?: number;
|
|
335
|
+
children?: TreeNode<TMetadata>[];
|
|
336
|
+
metadata?: TMetadata;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### Tree Utilities
|
|
341
|
+
|
|
342
|
+
| Function | Description |
|
|
343
|
+
|----------|-------------|
|
|
344
|
+
| `findNode(nodes, id)` | Find a node by ID |
|
|
345
|
+
| `findPath(nodes, id)` | Find full path from root to node |
|
|
346
|
+
| `getLeaves(nodes)` | Get all leaf nodes |
|
|
347
|
+
| `countNodes(nodes)` | Count all nodes |
|
|
348
|
+
| `getMaxDepth(nodes)` | Get maximum tree depth |
|
|
349
|
+
| `sortNodes(nodes)` | Sort by order, then name |
|
|
350
|
+
| `sortTree(nodes)` | Recursively sort all levels |
|
|
351
|
+
| `filterTree(nodes, { query })` | Filter by search query |
|
|
352
|
+
| `getExpandableIds(nodes, { maxDepth })` | Get IDs to expand |
|
|
353
|
+
| `getBranchIds(nodes)` | Get all branch node IDs |
|
|
354
|
+
| `flattenTree(nodes)` | Flatten to pre-order list with depth |
|
|
355
|
+
| `updateNode(nodes, id, updater)` | Immutable node update |
|
|
356
|
+
| `removeNode(nodes, id)` | Immutable node removal |
|
|
357
|
+
| `insertChild(nodes, parentId, child)` | Immutable child insertion |
|
|
358
|
+
|
|
359
|
+
### OpenAPI Adapter
|
|
360
|
+
|
|
361
|
+
#### `buildOpenApiTree(document, options?)`
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
interface OpenApiTreeOptions {
|
|
365
|
+
showInternal?: boolean; // default false
|
|
366
|
+
showComponents?: boolean; // default true
|
|
367
|
+
showWebhooks?: boolean; // default true
|
|
368
|
+
defaultExpandDepth?: number; // default 2
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Navigation strategy (priority order):**
|
|
373
|
+
1. OAS 3.2 native `parent`-nested tags
|
|
374
|
+
2. `x-tagGroups` (with powerduck nested `groups` extension)
|
|
375
|
+
3. Flat tag grouping (fallback)
|
|
376
|
+
|
|
377
|
+
**Supported extensions:**
|
|
378
|
+
- `x-order` — sort tags and operations
|
|
379
|
+
- `x-displayName` — human-friendly tag label
|
|
380
|
+
- `x-internal` / `x-scalar-ignore` — hide operations
|
|
381
|
+
- `x-tagGroups` — group tags into navigation folders
|
|
382
|
+
|
|
383
|
+
### JSON Schema Adapter
|
|
384
|
+
|
|
385
|
+
#### `buildSchemaTree(schema, options?)`
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
interface SchemaTreeOptions {
|
|
389
|
+
maxDepth?: number; // default 8
|
|
390
|
+
showDeprecated?: boolean; // default true
|
|
391
|
+
expandCombinators?: boolean; // default true
|
|
392
|
+
showArrayItems?: boolean; // default true
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### `findSchemaNodeByPath(root, jsonPath)`
|
|
397
|
+
|
|
398
|
+
Find a schema tree node by its JSON path.
|
|
399
|
+
|
|
400
|
+
#### `formatSchemaType(metadata)`
|
|
401
|
+
|
|
402
|
+
Format a schema type label for display (e.g. `"string (email)"`, `"string | null"`).
|
|
403
|
+
|
|
404
|
+
### Doc Adapter
|
|
405
|
+
|
|
406
|
+
#### `buildDocTree(document, options?)`
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
interface DocTreeOptions {
|
|
410
|
+
showInternal?: boolean; // default false
|
|
411
|
+
showComponents?: boolean; // default true
|
|
412
|
+
showWebhooks?: boolean; // default true
|
|
413
|
+
expandOperationDetails?: boolean; // default true
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Builds a Stripe-style documentation tree where each operation expands to show parameters, request body, and responses.
|
|
418
|
+
|
|
419
|
+
### React Component
|
|
420
|
+
|
|
421
|
+
#### `<Tree />` Props
|
|
422
|
+
|
|
423
|
+
| Prop | Type | Default | Description |
|
|
424
|
+
|------|------|---------|-------------|
|
|
425
|
+
| `nodes` | `TreeNode[]` | required | Root nodes to render |
|
|
426
|
+
| `onSelect` | `(node) => void` | — | Called when a leaf node is clicked |
|
|
427
|
+
| `onExpandedChange` | `(ids) => void` | — | Called when expanded nodes change |
|
|
428
|
+
| `defaultExpandedIds` | `string[]` | — | Initially expanded node IDs |
|
|
429
|
+
| `defaultExpandDepth` | `number` | `1` | Default expansion depth |
|
|
430
|
+
| `searchable` | `boolean` | `false` | Show search input |
|
|
431
|
+
| `searchPlaceholder` | `string` | `"Search..."` | Search input placeholder |
|
|
432
|
+
| `searchRowExtra` | `ReactNode` | — | Extra content inside the search row (between input and action buttons) |
|
|
433
|
+
| `showExpandAll` | `boolean` | `false` | Show expand/collapse all toggle button (single toggle, Material Design unfold icons) |
|
|
434
|
+
| `showRefresh` | `boolean` | `false` | Show refresh button |
|
|
435
|
+
| `onRefresh` | `() => void` | — | Called when refresh button is clicked |
|
|
436
|
+
| `toolbar` | `ReactNode` | — | Custom toolbar content rendered above the tree |
|
|
437
|
+
| `renderNode` | `(ctx, defaultNode) => ReactNode` | — | Fully custom node renderer |
|
|
438
|
+
| `renderIcon` | `(ctx) => ReactNode` | — | Custom icon renderer |
|
|
439
|
+
| `renderLabel` | `(ctx) => ReactNode` | — | Custom label renderer |
|
|
440
|
+
| `renderSuffix` | `(ctx) => ReactNode` | — | Custom suffix (badges, count, etc.) |
|
|
441
|
+
| `className` | `string` | — | Additional CSS class |
|
|
442
|
+
| `style` | `CSSProperties` | — | Inline styles |
|
|
443
|
+
| `size` | `"xs" \| "sm" \| "md"` | `"sm"` | Size variant |
|
|
444
|
+
| `showIndentGuides` | `boolean` | `true` | Show indent guide lines |
|
|
445
|
+
| `maxHeight` | `number \| string` | — | Max height before scrolling |
|
|
446
|
+
| `draggable` | `boolean` | `false` | Enable drag and drop reordering |
|
|
447
|
+
| `dragGroupKey` | `string` | — | Metadata key for drag isolation (nodes with different group values cannot intermix) |
|
|
448
|
+
| `onReorder` | `(result: ReorderResult) => void` | — | Called when a node is reordered within the same parent |
|
|
449
|
+
| `onMove` | `(nodes, movedNode, targetParentId) => void` | — | Called when a node is moved to a different parent |
|
|
450
|
+
| `canDrag` | `(node) => boolean` | — | Predicate to control which nodes can be dragged |
|
|
451
|
+
| `canDrop` | `(dragged, target, position) => boolean` | — | Predicate to control allowed drop targets |
|
|
452
|
+
| `contextMenuItems` | `(node) => ContextMenuItem[]` | — | Right-click / more-button context menu items |
|
|
453
|
+
| `onPatch` | `(ops, context) => void` | — | Called with RFC 6902 JSON Patch ops on reorder (for conf-patch integration) |
|
|
454
|
+
|
|
455
|
+
#### Tree Handle (via ref)
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
interface TreeHandle {
|
|
459
|
+
expandAll: () => void;
|
|
460
|
+
collapseAll: () => void;
|
|
461
|
+
expandToDepth: (depth: number) => void;
|
|
462
|
+
getExpandedIds: () => string[];
|
|
463
|
+
getSelectedNode: () => TreeNode | undefined;
|
|
464
|
+
scrollToNode: (id: string) => void;
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Architecture
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
@powerduck/tree
|
|
472
|
+
├── core/ Generic tree types and utilities (zero dependencies)
|
|
473
|
+
├── adapters/ Data source adapters
|
|
474
|
+
│ ├── openapi.ts OpenAPI document → navigation tree
|
|
475
|
+
│ ├── json-schema.ts JSON Schema → deep exploration tree
|
|
476
|
+
│ └── doc.ts OpenAPI → Stripe-style documentation tree
|
|
477
|
+
├── react/ React Tree component
|
|
478
|
+
│ ├── Tree.tsx Main component
|
|
479
|
+
│ ├── Tree.module.css Styles (CSS variables)
|
|
480
|
+
│ ├── hooks/ useTreeSearch, useTreeExpansion
|
|
481
|
+
│ └── libs/ Types and utilities
|
|
482
|
+
└── index.ts Main entry (core + adapters)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Entry Points
|
|
486
|
+
|
|
487
|
+
| Import | Description |
|
|
488
|
+
|--------|-------------|
|
|
489
|
+
| `@powerduck/tree` | Core types, utilities, and all adapters |
|
|
490
|
+
| `@powerduck/tree/react` | React Tree component and hooks |
|
|
491
|
+
| `@powerduck/tree/adapters` | OpenAPI, JSON Schema, and Doc adapters |
|
|
492
|
+
| `@powerduck/tree/core` | Core types and utilities only |
|
|
493
|
+
|
|
494
|
+
## Theming
|
|
495
|
+
|
|
496
|
+
The component uses CSS variables that align with powerduck's `tokens.css`. Override them in your application:
|
|
497
|
+
|
|
498
|
+
```css
|
|
499
|
+
:root {
|
|
500
|
+
--color-surface: #ffffff;
|
|
501
|
+
--color-text-primary: #26282b;
|
|
502
|
+
--color-accent: #f28c28;
|
|
503
|
+
/* ... see tokens.css for full list */
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
:root[data-theme="dark"] {
|
|
507
|
+
--color-surface: #1f2125;
|
|
508
|
+
--color-text-primary: #edf0f3;
|
|
509
|
+
/* ... */
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## OAS 3.2 Nested Tags
|
|
514
|
+
|
|
515
|
+
OAS 3.2 supports native tag nesting via the `parent` field:
|
|
516
|
+
|
|
517
|
+
```yaml
|
|
518
|
+
tags:
|
|
519
|
+
- name: Account
|
|
520
|
+
- name: Users
|
|
521
|
+
parent: Account
|
|
522
|
+
- name: Admin
|
|
523
|
+
parent: Account
|
|
524
|
+
- name: Roles
|
|
525
|
+
parent: Admin
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
The adapter automatically detects and resolves nested tags, with circular reference detection and unknown parent warnings.
|
|
529
|
+
|
|
530
|
+
## x-tagGroups with Nested Folders
|
|
531
|
+
|
|
532
|
+
The powerduck extension supports nested groups via the `groups` field:
|
|
533
|
+
|
|
534
|
+
```yaml
|
|
535
|
+
x-tagGroups:
|
|
536
|
+
- name: Account
|
|
537
|
+
tags: [Users]
|
|
538
|
+
groups:
|
|
539
|
+
- name: Admin
|
|
540
|
+
tags: [Roles, Permissions]
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Redoc and Scalar safely ignore the unknown `groups` field.
|
|
544
|
+
|
|
545
|
+
## Development
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
npm install
|
|
549
|
+
npm run build # Build with tsup
|
|
550
|
+
npm run test # Run tests with vitest
|
|
551
|
+
npm run typecheck # Type check with tsc
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## License
|
|
555
|
+
|
|
556
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,r=Object.prototype.hasOwnProperty,a={};function s(e){return[...e].sort((e,t)=>{const n=e.order??Number.MAX_SAFE_INTEGER,o=t.order??Number.MAX_SAFE_INTEGER;if(n!==o)return n-o;return e.name.localeCompare(t.name,"en-US",{numeric:!0,sensitivity:"base"})||e.id.localeCompare(t.id)})}((e,n)=>{for(var o in n)t(e,o,{get:n[o],enumerable:!0})})(a,{buildDocTree:()=>z,buildOpenApiTree:()=>y,buildSchemaTree:()=>v,findSchemaNodeByPath:()=>N,formatSchemaType:()=>C}),module.exports=(e=a,((e,a,s,i)=>{if(a&&"object"==typeof a||"function"==typeof a)for(let c of o(a))r.call(e,c)||c===s||t(e,c,{get:()=>a[c],enumerable:!(i=n(a,c))||i.enumerable});return e})(t({},"__esModule",{value:!0}),e));var i=["get","put","post","delete","options","head","patch","trace"],c={showInternal:!1,showComponents:!0,showWebhooks:!0,defaultExpandDepth:2};function d(e){return"object"!=typeof e||null===e||Array.isArray(e)?null:e}function p(e){return"string"==typeof e&&e.trim().length>0?e.trim():void 0}function u(e){return"boolean"==typeof e?e:void 0}function m(e){return Array.isArray(e)?e:void 0}function f(e,t){return!t&&(!0===u(e["x-scalar-ignore"])||!0===u(e["x-internal"]))}function h(e){return"number"==typeof(t=e["x-order"])&&Number.isFinite(t)?t:void 0;var t}function l(e){return p(e["x-displayName"])}function g(e,t,n,o){const r=new Map;for(const t of e)r.set(t.name,t);const a=new Map;for(const t of e){if(!t.parent){a.set(t.name,null);continue}if(!r.has(t.parent)){o.push(`Tag "${t.name}" references unknown parent "${t.parent}", treating as root.`),a.set(t.name,null);continue}const e=[t.name];let n=t.parent,s=!1;for(;n;){if(e.includes(n)){s=!0,o.push(`Circular tag reference detected: ${e.join(" -> ")} -> ${n}. Breaking cycle.`);break}e.push(n),n=r.get(n)?.parent}a.set(t.name,s?null:t.parent)}const i=new Map;for(const e of t){if(f(e.raw,n.showInternal))continue;const t=e.tags.length>0?e.tags:["Other"];for(const n of t){const t=i.get(n)??[];t.push(e),i.set(n,t)}}const c=new Map;for(const t of e){const e=a.get(t.name)??null??"__root__",n=c.get(e)??[];n.push(t),c.set(e,n)}const d=e=>s((c.get(e)??[]).map(e=>{const t=(e=>{const t=(i.get(e.name)??[]).map(t=>({id:`op:${e.name}:${t.id}`,name:t.name,order:h(t.raw),metadata:{method:t.method,path:t.path,operationId:t.operationId,deprecated:t.deprecated,summary:t.summary,pointer:t.pointer,source:"oas32-parent",kind:"operation"}}));return{id:`tag:${e.name}`,name:e.displayName,order:e.order,children:s(t),metadata:{source:"oas32-parent",kind:"tag"}}})(e),n=d(e.name);return n.length>0&&(t.children=s([...t.children??[],...n])),t})),p=d("__root__"),u=i.get("Other")??[];if(u.length>0){const e=u.map(e=>({id:`op:Other:${e.id}`,name:e.name,order:h(e.raw),metadata:{method:e.method,path:e.path,operationId:e.operationId,deprecated:e.deprecated,summary:e.summary,pointer:e.pointer,source:"fallback",kind:"operation"}}));p.push({id:"tag:Other",name:"Other",order:Number.MAX_SAFE_INTEGER,children:s(e),metadata:{source:"fallback",kind:"tag"}})}return s(p)}function y(e,t={}){const n={...c,...t},o=[],r=d(e);if(!r)return{root:{id:"root",name:"OpenAPI",children:[],metadata:{kind:"root"}},warnings:["Document is null or not an object."]};const a=function(e){const t=d(e.paths);if(!t)return[];const n=[];for(const[e,o]of Object.entries(t)){const t=d(o);if(t)for(const o of i){const r=t[o];if(null==r)continue;const a=d(r);if(!a)continue;const s=p(a.operationId),i=p(a.summary),c=m(a.tags)?.filter(e=>"string"==typeof e).map(e=>e.trim()).filter(e=>e.length>0)??[],f=i??s??`${o.toUpperCase()} ${e}`,h=s??`${o}:${e}`,l=`/paths/${encodeURIComponent(e)}/${o}`;n.push({id:h,path:e,method:o,name:f,summary:i,operationId:s,deprecated:!0===u(a.deprecated),tags:c,pointer:l,raw:a})}}return n}(r),y=function(e){const t=m(e.tags);if(!t)return[];const n=[];for(const e of t){const t=d(e),o=p(t?.name);o&&t&&n.push({name:o,displayName:l(t)??o,description:p(t.description),parent:p(t.parent),order:h(t),raw:t})}return n}(r);let $;if(function(e){return e.some(e=>void 0!==e.parent)}(y))$=g(y,a,n,o);else{const e=function(e){const t=m(e["x-tagGroups"]);if(!t)return[];const n=(e,t)=>{const o=d(e),r=p(o?.name),a=m(o?.tags)?.filter(e=>"string"==typeof e).map(e=>e.trim()).filter(e=>e.length>0)??[];if(!r)return null;const s=m(o?.groups)?.map((e,t)=>n(e,t)).filter(e=>null!==e)??[];return{name:r,tags:a,groups:s.length>0?s:void 0,order:h(o??{})??t}};return t.map((e,t)=>n(e,t)).filter(e=>null!==e)}(r);$=e.length>0?function(e,t,n,o){const r=new Map;for(const e of t)r.set(e.name,e.displayName);const a=new Map;for(const e of n)if(!f(e.raw,o.showInternal))for(const t of e.tags){const n=a.get(t)??[];n.push(e),a.set(t,n)}const i=e=>(a.get(e)??[]).map(t=>({id:`op:${e}:${t.id}`,name:t.name,order:h(t.raw),metadata:{method:t.method,path:t.path,operationId:t.operationId,deprecated:t.deprecated,summary:t.summary,pointer:t.pointer,source:"tag-group",kind:"operation"}})),c=e=>({id:`tag:${e}`,name:r.get(e)??e,children:s(i(e)),metadata:{source:"tag-group",kind:"tag"}}),d=e=>{const t=[];for(const n of e.tags){const e=a.get(n);e?.length&&t.push(c(n))}for(const n of e.groups??[])t.push(d(n));return{id:`tag-group:${e.name}`,name:e.name,order:e.order,children:s(t),metadata:{source:"tag-group",kind:"tag-group"}}};return s(e.map(d))}(e,y,a,n):function(e,t,n){const o=new Map,r=new Map;for(let t=0;t<e.length;t+=1){const n=e[t];r.set(n.name,n.order??t)}for(const e of t){if(f(e.raw,n.showInternal))continue;const t=e.tags.length>0?e.tags:["Other"];for(const n of t){const t=o.get(n)??[];t.push(e),o.set(n,t)}}const a=new Map;for(const t of e)a.set(t.name,t.displayName);const i=[];for(const[e,t]of o){const n=t.map(t=>({id:`op:${e}:${t.id}`,name:t.name,order:h(t.raw),metadata:{method:t.method,path:t.path,operationId:t.operationId,deprecated:t.deprecated,summary:t.summary,pointer:t.pointer,source:"tag",kind:"operation"}}));i.push({id:`tag:${e}`,name:a.get(e)??e,order:"Other"===e?Number.MAX_SAFE_INTEGER:r.get(e),children:s(n),metadata:{source:"tag",kind:"tag"}})}return s(i)}(y,a,n)}const k=function(e,t){if(!t.showWebhooks)return;const n=d(e.webhooks);if(!n)return;const o=[];for(const[e,r]of Object.entries(n)){const n=d(r);if(n)for(const r of i){const a=d(n[r]);if(!a||f(a,t.showInternal))continue;const s=p(a.operationId),i=p(a.summary),c=i??s??`${r.toUpperCase()} ${e}`;o.push({id:`webhook:${e}:${r}`,name:c,order:h(a),metadata:{method:r,path:e,operationId:s,deprecated:!0===u(a.deprecated),summary:i,pointer:`/webhooks/${encodeURIComponent(e)}/${r}`,source:"tag",kind:"webhook"}})}}return 0!==o.length?{id:"section:webhooks",name:"Webhooks",order:Number.MAX_SAFE_INTEGER-2,children:s(o),metadata:{source:"section",kind:"section",section:"webhooks"}}:void 0}(r,n),w=function(e,t){if(!t.showComponents)return;const n=d(e.components),o=d(n?.schemas);if(!o)return;const r=[];for(const[e,n]of Object.entries(o)){const o=d(n);o&&!f(o,t.showInternal)&&r.push({id:`schema:${e}`,name:e,order:h(o),metadata:{pointer:`/components/schemas/${encodeURIComponent(e)}`,source:"tag",kind:"schema"}})}return 0!==r.length?{id:"section:components",name:"Components",order:Number.MAX_SAFE_INTEGER-1,children:s(r),metadata:{source:"section",kind:"section",section:"components"}}:void 0}(r,n);return{root:{id:"root",name:"OpenAPI",children:s([{id:"section:apis",name:"APIs",children:$,metadata:{kind:"section",source:"section",section:"apis"},order:0},...k?[k]:[],...w?[w]:[]]),metadata:{kind:"root"}},warnings:o}}var $={openapi:0,info:1,servers:2,paths:3,webhooks:4,components:5,security:6,tags:7,externalDocs:8,jsonSchemaDialect:9},k={maxDepth:8,showDeprecated:!0,expandCombinators:!0,showArrayItems:!0},w=new Set(["string","number","integer","boolean","null"]);function b(e){return"object"!=typeof e||null===e||Array.isArray(e)?null:e}function I(e){return"string"==typeof e&&e.trim().length>0?e.trim():void 0}function A(e){return"boolean"==typeof e?e:void 0}function O(e){return Array.isArray(e)?e:void 0}function j(e){return e?Array.isArray(e)?e.join(" | "):e:"any"}function _(e,t,n,o,r,a){const i=function(e){const t=e.type;return"string"==typeof t||Array.isArray(t)&&t.every(e=>"string"==typeof e)?t:void 0}(t),c=I(t.format),d=I(t.description),p=!0===A(t.deprecated),u=function(e){if(!e)return"unknown";switch(Array.isArray(e)?e[0]:e){case"object":return"object";case"array":return"array";case"string":return"string";case"number":return"number";case"integer":return"integer";case"boolean":return"boolean";case"null":return"null";default:return"unknown"}}(i),m={jsonPath:n,type:i,format:c,required:a,deprecated:p,description:d,kind:u},f=I(t.$ref);if(f){if(r.refStack.has(f))return r.warnings.push(`Circular $ref detected at ${n.join(".")}: ${f}. Breaking recursion.`),{id:n.join(".")||"root",name:e,metadata:{...m,kind:"ref",isLeaf:!0}};r.refStack.add(f)}let h,l=function(e){return!!e&&(Array.isArray(e)?e.every(e=>w.has(e)):w.has(e))}(i);if(o>=r.options.maxDepth)return f&&r.refStack.delete(f),{id:n.join(".")||"root",name:e,metadata:{...m,isLeaf:!0}};const g=b(t.properties);if(g){const e=O(t.required)??[],a=new Set(e.filter(e=>"string"==typeof e)),i=[];for(const[e,t]of Object.entries(g)){const s=b(t);s&&((r.options.showDeprecated||!0!==A(s.deprecated))&&i.push(_(e,s,[...n,"properties",e],o+1,r,a.has(e))))}h=[...h??[],...s(i)],l=!1}if(r.options.showArrayItems&&"array"===i){const e=b(t.items);if(e){h=[...h??[],_("items",e,[...n,"items"],o+1,r,!1)],l=!1}else m.hasItems=!1}if(r.options.expandCombinators){const e=[{key:"allOf",kind:"allOf"},{key:"anyOf",kind:"anyOf"},{key:"oneOf",kind:"oneOf"}];for(const{key:a,kind:i}of e){const e=O(t[a]);if(!e?.length)continue;const c=e.map((e,t)=>{const s=b(e);return s?_(`[${t}]`,s,[...n,a,String(t)],o+1,r,!1):{id:`${n.join(".")}.${a}[${t}]`,name:`[${t}]`,metadata:{jsonPath:[...n,a,String(t)],kind:"unknown",isLeaf:!0}}});h=[...h??[],{id:`${n.join(".")}.${a}`,name:a,children:s(c),metadata:{jsonPath:[...n,a],kind:i}}],l=!1}}const y=O(t.enum);return y?.length&&!h?.length&&(m.kind="enum",l=!0),f&&r.refStack.delete(f),{id:n.join(".")||"root",name:e,order:$[e],metadata:{...m,isLeaf:l},children:h}}function v(e,t={}){const n={...k,...t},o=[],r=b(e);if(!r)return{root:{id:"root",name:"Schema",metadata:{jsonPath:[],kind:"root",isLeaf:!0}},warnings:["Schema is null or not an object."]};return{root:_("root",r,[],0,{options:n,warnings:o,refStack:new Set},!0),warnings:o}}function N(e,t){if(0===t.length)return e;const n=t.join("."),o=[e];for(;o.length>0;){const e=o.shift();if(!e)continue;const t=e.metadata?.jsonPath?.join(".");if(t===n)return e;for(const t of e.children??[])o.push(t)}}function C(e){return"enum"===e.kind?"enum":"ref"===e.kind?"$ref":e.format?`${j(e.type)} (${e.format})`:j(e.type)}var E=["get","put","post","delete","options","head","patch","trace"],M={showInternal:!1,showComponents:!1,showWebhooks:!0,expandOperationDetails:!0};function S(e){return"object"!=typeof e||null===e||Array.isArray(e)?null:e}function R(e){return"string"==typeof e&&e.trim().length>0?e.trim():void 0}function x(e){return"boolean"==typeof e?e:void 0}function T(e){return Array.isArray(e)?e:void 0}function U(e,t){return!t&&(!0===x(e["x-scalar-ignore"])||!0===x(e["x-internal"]))}function P(e){const t=e["x-order"];return"number"==typeof t&&Number.isFinite(t)?t:void 0}function D(e){return R(e["x-displayName"])}function G(e,t){const n=[],o=T(e.parameters);if(o?.length){const e=function(e,t){const n=[];for(const o of e){const e=S(o);if(!e)continue;const r=R(e.name),a=R(e.in);if(!r||!a)continue;const s=`${a}:${r}`;n.push({id:`${t}/parameters/${encodeURIComponent(s)}`,name:r,metadata:{paramIn:a,required:!0===x(e.required),description:R(e.description),pointer:`${t}/parameters`,kind:"parameter"}})}return s(n)}(o,t);e.length>0&&n.push({id:`${t}/parameters`,name:"Parameters",children:e,metadata:{pointer:`${t}/parameters`,kind:"parameters"}})}const r=S(e.requestBody);if(r){const e=S(r.content),o=[];if(e)for(const[n]of Object.entries(e))o.push({id:`${t}/requestBody/content/${encodeURIComponent(n)}`,name:n,metadata:{contentType:n,pointer:`${t}/requestBody/content/${encodeURIComponent(n)}`,kind:"content"}});n.push({id:`${t}/requestBody`,name:"Request Body",metadata:{description:R(r.description),required:!0===x(r.required),pointer:`${t}/requestBody`,kind:"request-body"},children:o.length>0?o:void 0})}const a=S(e.responses);if(a){const e=function(e,t){const n=[];for(const[o,r]of Object.entries(e)){const e=S(r);if(!e)continue;const a=R(e.description),s=S(e.content),i=[];if(s)for(const[e]of Object.entries(s))i.push({id:`${t}/responses/${encodeURIComponent(o)}/content/${encodeURIComponent(e)}`,name:e,metadata:{contentType:e,pointer:`${t}/responses/${encodeURIComponent(o)}/content/${encodeURIComponent(e)}`,kind:"content"}});n.push({id:`${t}/responses/${encodeURIComponent(o)}`,name:o,metadata:{statusCode:o,description:a,pointer:`${t}/responses/${encodeURIComponent(o)}`,kind:"response"},children:i.length>0?i:void 0})}return s(n)}(a,t);e.length>0&&n.push({id:`${t}/responses`,name:"Responses",children:e,metadata:{pointer:`${t}/responses`,kind:"responses"}})}return n}function q(e,t,n,o){const r=t.expandOperationDetails?G(e.raw,e.pointer):void 0;return{id:`op:${o}:${e.id}`,name:e.name,order:P(e.raw),metadata:{method:e.method,path:e.path,operationId:e.operationId,deprecated:e.deprecated,summary:e.summary,description:e.description,pointer:e.pointer,source:n,kind:"operation"},children:r&&r.length>0?r:void 0}}function F(e,t,n,o){const r=new Map;for(const t of e)r.set(t.name,t);const a=new Map;for(const t of e){if(!t.parent){a.set(t.name,null);continue}if(!r.has(t.parent)){o.push(`Tag "${t.name}" references unknown parent "${t.parent}", treating as root.`),a.set(t.name,null);continue}const e=[t.name];let n=t.parent,s=!1;for(;n;){if(e.includes(n)){s=!0,o.push(`Circular tag reference detected: ${e.join(" -> ")} -> ${n}. Breaking cycle.`);break}e.push(n),n=r.get(n)?.parent}a.set(t.name,s?null:t.parent)}const i=new Map;for(const e of t){if(U(e.raw,n.showInternal))continue;const t=e.tags.length>0?e.tags:["Other"];for(const n of t){const t=i.get(n)??[];t.push(e),i.set(n,t)}}const c=new Map;for(const t of e){const e=a.get(t.name)??null??"__root__",n=c.get(e)??[];n.push(t),c.set(e,n)}const d=e=>s((c.get(e)??[]).map(e=>{const t=(e=>{const t=(i.get(e.name)??[]).map(t=>q(t,n,"oas32-parent",e.name));return{id:`tag:${e.name}`,name:e.displayName,order:e.order,children:s(t),metadata:{description:e.description,source:"oas32-parent",kind:"tag"}}})(e),o=d(e.name);return o.length>0&&(t.children=s([...t.children??[],...o])),t})),p=d("__root__"),u=i.get("Other")??[];if(u.length>0){const e=u.map(e=>q(e,n,"fallback","Other"));p.push({id:"tag:Other",name:"Other",order:Number.MAX_SAFE_INTEGER,children:s(e),metadata:{source:"fallback",kind:"tag"}})}return s(p)}function B(e){if(!Array.isArray(e))return[];const t=[];for(const n of e){const e=S(n);if(!e||"string"!=typeof e.name)continue;const o={name:e.name};Array.isArray(e.tags)&&(o.tags=e.tags.filter(e=>"string"==typeof e)),Array.isArray(e.groups)&&(o.groups=B(e.groups)),t.push(o)}return t}function X(e,t,n,o,r,a=0){const i=[];if(e.tags)for(const a of e.tags){const e=t.get(a);if(!e||0===e.length)continue;const c=e.map(e=>q(e,r,"tag-group",a));i.push({id:`tag:${a}`,name:n.get(a)??a,order:o.get(a),children:s(c),metadata:{source:"tag-group",kind:"tag"}})}if(e.groups)for(const s of e.groups)i.push(X(s,t,n,o,r,a+1));return{id:`tag-group:${e.name}`,name:e.name,order:1e3*a,children:s(i),metadata:{source:"tag-group",kind:"tag-group"}}}function L(e,t,n,o){const r=new Map,a=new Map,i=new Map;for(let e=0;e<t.length;e+=1)a.set(t[e].name,e),i.set(t[e].name,t[e].displayName);for(const e of n){if(U(e.raw,o.showInternal))continue;const t=e.tags.length>0?e.tags:["Other"];for(const n of t){const t=r.get(n)??[];t.push(e),r.set(n,t)}}const c=B(e["x-tagGroups"]).map(e=>X(e,r,i,a,o)),d=r.get("Other");if(d&&d.length>0){const e=d.map(e=>q(e,o,"fallback","Other"));c.push({id:"tag:Other",name:"Other",order:Number.MAX_SAFE_INTEGER,children:s(e),metadata:{source:"fallback",kind:"tag"}})}return s(c)}function W(e){return e.map(e=>{const t=e.children?W(e.children):void 0,n="operation"===e.metadata?.kind?1:0,o=t?.reduce((e,t)=>e+(t.metadata?.operationCount??0),0)??0;return{...e,children:t,metadata:{...e.metadata,operationCount:n+o}}})}function z(e,t={}){const n={...M,...t},o=[],r=S(e);if(!r)return{root:{id:"root",name:"API Documentation",children:[],metadata:{kind:"root"}},warnings:["Document is null or not an object."]};const a=function(e){const t=S(e.paths);if(!t)return[];const n=[];for(const[e,o]of Object.entries(t)){const t=S(o);if(t)for(const o of E){const r=S(t[o]);if(!r)continue;const a=R(r.operationId),s=R(r.summary),i=R(r.description),c=T(r.tags)?.filter(e=>"string"==typeof e).map(e=>e.trim()).filter(e=>e.length>0)??[],d=s??a??`${o.toUpperCase()} ${e}`,p=a??`${o}:${e}`,u=`/paths/${encodeURIComponent(e)}/${o}`;n.push({id:p,path:e,method:o,name:d,summary:s,description:i,operationId:a,deprecated:!0===x(r.deprecated),tags:c,pointer:u,raw:r})}}return n}(r),i=function(e){const t=T(e.tags);if(!t)return[];const n=[];for(const e of t){const t=S(e),o=R(t?.name);o&&t&&n.push({name:o,displayName:D(t)??o,description:R(t.description),parent:R(t.parent),order:P(t)})}return n}(r);let c;c=function(e){return e.some(e=>void 0!==e.parent)}(i)?F(i,a,n,o):function(e){return Array.isArray(e["x-tagGroups"])&&e["x-tagGroups"].length>0}(r)?L(r,i,a,n):function(e,t,n){const o=new Map,r=new Map;for(let t=0;t<e.length;t+=1)r.set(e[t].name,t);for(const e of t){if(U(e.raw,n.showInternal))continue;const t=e.tags.length>0?e.tags:["Other"];for(const n of t){const t=o.get(n)??[];t.push(e),o.set(n,t)}}const a=new Map;for(const t of e)a.set(t.name,t.displayName);const i=[];for(const[e,t]of o){const o=t.map(t=>q(t,n,"tag",e));i.push({id:`tag:${e}`,name:a.get(e)??e,order:"Other"===e?Number.MAX_SAFE_INTEGER:r.get(e),children:s(o),metadata:{source:"tag",kind:"tag"}})}return s(i)}(i,a,n);const d=function(e,t){if(!t.showComponents)return;const n=S(e.components),o=S(n?.schemas);if(!o)return;const r=[];for(const[e,n]of Object.entries(o)){const o=S(n);o&&!U(o,t.showInternal)&&r.push({id:`schema:${e}`,name:e,order:P(o),metadata:{description:R(o.description),pointer:`/components/schemas/${encodeURIComponent(e)}`,source:"tag",kind:"schema"}})}return 0!==r.length?{id:"section:components",name:"Components",order:Number.MAX_SAFE_INTEGER-1,children:s(r),metadata:{source:"tag",kind:"component"}}:void 0}(r,n);return{root:{id:"root",name:"API Documentation",children:s(W([...c,...d?[d]:[]])),metadata:{kind:"root"}},warnings:o}}
|