@erenthedeveloper0/zen-openapi 0.1.0-alpha.1
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 +9 -0
- package/README.md +76 -0
- package/dist/diff.d.ts +37 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +320 -0
- package/dist/diff.js.map +1 -0
- package/dist/document.d.ts +75 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +689 -0
- package/dist/document.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +29 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +84 -0
- package/dist/plugin.js.map +1 -0
- package/dist/schema.d.ts +103 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +434 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +19 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +192 -0
- package/dist/ui.js.map +1 -0
- package/package.json +65 -0
- package/src/diff.ts +439 -0
- package/src/document.ts +882 -0
- package/src/index.ts +20 -0
- package/src/plugin.ts +135 -0
- package/src/schema.ts +497 -0
- package/src/types.ts +166 -0
- package/src/ui.ts +198 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { JsonSchema } from '@erenthedeveloper0/zen-core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAPI 3.1 document types — rfcs/0001 §29.
|
|
5
|
+
*
|
|
6
|
+
* 3.1 rather than 3.0 for one reason that matters here: 3.1's Schema Object *is*
|
|
7
|
+
* JSON Schema 2020-12, the same dialect the validation layer produces and the
|
|
8
|
+
* serializer IR consumes. Under 3.0 every schema would need lossy
|
|
9
|
+
* down-conversion (`nullable`, `exclusiveMinimum`, tuples, `const`) — which is
|
|
10
|
+
* precisely the silent drift this subsystem exists to prevent.
|
|
11
|
+
*
|
|
12
|
+
* Only the parts Zen emits are typed. A partial-but-honest type is more useful
|
|
13
|
+
* than a complete one nobody reads, and `[extension: string]` keeps `x-` fields
|
|
14
|
+
* legal without pretending to enumerate them.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** OAS 3.1 Schema Objects are JSON Schema 2020-12, so this is not an alias of convenience. */
|
|
18
|
+
export type OpenApiSchema = JsonSchema
|
|
19
|
+
|
|
20
|
+
export interface OpenApiDocument {
|
|
21
|
+
readonly openapi: string
|
|
22
|
+
readonly info: InfoObject
|
|
23
|
+
readonly servers?: readonly ServerObject[] | undefined
|
|
24
|
+
readonly paths: Readonly<Record<string, PathItemObject>>
|
|
25
|
+
readonly components?: ComponentsObject | undefined
|
|
26
|
+
readonly tags?: readonly TagObject[] | undefined
|
|
27
|
+
readonly security?: readonly SecurityRequirement[] | undefined
|
|
28
|
+
readonly externalDocs?: ExternalDocs | undefined
|
|
29
|
+
readonly [extension: string]: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface InfoObject {
|
|
33
|
+
readonly title: string
|
|
34
|
+
readonly version: string
|
|
35
|
+
readonly summary?: string | undefined
|
|
36
|
+
readonly description?: string | undefined
|
|
37
|
+
readonly license?: { readonly name: string; readonly identifier?: string; readonly url?: string } | undefined
|
|
38
|
+
readonly contact?: { readonly name?: string; readonly url?: string; readonly email?: string } | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ServerObject {
|
|
42
|
+
readonly url: string
|
|
43
|
+
readonly description?: string | undefined
|
|
44
|
+
readonly variables?: Readonly<Record<string, { readonly default: string; readonly enum?: readonly string[]; readonly description?: string }>> | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TagObject {
|
|
48
|
+
readonly name: string
|
|
49
|
+
readonly description?: string | undefined
|
|
50
|
+
readonly externalDocs?: ExternalDocs | undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ExternalDocs {
|
|
54
|
+
readonly url: string
|
|
55
|
+
readonly description?: string | undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type HttpOperation = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace'
|
|
59
|
+
|
|
60
|
+
export type PathItemObject = {
|
|
61
|
+
readonly [M in HttpOperation]?: OperationObject
|
|
62
|
+
} & {
|
|
63
|
+
readonly summary?: string | undefined
|
|
64
|
+
readonly description?: string | undefined
|
|
65
|
+
readonly parameters?: readonly ParameterObject[] | undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface OperationObject {
|
|
69
|
+
readonly operationId: string
|
|
70
|
+
readonly summary?: string | undefined
|
|
71
|
+
readonly description?: string | undefined
|
|
72
|
+
readonly tags?: readonly string[] | undefined
|
|
73
|
+
readonly deprecated?: boolean | undefined
|
|
74
|
+
readonly parameters?: readonly ParameterObject[] | undefined
|
|
75
|
+
readonly requestBody?: RequestBodyObject | undefined
|
|
76
|
+
readonly responses: Readonly<Record<string, ResponseObject>>
|
|
77
|
+
readonly security?: readonly SecurityRequirement[] | undefined
|
|
78
|
+
readonly externalDocs?: ExternalDocs | undefined
|
|
79
|
+
readonly [extension: string]: unknown
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type ParameterLocation = 'path' | 'query' | 'header' | 'cookie'
|
|
83
|
+
|
|
84
|
+
export interface ParameterObject {
|
|
85
|
+
readonly name: string
|
|
86
|
+
readonly in: ParameterLocation
|
|
87
|
+
readonly required?: boolean | undefined
|
|
88
|
+
readonly description?: string | undefined
|
|
89
|
+
readonly deprecated?: boolean | undefined
|
|
90
|
+
readonly schema: OpenApiSchema
|
|
91
|
+
readonly [extension: string]: unknown
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RequestBodyObject {
|
|
95
|
+
readonly required?: boolean | undefined
|
|
96
|
+
readonly description?: string | undefined
|
|
97
|
+
readonly content: Readonly<Record<string, MediaTypeObject>>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ResponseObject {
|
|
101
|
+
readonly description: string
|
|
102
|
+
readonly content?: Readonly<Record<string, MediaTypeObject>> | undefined
|
|
103
|
+
readonly headers?: Readonly<Record<string, HeaderObject>> | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface HeaderObject {
|
|
107
|
+
readonly description?: string | undefined
|
|
108
|
+
readonly required?: boolean | undefined
|
|
109
|
+
readonly schema: OpenApiSchema
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface MediaTypeObject {
|
|
113
|
+
readonly schema: OpenApiSchema
|
|
114
|
+
readonly example?: unknown
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ComponentsObject {
|
|
118
|
+
readonly schemas?: Readonly<Record<string, OpenApiSchema>> | undefined
|
|
119
|
+
readonly securitySchemes?: Readonly<Record<string, SecurityScheme>> | undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type SecurityRequirement = Readonly<Record<string, readonly string[]>>
|
|
123
|
+
|
|
124
|
+
export interface SecurityScheme {
|
|
125
|
+
readonly type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect' | 'mutualTLS'
|
|
126
|
+
readonly description?: string | undefined
|
|
127
|
+
readonly name?: string | undefined
|
|
128
|
+
readonly in?: 'query' | 'header' | 'cookie' | undefined
|
|
129
|
+
readonly scheme?: string | undefined
|
|
130
|
+
readonly bearerFormat?: string | undefined
|
|
131
|
+
readonly openIdConnectUrl?: string | undefined
|
|
132
|
+
readonly flows?: Readonly<Record<string, unknown>> | undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Route metadata this generator reads — rfcs/0001 §5.1.
|
|
139
|
+
*
|
|
140
|
+
* `meta` is a free-form map on purpose, but a generator that reads undocumented
|
|
141
|
+
* keys is a generator nobody can predict. These are the keys, and they are the
|
|
142
|
+
* whole list:
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* app.get('/users/:id<int>', {
|
|
146
|
+
* name: 'users.show',
|
|
147
|
+
* meta: { summary: 'Fetch one user', tags: ['users'], deprecated: true },
|
|
148
|
+
* response: { 200: PublicUser },
|
|
149
|
+
* }, handler)
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export interface RouteDocMeta {
|
|
153
|
+
readonly summary?: string
|
|
154
|
+
readonly description?: string
|
|
155
|
+
readonly tags?: readonly string[]
|
|
156
|
+
readonly operationId?: string
|
|
157
|
+
readonly deprecated?: boolean
|
|
158
|
+
/** Excluded from the document entirely. The docs endpoints set this on themselves. */
|
|
159
|
+
readonly hidden?: boolean
|
|
160
|
+
readonly security?: readonly SecurityRequirement[]
|
|
161
|
+
readonly externalDocs?: ExternalDocs
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const DOC_META_KEYS = [
|
|
165
|
+
'summary', 'description', 'tags', 'operationId', 'deprecated', 'hidden', 'security', 'externalDocs',
|
|
166
|
+
] as const satisfies readonly (keyof RouteDocMeta)[]
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { OpenApiDocument } from './types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A self-contained API reference — rfcs/0001 §29.2.
|
|
5
|
+
*
|
|
6
|
+
* Scalar, Swagger UI and Redoc are all better-looking than this, and all three
|
|
7
|
+
* are normally wired in with a `<script src="https://cdn…">`. That is a fine
|
|
8
|
+
* default for a public docs site and a bad one for a framework: it makes an
|
|
9
|
+
* internal endpoint phone a third party on every page load, it breaks behind a
|
|
10
|
+
* strict CSP, it breaks in an air-gapped deployment, and it breaks on a laptop
|
|
11
|
+
* on a train. So the built-in viewer has **no external requests at all** — the
|
|
12
|
+
* document is inlined at boot and the page is HTML, CSS and ~90 lines of script.
|
|
13
|
+
*
|
|
14
|
+
* `ui: false` turns it off; serving Scalar or Redoc instead is a five-line route
|
|
15
|
+
* in application code, and the README shows it.
|
|
16
|
+
*/
|
|
17
|
+
export function renderReference(document: OpenApiDocument, options: { jsonPath: string | null } = { jsonPath: null }): string {
|
|
18
|
+
const title = escapeHtml(document.info.title)
|
|
19
|
+
// `</script` inside a JSON island would end the block early. Escaping `<` is
|
|
20
|
+
// the standard, complete fix — `<` is the same character to JSON.parse.
|
|
21
|
+
const inlined = JSON.stringify(document).replace(/</g, '\\u003c')
|
|
22
|
+
const jsonLink = options.jsonPath === null
|
|
23
|
+
? ''
|
|
24
|
+
: `<a class="json-link" href="${escapeHtml(options.jsonPath)}">openapi.json</a>`
|
|
25
|
+
|
|
26
|
+
return `<!doctype html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="utf-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
31
|
+
<meta name="robots" content="noindex">
|
|
32
|
+
<title>${title} — API reference</title>
|
|
33
|
+
<style>${STYLE}</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<header>
|
|
37
|
+
<h1>${title}</h1>
|
|
38
|
+
<p class="version">v${escapeHtml(document.info.version)}${jsonLink}</p>
|
|
39
|
+
</header>
|
|
40
|
+
<main id="root"></main>
|
|
41
|
+
<script type="application/json" id="spec">${inlined}</script>
|
|
42
|
+
<script>${SCRIPT}</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function escapeHtml(raw: string): string {
|
|
49
|
+
return raw.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const HTML_ESCAPES: Readonly<Record<string, string>> = {
|
|
53
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const STYLE = `
|
|
57
|
+
:root{--bg:#fff;--fg:#1a1a1a;--muted:#666;--line:#e3e3e3;--code:#f6f6f7;--accent:#3060d0}
|
|
58
|
+
@media(prefers-color-scheme:dark){:root{--bg:#16171a;--fg:#e8e8ea;--muted:#9a9aa2;--line:#2c2d33;--code:#1e1f24;--accent:#7aa2f7}}
|
|
59
|
+
*{box-sizing:border-box}
|
|
60
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
|
|
61
|
+
header{padding:28px 32px 18px;border-bottom:1px solid var(--line)}
|
|
62
|
+
h1{margin:0;font-size:22px;font-weight:650}
|
|
63
|
+
.version{margin:6px 0 0;color:var(--muted);font-size:13px}
|
|
64
|
+
.json-link{margin-left:12px;color:var(--accent);text-decoration:none}
|
|
65
|
+
.json-link:hover{text-decoration:underline}
|
|
66
|
+
main{padding:24px 32px 64px;max-width:1000px}
|
|
67
|
+
h2{margin:32px 0 4px;font-size:15px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
68
|
+
.tag-desc{margin:0 0 10px;color:var(--muted);font-size:13.5px}
|
|
69
|
+
details{border:1px solid var(--line);border-radius:8px;margin:8px 0;background:var(--bg)}
|
|
70
|
+
details[open]{background:var(--code)}
|
|
71
|
+
summary{cursor:pointer;padding:11px 14px;display:flex;align-items:center;gap:10px;list-style:none;font-size:14px}
|
|
72
|
+
summary::-webkit-details-marker{display:none}
|
|
73
|
+
.method{font:600 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.05em;padding:5px 7px;border-radius:4px;color:#fff;min-width:56px;text-align:center}
|
|
74
|
+
.get{background:#2b7a4b}.post{background:#2f5fb3}.put{background:#a86100}.patch{background:#7a4bb3}.delete{background:#b33a3a}.head,.options{background:#5a5a66}
|
|
75
|
+
.path{font:13.5px ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
76
|
+
.summary{color:var(--muted);font-size:13px;margin-left:auto;text-align:right}
|
|
77
|
+
.dep{text-decoration:line-through;opacity:.65}
|
|
78
|
+
.body{padding:2px 14px 16px;border-top:1px solid var(--line)}
|
|
79
|
+
h3{margin:16px 0 6px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
80
|
+
table{border-collapse:collapse;width:100%;font-size:13.5px}
|
|
81
|
+
td,th{border-bottom:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}
|
|
82
|
+
th{color:var(--muted);font-weight:500;font-size:12px}
|
|
83
|
+
code{font:12.5px ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--bg);border:1px solid var(--line);border-radius:4px;padding:1px 5px}
|
|
84
|
+
pre{background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:10px 12px;overflow:auto;font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;margin:6px 0 0}
|
|
85
|
+
.req{color:#b33a3a;font-size:11px;margin-left:4px}
|
|
86
|
+
.status{font:600 12px ui-monospace,SFMono-Regular,Menlo,monospace;margin-right:8px}
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
const SCRIPT = `
|
|
90
|
+
const spec = JSON.parse(document.getElementById('spec').textContent)
|
|
91
|
+
const root = document.getElementById('root')
|
|
92
|
+
const METHODS = ['get','post','put','patch','delete','head','options']
|
|
93
|
+
|
|
94
|
+
function resolve(schema, depth) {
|
|
95
|
+
if (!schema || depth > 6) return schema || {}
|
|
96
|
+
if (schema.$ref) {
|
|
97
|
+
const name = schema.$ref.split('/').pop()
|
|
98
|
+
const target = (spec.components && spec.components.schemas || {})[name]
|
|
99
|
+
return target ? Object.assign({ 'x-name': name }, resolve(target, depth + 1)) : {}
|
|
100
|
+
}
|
|
101
|
+
return schema
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function typeOf(schema) {
|
|
105
|
+
const s = resolve(schema, 0)
|
|
106
|
+
if (s['x-name']) return s['x-name']
|
|
107
|
+
if (Array.isArray(s.type)) return s.type.join(' | ')
|
|
108
|
+
if (s.type === 'array') return typeOf(s.items || {}) + '[]'
|
|
109
|
+
if (s.enum) return s.enum.map(v => JSON.stringify(v)).join(' | ')
|
|
110
|
+
if (s.const !== undefined) return JSON.stringify(s.const)
|
|
111
|
+
return s.type || 'any'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function el(tag, cls, text) {
|
|
115
|
+
const node = document.createElement(tag)
|
|
116
|
+
if (cls) node.className = cls
|
|
117
|
+
if (text !== undefined) node.textContent = text
|
|
118
|
+
return node
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function paramTable(params) {
|
|
122
|
+
const table = el('table')
|
|
123
|
+
table.innerHTML = '<tr><th>Name</th><th>In</th><th>Type</th><th>Description</th></tr>'
|
|
124
|
+
for (const p of params) {
|
|
125
|
+
const row = table.insertRow()
|
|
126
|
+
const name = row.insertCell()
|
|
127
|
+
name.appendChild(el('code', null, p.name))
|
|
128
|
+
if (p.required) name.appendChild(el('span', 'req', 'required'))
|
|
129
|
+
row.insertCell().textContent = p.in
|
|
130
|
+
row.insertCell().appendChild(el('code', null, typeOf(p.schema)))
|
|
131
|
+
row.insertCell().textContent = p.description || ''
|
|
132
|
+
}
|
|
133
|
+
return table
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function schemaBlock(schema) {
|
|
137
|
+
return el('pre', null, JSON.stringify(schema, null, 2))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function operationNode(path, method, op) {
|
|
141
|
+
const details = el('details')
|
|
142
|
+
const summary = el('summary')
|
|
143
|
+
summary.appendChild(el('span', 'method ' + method, method.toUpperCase()))
|
|
144
|
+
summary.appendChild(el('span', 'path' + (op.deprecated ? ' dep' : ''), path))
|
|
145
|
+
if (op.summary) summary.appendChild(el('span', 'summary', op.summary))
|
|
146
|
+
details.appendChild(summary)
|
|
147
|
+
|
|
148
|
+
const body = el('div', 'body')
|
|
149
|
+
if (op.description) body.appendChild(el('p', 'tag-desc', op.description))
|
|
150
|
+
body.appendChild(el('h3', null, 'operationId'))
|
|
151
|
+
body.appendChild(el('code', null, op.operationId))
|
|
152
|
+
|
|
153
|
+
if (op.parameters && op.parameters.length) {
|
|
154
|
+
body.appendChild(el('h3', null, 'Parameters'))
|
|
155
|
+
body.appendChild(paramTable(op.parameters))
|
|
156
|
+
}
|
|
157
|
+
if (op.requestBody) {
|
|
158
|
+
body.appendChild(el('h3', null, 'Request body'))
|
|
159
|
+
for (const [media, content] of Object.entries(op.requestBody.content || {})) {
|
|
160
|
+
body.appendChild(el('code', null, media))
|
|
161
|
+
body.appendChild(schemaBlock(content.schema))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
body.appendChild(el('h3', null, 'Responses'))
|
|
165
|
+
for (const [status, response] of Object.entries(op.responses || {})) {
|
|
166
|
+
const line = el('div')
|
|
167
|
+
line.appendChild(el('span', 'status', status))
|
|
168
|
+
line.appendChild(el('span', null, response.description || ''))
|
|
169
|
+
body.appendChild(line)
|
|
170
|
+
for (const content of Object.values(response.content || {})) {
|
|
171
|
+
body.appendChild(schemaBlock(content.schema))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
details.appendChild(body)
|
|
175
|
+
return details
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const groups = new Map()
|
|
179
|
+
for (const [path, item] of Object.entries(spec.paths || {})) {
|
|
180
|
+
for (const method of METHODS) {
|
|
181
|
+
const op = item[method]
|
|
182
|
+
if (!op) continue
|
|
183
|
+
const tag = (op.tags && op.tags[0]) || 'default'
|
|
184
|
+
if (!groups.has(tag)) groups.set(tag, [])
|
|
185
|
+
groups.get(tag).push([path, method, op])
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const described = new Map((spec.tags || []).map(t => [t.name, t.description]))
|
|
190
|
+
for (const tag of [...groups.keys()].sort()) {
|
|
191
|
+
root.appendChild(el('h2', null, tag))
|
|
192
|
+
const description = described.get(tag)
|
|
193
|
+
if (description) root.appendChild(el('p', 'tag-desc', description))
|
|
194
|
+
for (const [path, method, op] of groups.get(tag)) {
|
|
195
|
+
root.appendChild(operationNode(path, method, op))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
`
|