docula 1.1.0 → 1.2.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.
- package/README.md +10 -1
- package/dist/docula.d.ts +27 -0
- package/dist/docula.js +92 -3
- package/package.json +1 -1
- package/templates/modern/api.hbs +6 -2
- package/templates/modern/css/api.css +30 -0
- package/templates/modern/css/styles.css +32 -5
- package/templates/modern/home.hbs +1 -1
- package/templates/modern/includes/footer.hbs +11 -3
- package/templates/modern/includes/header-bar.hbs +4 -2
- package/templates/modern/includes/header.hbs +2 -1
- package/templates/modern/includes/scripts.hbs +26 -7
- package/templates/modern/js/api.js +61 -12
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* For more complex projects easily add a `docula.config.ts` (TypeScript) or `docula.config.mjs` (JavaScript) file to customize the build process with lifecycle hooks.
|
|
37
37
|
* Full TypeScript support with typed configuration and IDE autocompletion.
|
|
38
38
|
* Support for single page with readme or multiple markdown pages in a docs folder.
|
|
39
|
-
* Will generate a sitemap.xml
|
|
39
|
+
* Will generate a sitemap.xml, robots.txt, and `feed.xml` for your site.
|
|
40
40
|
* Automatically generates `llms.txt` and `llms-full.txt` for LLM-friendly indexing of docs, API reference, and changelog content.
|
|
41
41
|
* Uses Github release notes to generate a changelog / releases page.
|
|
42
42
|
* Uses Github to show contributors and link to their profiles.
|
|
@@ -558,6 +558,15 @@ When changelog entries are found, Docula generates:
|
|
|
558
558
|
|
|
559
559
|
Changelog URLs are also automatically added to the generated `sitemap.xml`.
|
|
560
560
|
|
|
561
|
+
## RSS Feed
|
|
562
|
+
|
|
563
|
+
Docula automatically generates a docs-only RSS 2.0 feed at `/feed.xml` when your site has documentation pages.
|
|
564
|
+
|
|
565
|
+
- The feed includes one item per generated doc page.
|
|
566
|
+
- Each item uses the document title and canonical page URL.
|
|
567
|
+
- Item descriptions prefer front matter descriptions and otherwise fall back to a short excerpt from the markdown body.
|
|
568
|
+
- `/feed.xml` is added to `sitemap.xml` for discovery.
|
|
569
|
+
|
|
561
570
|
## Styling
|
|
562
571
|
|
|
563
572
|
Tags receive a CSS class based on their value (e.g., a tag of `"Bug Fix"` gets the class `changelog-tag-bug-fix`). You can style tags and other changelog elements by overriding these classes in your `variables.css`:
|
package/dist/docula.d.ts
CHANGED
|
@@ -52,6 +52,27 @@ type ApiGroup = {
|
|
|
52
52
|
id: string;
|
|
53
53
|
operations: ApiOperation[];
|
|
54
54
|
};
|
|
55
|
+
type ApiOAuth2Flow = {
|
|
56
|
+
authorizationUrl?: string;
|
|
57
|
+
tokenUrl?: string;
|
|
58
|
+
refreshUrl?: string;
|
|
59
|
+
scopes: Record<string, string>;
|
|
60
|
+
};
|
|
61
|
+
type ApiSecurityScheme = {
|
|
62
|
+
key: string;
|
|
63
|
+
type: string;
|
|
64
|
+
scheme?: string;
|
|
65
|
+
bearerFormat?: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
in?: string;
|
|
68
|
+
description: string;
|
|
69
|
+
flows?: {
|
|
70
|
+
authorizationCode?: ApiOAuth2Flow;
|
|
71
|
+
implicit?: ApiOAuth2Flow;
|
|
72
|
+
clientCredentials?: ApiOAuth2Flow;
|
|
73
|
+
password?: ApiOAuth2Flow;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
55
76
|
type ApiSpecData = {
|
|
56
77
|
info: {
|
|
57
78
|
title: string;
|
|
@@ -63,6 +84,7 @@ type ApiSpecData = {
|
|
|
63
84
|
description: string;
|
|
64
85
|
}>;
|
|
65
86
|
groups: ApiGroup[];
|
|
87
|
+
securitySchemes: ApiSecurityScheme[];
|
|
66
88
|
};
|
|
67
89
|
|
|
68
90
|
type GithubData = {
|
|
@@ -232,6 +254,8 @@ type DoculaData = {
|
|
|
232
254
|
url: string;
|
|
233
255
|
icon?: string;
|
|
234
256
|
}>;
|
|
257
|
+
enableLlmsTxt?: boolean;
|
|
258
|
+
hasFeed?: boolean;
|
|
235
259
|
};
|
|
236
260
|
type DoculaTemplates = {
|
|
237
261
|
home: string;
|
|
@@ -273,11 +297,14 @@ declare class DoculaBuilder {
|
|
|
273
297
|
getTemplateFile(path: string, name: string): Promise<string | undefined>;
|
|
274
298
|
buildRobotsPage(options: DoculaOptions): Promise<void>;
|
|
275
299
|
buildSiteMapPage(data: DoculaData): Promise<void>;
|
|
300
|
+
buildFeedPage(data: DoculaData): Promise<void>;
|
|
276
301
|
buildLlmsFiles(data: DoculaData): Promise<void>;
|
|
277
302
|
private generateLlmsIndexContent;
|
|
278
303
|
private generateLlmsFullContent;
|
|
279
304
|
private buildAbsoluteSiteUrl;
|
|
280
305
|
private normalizePathForUrl;
|
|
306
|
+
private escapeXml;
|
|
307
|
+
private summarizeMarkdown;
|
|
281
308
|
private isRemoteUrl;
|
|
282
309
|
private resolveOpenApiSpecUrl;
|
|
283
310
|
private resolveLocalOpenApiPath;
|
package/dist/docula.js
CHANGED
|
@@ -100,7 +100,43 @@ function parseOpenApiSpec(specJson) {
|
|
|
100
100
|
operations
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
const securitySchemes = [];
|
|
104
|
+
const schemesObj = spec.components?.securitySchemes;
|
|
105
|
+
if (schemesObj && typeof schemesObj === "object") {
|
|
106
|
+
for (const [key, value] of Object.entries(schemesObj)) {
|
|
107
|
+
const scheme = value;
|
|
108
|
+
const entry = {
|
|
109
|
+
key,
|
|
110
|
+
type: scheme.type ?? "",
|
|
111
|
+
scheme: scheme.scheme,
|
|
112
|
+
bearerFormat: scheme.bearerFormat,
|
|
113
|
+
name: scheme.name,
|
|
114
|
+
in: scheme.in,
|
|
115
|
+
description: scheme.description ?? ""
|
|
116
|
+
};
|
|
117
|
+
if (scheme.type === "oauth2" && scheme.flows) {
|
|
118
|
+
entry.flows = {};
|
|
119
|
+
for (const flowType of [
|
|
120
|
+
"authorizationCode",
|
|
121
|
+
"implicit",
|
|
122
|
+
"clientCredentials",
|
|
123
|
+
"password"
|
|
124
|
+
]) {
|
|
125
|
+
const flow = scheme.flows[flowType];
|
|
126
|
+
if (flow) {
|
|
127
|
+
entry.flows[flowType] = {
|
|
128
|
+
authorizationUrl: flow.authorizationUrl,
|
|
129
|
+
tokenUrl: flow.tokenUrl,
|
|
130
|
+
refreshUrl: flow.refreshUrl,
|
|
131
|
+
scopes: flow.scopes ?? {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
securitySchemes.push(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { info, servers, groups, securitySchemes };
|
|
104
140
|
}
|
|
105
141
|
function getStatusClass(statusCode) {
|
|
106
142
|
if (statusCode.startsWith("2")) {
|
|
@@ -200,7 +236,7 @@ function getSchemaType(schema) {
|
|
|
200
236
|
if (schema.type === "array") {
|
|
201
237
|
const items = schema.items;
|
|
202
238
|
const itemType = items?.type ?? "any";
|
|
203
|
-
return `array
|
|
239
|
+
return `array(${itemType})`;
|
|
204
240
|
}
|
|
205
241
|
if (schema.type) {
|
|
206
242
|
if (schema.format) {
|
|
@@ -1108,7 +1144,8 @@ var DoculaBuilder = class {
|
|
|
1108
1144
|
homePage: this.options.homePage,
|
|
1109
1145
|
themeMode: this.options.themeMode,
|
|
1110
1146
|
cookieAuth: this.options.cookieAuth,
|
|
1111
|
-
headerLinks: this.options.headerLinks
|
|
1147
|
+
headerLinks: this.options.headerLinks,
|
|
1148
|
+
enableLlmsTxt: this.options.enableLlmsTxt
|
|
1112
1149
|
};
|
|
1113
1150
|
if (!doculaData.openApiUrl && fs3.existsSync(`${doculaData.sitePath}/api/swagger.json`)) {
|
|
1114
1151
|
doculaData.openApiUrl = "/api/swagger.json";
|
|
@@ -1125,6 +1162,7 @@ var DoculaBuilder = class {
|
|
|
1125
1162
|
this.options
|
|
1126
1163
|
);
|
|
1127
1164
|
doculaData.hasDocuments = doculaData.documents?.length > 0;
|
|
1165
|
+
doculaData.hasFeed = doculaData.hasDocuments;
|
|
1128
1166
|
const changelogPath = `${doculaData.sitePath}/changelog`;
|
|
1129
1167
|
const fileChangelogEntries = this.getChangelogEntries(changelogPath);
|
|
1130
1168
|
const hasChangelogTemplate = await this.getTemplateFile(resolvedTemplatePath, "changelog") !== void 0;
|
|
@@ -1176,6 +1214,10 @@ var DoculaBuilder = class {
|
|
|
1176
1214
|
this._console.fileBuilt("sitemap.xml");
|
|
1177
1215
|
await this.buildRobotsPage(this.options);
|
|
1178
1216
|
this._console.fileBuilt("robots.txt");
|
|
1217
|
+
if (doculaData.hasDocuments) {
|
|
1218
|
+
await this.buildFeedPage(doculaData);
|
|
1219
|
+
this._console.fileBuilt("feed.xml");
|
|
1220
|
+
}
|
|
1179
1221
|
if (doculaData.hasDocuments) {
|
|
1180
1222
|
this._console.step("Building documentation pages...");
|
|
1181
1223
|
await this.buildDocsPages(doculaData);
|
|
@@ -1339,6 +1381,9 @@ var DoculaBuilder = class {
|
|
|
1339
1381
|
async buildSiteMapPage(data) {
|
|
1340
1382
|
const sitemapPath = `${data.output}/sitemap.xml`;
|
|
1341
1383
|
const urls = [{ url: data.siteUrl }];
|
|
1384
|
+
if (data.documents?.length) {
|
|
1385
|
+
urls.push({ url: `${data.siteUrl}/feed.xml` });
|
|
1386
|
+
}
|
|
1342
1387
|
if (data.openApiUrl && data.templates?.api) {
|
|
1343
1388
|
urls.push({ url: `${data.siteUrl}/api` });
|
|
1344
1389
|
}
|
|
@@ -1368,6 +1413,40 @@ var DoculaBuilder = class {
|
|
|
1368
1413
|
await fs3.promises.mkdir(data.output, { recursive: true });
|
|
1369
1414
|
await fs3.promises.writeFile(sitemapPath, xml, "utf8");
|
|
1370
1415
|
}
|
|
1416
|
+
async buildFeedPage(data) {
|
|
1417
|
+
if (!data.documents?.length) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const feedPath = `${data.output}/feed.xml`;
|
|
1421
|
+
const channelLink = this.buildAbsoluteSiteUrl(data.siteUrl, "/");
|
|
1422
|
+
const feedUrl = this.buildAbsoluteSiteUrl(data.siteUrl, "/feed.xml");
|
|
1423
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
1424
|
+
xml += '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">';
|
|
1425
|
+
xml += "<channel>";
|
|
1426
|
+
xml += `<title>${this.escapeXml(data.siteTitle)}</title>`;
|
|
1427
|
+
xml += `<link>${this.escapeXml(channelLink)}</link>`;
|
|
1428
|
+
xml += `<description>${this.escapeXml(data.siteDescription)}</description>`;
|
|
1429
|
+
xml += `<lastBuildDate>${this.escapeXml((/* @__PURE__ */ new Date()).toUTCString())}</lastBuildDate>`;
|
|
1430
|
+
xml += `<atom:link href="${this.escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />`;
|
|
1431
|
+
for (const document of data.documents) {
|
|
1432
|
+
const itemTitle = document.navTitle || document.title || document.urlPath;
|
|
1433
|
+
const itemLink = this.buildAbsoluteSiteUrl(
|
|
1434
|
+
data.siteUrl,
|
|
1435
|
+
this.normalizePathForUrl(document.urlPath)
|
|
1436
|
+
);
|
|
1437
|
+
const summary = document.description || this.summarizeMarkdown(new Writr(document.content).body);
|
|
1438
|
+
xml += "<item>";
|
|
1439
|
+
xml += `<title>${this.escapeXml(itemTitle)}</title>`;
|
|
1440
|
+
xml += `<link>${this.escapeXml(itemLink)}</link>`;
|
|
1441
|
+
xml += `<guid isPermaLink="true">${this.escapeXml(itemLink)}</guid>`;
|
|
1442
|
+
xml += `<description>${this.escapeXml(summary)}</description>`;
|
|
1443
|
+
xml += "</item>";
|
|
1444
|
+
}
|
|
1445
|
+
xml += "</channel>";
|
|
1446
|
+
xml += "</rss>";
|
|
1447
|
+
await fs3.promises.mkdir(data.output, { recursive: true });
|
|
1448
|
+
await fs3.promises.writeFile(feedPath, xml, "utf8");
|
|
1449
|
+
}
|
|
1371
1450
|
async buildLlmsFiles(data) {
|
|
1372
1451
|
if (!this.options.enableLlmsTxt) {
|
|
1373
1452
|
return;
|
|
@@ -1545,6 +1624,16 @@ var DoculaBuilder = class {
|
|
|
1545
1624
|
}
|
|
1546
1625
|
return urlPath;
|
|
1547
1626
|
}
|
|
1627
|
+
escapeXml(value) {
|
|
1628
|
+
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1629
|
+
}
|
|
1630
|
+
summarizeMarkdown(markdown, maxLength = 240) {
|
|
1631
|
+
const plainText = markdown.replace(/^#{1,6}\s+.*$/gm, " ").replace(/^\s*[-*+]\s+/gm, " ").replace(/^\s*---+\s*$/gm, " ").replace(/```[\s\S]*?```/g, " ").replace(/`([^`]+)`/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_~>#]+/g, " ").replace(/\s+/g, " ").trim();
|
|
1632
|
+
if (plainText.length <= maxLength) {
|
|
1633
|
+
return plainText;
|
|
1634
|
+
}
|
|
1635
|
+
return `${plainText.slice(0, maxLength).trimEnd()}...`;
|
|
1636
|
+
}
|
|
1548
1637
|
isRemoteUrl(url) {
|
|
1549
1638
|
return /^https?:\/\//i.test(url);
|
|
1550
1639
|
}
|
package/package.json
CHANGED
package/templates/modern/api.hbs
CHANGED
|
@@ -58,17 +58,21 @@
|
|
|
58
58
|
{{/each}}
|
|
59
59
|
</div>
|
|
60
60
|
{{/if}}
|
|
61
|
+
{{#if apiSpec.securitySchemes.length}}
|
|
61
62
|
<div class="api-auth">
|
|
62
63
|
<div class="api-auth__label">Authorization</div>
|
|
63
64
|
<div class="api-auth__controls">
|
|
64
65
|
<select class="api-auth__type" id="api-auth-type">
|
|
65
66
|
<option value="none">None</option>
|
|
66
|
-
|
|
67
|
-
<option value="
|
|
67
|
+
{{#each apiSpec.securitySchemes}}
|
|
68
|
+
<option value="{{this.key}}" data-scheme-type="{{this.type}}" data-scheme-scheme="{{this.scheme}}" data-scheme-name="{{this.name}}" data-scheme-in="{{this.in}}">{{this.description}}</option>
|
|
69
|
+
{{/each}}
|
|
68
70
|
</select>
|
|
69
71
|
<input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value" placeholder="Enter value..." />
|
|
72
|
+
<span class="api-auth__cookie-status api-auth__cookie-status--hidden" id="api-auth-cookie-status"></span>
|
|
70
73
|
</div>
|
|
71
74
|
</div>
|
|
75
|
+
{{/if}}
|
|
72
76
|
</section>
|
|
73
77
|
|
|
74
78
|
{{#each apiSpec.groups}}
|
|
@@ -351,6 +351,36 @@
|
|
|
351
351
|
display: none;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
.api-auth__cookie-status {
|
|
355
|
+
font-size: 0.8rem;
|
|
356
|
+
padding: 0.25rem 0.6rem;
|
|
357
|
+
border-radius: 6px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.api-auth__cookie-status--hidden {
|
|
361
|
+
display: none;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.api-auth__cookie-status--ok {
|
|
365
|
+
color: #047857;
|
|
366
|
+
background: rgba(16, 185, 129, 0.15);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
[data-theme="light"] .api-auth__cookie-status--ok {
|
|
370
|
+
color: #065f46;
|
|
371
|
+
background: rgba(16, 185, 129, 0.12);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.api-auth__cookie-status--warn {
|
|
375
|
+
color: #fbbf24;
|
|
376
|
+
background: rgba(251, 191, 36, 0.15);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
[data-theme="light"] .api-auth__cookie-status--warn {
|
|
380
|
+
color: #92400e;
|
|
381
|
+
background: rgba(251, 191, 36, 0.12);
|
|
382
|
+
}
|
|
383
|
+
|
|
354
384
|
/* Tag Group */
|
|
355
385
|
.api-group {
|
|
356
386
|
margin-bottom: 40px;
|
|
@@ -158,6 +158,17 @@ body {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
.cookie-auth-user:empty {
|
|
162
|
+
display: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.cookie-auth-user {
|
|
166
|
+
font-weight: 500;
|
|
167
|
+
max-width: 150px;
|
|
168
|
+
overflow: hidden;
|
|
169
|
+
text-overflow: ellipsis;
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
.cookie-auth-btn--fixed {
|
|
162
173
|
position: fixed;
|
|
163
174
|
bottom: 16px;
|
|
@@ -743,14 +754,30 @@ footer {
|
|
|
743
754
|
color: var(--muted);
|
|
744
755
|
}
|
|
745
756
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
757
|
+
|
|
758
|
+
.footer__link {
|
|
759
|
+
margin: 0 0.35rem;
|
|
760
|
+
color: var(--muted);
|
|
761
|
+
text-decoration: none;
|
|
762
|
+
display: inline-flex;
|
|
763
|
+
align-items: center;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.footer__link:hover {
|
|
767
|
+
color: var(--link);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.footer__icon {
|
|
771
|
+
width: 1rem;
|
|
772
|
+
height: 1rem;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.footer__bat {
|
|
776
|
+
width: 1.25rem;
|
|
777
|
+
height: 1.25rem;
|
|
750
778
|
}
|
|
751
779
|
|
|
752
780
|
@media (min-width: 992px) {
|
|
753
781
|
footer { height: 4.75rem; }
|
|
754
|
-
footer img { height: 1.75rem; }
|
|
755
782
|
}
|
|
756
783
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
</main>
|
|
45
45
|
{{/if}}
|
|
46
46
|
|
|
47
|
-
{{#if cookieAuth}}
|
|
47
|
+
{{#if cookieAuth.loginUrl}}
|
|
48
48
|
<a href="{{cookieAuth.loginUrl}}" class="cookie-auth-btn cookie-auth-btn--fixed" id="cookie-auth-login">
|
|
49
49
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>
|
|
50
50
|
<span>Log In</span>
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
<footer>
|
|
2
|
-
|
|
3
|
-
<a href="
|
|
4
|
-
|
|
2
|
+
{{#if enableLlmsTxt}}
|
|
3
|
+
<a href="/llms.txt" class="footer__link">llms.txt</a>
|
|
4
|
+
{{/if}}
|
|
5
|
+
{{#if hasFeed}}
|
|
6
|
+
<a href="/feed.xml" class="footer__link" aria-label="RSS Feed">
|
|
7
|
+
<svg class="footer__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>
|
|
8
|
+
</a>
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if enableLlmsTxt}}{{else}}{{#if hasFeed}}{{else}}Built using {{/if}}{{/if}}
|
|
11
|
+
<a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
|
|
12
|
+
<svg class="footer__bat" xmlns="http://www.w3.org/2000/svg" viewBox="70 110 360 205" fill="currentColor"><path d="M420,173c-16.76-18-72.78-36.65-131.59-30.58A160.15,160.15,0,0,0,269,120.55c-3.78,4.52-11.15,9.45-19,9.45s-15.23-4.93-19-9.45a160.15,160.15,0,0,0-19.4,21.83C152.78,136.31,96.76,154.94,80,173c48.86,5.86,79.5,34.09,79.5,72.06,52.37,0,77,28,90.5,59.94C263.54,273,288.13,245,340.5,245,340.5,207,371.14,178.8,420,173Z"/><circle cx="239.19" cy="145.36" r="3.32" fill="#fff"/><circle cx="260.81" cy="145.36" r="3.32" fill="#fff"/></svg>
|
|
5
13
|
</a>
|
|
6
14
|
</footer>
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
{{/each}}
|
|
40
40
|
{{/if}}
|
|
41
41
|
</nav>
|
|
42
|
-
{{#if cookieAuth}}
|
|
42
|
+
{{#if cookieAuth.loginUrl}}
|
|
43
43
|
<a href="{{cookieAuth.loginUrl}}" class="cookie-auth-btn" id="cookie-auth-login">
|
|
44
44
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>
|
|
45
45
|
<span>Log In</span>
|
|
46
46
|
</a>
|
|
47
|
+
<span class="cookie-auth-user" id="cookie-auth-user" style="display:none"></span>
|
|
47
48
|
<button class="cookie-auth-btn" id="cookie-auth-logout" style="display:none">
|
|
48
49
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
|
|
49
50
|
<span>Log Out</span>
|
|
@@ -85,11 +86,12 @@
|
|
|
85
86
|
</a>
|
|
86
87
|
{{/each}}
|
|
87
88
|
{{/if}}
|
|
88
|
-
{{#if cookieAuth}}
|
|
89
|
+
{{#if cookieAuth.loginUrl}}
|
|
89
90
|
<a class="mobile-nav__item" href="{{cookieAuth.loginUrl}}" id="cookie-auth-login-mobile">
|
|
90
91
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>
|
|
91
92
|
<span>Log In</span>
|
|
92
93
|
</a>
|
|
94
|
+
<span class="cookie-auth-user" id="cookie-auth-user-mobile" style="display:none"></span>
|
|
93
95
|
<button class="mobile-nav__item cookie-auth-btn--mobile" id="cookie-auth-logout-mobile" style="display:none">
|
|
94
96
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
|
|
95
97
|
<span>Log Out</span>
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
<link rel="icon" href="/favicon.ico">
|
|
7
7
|
<script>
|
|
8
8
|
(function(){
|
|
9
|
-
|
|
9
|
+
window.__doculaThemeKey = 'docula:theme:' + ({{#if siteUrl}}'{{siteUrl}}'{{else}}location.origin{{/if}}).replace(/^https?:\/\//, '');
|
|
10
|
+
var mode = localStorage.getItem(window.__doculaThemeKey) || {{#if themeMode}}'{{themeMode}}'{{else}}'system'{{/if}};
|
|
10
11
|
var resolved = mode === 'system'
|
|
11
12
|
? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
|
|
12
13
|
: mode;
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
</script>
|
|
10
10
|
<script>
|
|
11
11
|
(function() {
|
|
12
|
+
var themeKey = window.__doculaThemeKey;
|
|
12
13
|
function getMode() {
|
|
13
|
-
return localStorage.getItem(
|
|
14
|
+
return localStorage.getItem(themeKey) || {{#if themeMode}}'{{themeMode}}'{{else}}'system'{{/if}};
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function resolveTheme(mode) {
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
if (els.toggle) {
|
|
76
77
|
els.toggle.addEventListener('click', function() {
|
|
77
78
|
currentMode = nextMode(currentMode);
|
|
78
|
-
localStorage.setItem(
|
|
79
|
+
localStorage.setItem(themeKey, currentMode);
|
|
79
80
|
applyTheme(currentMode);
|
|
80
81
|
});
|
|
81
82
|
}
|
|
@@ -105,20 +106,38 @@
|
|
|
105
106
|
return false;
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
109
|
+
function getCookieValue() {
|
|
110
|
+
var match = document.cookie.split(';').find(function(c) {
|
|
110
111
|
return c.trim().startsWith(cookieName + '=');
|
|
111
112
|
});
|
|
113
|
+
return match ? match.trim().substring(cookieName.length + 1) : null;
|
|
114
|
+
}
|
|
115
|
+
function getDisplayName(token) {
|
|
116
|
+
try {
|
|
117
|
+
var payload = token.split('.')[1];
|
|
118
|
+
if (!payload) return null;
|
|
119
|
+
var json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
120
|
+
var claims = JSON.parse(json);
|
|
121
|
+
return claims.name || claims.preferred_username || claims.email || null;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
112
125
|
}
|
|
113
126
|
function updateAuthUI() {
|
|
114
|
-
var
|
|
127
|
+
var token = getCookieValue();
|
|
128
|
+
var loggedIn = !!token;
|
|
129
|
+
var displayName = loggedIn ? getDisplayName(token) : null;
|
|
115
130
|
var els = [
|
|
116
|
-
{ login: document.getElementById('cookie-auth-login'), logout: document.getElementById('cookie-auth-logout') },
|
|
117
|
-
{ login: document.getElementById('cookie-auth-login-mobile'), logout: document.getElementById('cookie-auth-logout-mobile') }
|
|
131
|
+
{ login: document.getElementById('cookie-auth-login'), logout: document.getElementById('cookie-auth-logout'), user: document.getElementById('cookie-auth-user') },
|
|
132
|
+
{ login: document.getElementById('cookie-auth-login-mobile'), logout: document.getElementById('cookie-auth-logout-mobile'), user: document.getElementById('cookie-auth-user-mobile') }
|
|
118
133
|
];
|
|
119
134
|
els.forEach(function(pair) {
|
|
120
135
|
if (pair.login) pair.login.style.display = loggedIn ? 'none' : '';
|
|
121
136
|
if (pair.logout) pair.logout.style.display = loggedIn ? '' : 'none';
|
|
137
|
+
if (pair.user) {
|
|
138
|
+
pair.user.textContent = displayName || '';
|
|
139
|
+
pair.user.style.display = (loggedIn && displayName) ? '' : 'none';
|
|
140
|
+
}
|
|
122
141
|
});
|
|
123
142
|
}
|
|
124
143
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -104,19 +104,52 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
104
104
|
group.classList.add('api-sidebar__group--collapsed');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
// Auth type selector: show/hide value input
|
|
107
|
+
// Auth type selector: show/hide value input based on OpenAPI securitySchemes
|
|
108
108
|
var authTypeSelect = document.getElementById('api-auth-type');
|
|
109
109
|
var authValueInput = document.getElementById('api-auth-value');
|
|
110
|
+
var cookieStatusEl = document.getElementById('api-auth-cookie-status');
|
|
110
111
|
if (authTypeSelect && authValueInput) {
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
function getSelectedSchemeData() {
|
|
113
|
+
var option = authTypeSelect.options[authTypeSelect.selectedIndex];
|
|
114
|
+
return {
|
|
115
|
+
key: option.value,
|
|
116
|
+
type: option.getAttribute('data-scheme-type'),
|
|
117
|
+
scheme: option.getAttribute('data-scheme-scheme'),
|
|
118
|
+
name: option.getAttribute('data-scheme-name'),
|
|
119
|
+
inLocation: option.getAttribute('data-scheme-in')
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function updateAuthUI() {
|
|
123
|
+
var data = getSelectedSchemeData();
|
|
124
|
+
if (data.key === 'none') {
|
|
125
|
+
authValueInput.classList.add('api-auth__value--hidden');
|
|
126
|
+
authValueInput.value = '';
|
|
127
|
+
if (cookieStatusEl) cookieStatusEl.classList.add('api-auth__cookie-status--hidden');
|
|
128
|
+
} else if (data.type === 'apiKey' && data.inLocation === 'cookie') {
|
|
113
129
|
authValueInput.classList.add('api-auth__value--hidden');
|
|
114
130
|
authValueInput.value = '';
|
|
131
|
+
if (cookieStatusEl) {
|
|
132
|
+
cookieStatusEl.classList.remove('api-auth__cookie-status--hidden');
|
|
133
|
+
var configEl = document.getElementById('cookie-auth-config');
|
|
134
|
+
var cookieName = configEl ? configEl.getAttribute('data-cookie-name') : (data.name || 'token');
|
|
135
|
+
var hasCookie = document.cookie.split(';').some(function(c) { return c.trim().startsWith(cookieName + '='); });
|
|
136
|
+
cookieStatusEl.textContent = hasCookie ? 'Logged in' : 'Not logged in — use Login button above';
|
|
137
|
+
cookieStatusEl.className = 'api-auth__cookie-status' + (hasCookie ? ' api-auth__cookie-status--ok' : ' api-auth__cookie-status--warn');
|
|
138
|
+
}
|
|
115
139
|
} else {
|
|
116
140
|
authValueInput.classList.remove('api-auth__value--hidden');
|
|
117
|
-
|
|
141
|
+
if (data.type === 'apiKey') {
|
|
142
|
+
authValueInput.placeholder = 'Enter API key...';
|
|
143
|
+
} else if (data.scheme === 'bearer') {
|
|
144
|
+
authValueInput.placeholder = 'Enter bearer token...';
|
|
145
|
+
} else {
|
|
146
|
+
authValueInput.placeholder = 'Enter value...';
|
|
147
|
+
}
|
|
148
|
+
if (cookieStatusEl) cookieStatusEl.classList.add('api-auth__cookie-status--hidden');
|
|
118
149
|
}
|
|
119
|
-
}
|
|
150
|
+
}
|
|
151
|
+
authTypeSelect.addEventListener('change', updateAuthUI);
|
|
152
|
+
updateAuthUI();
|
|
120
153
|
}
|
|
121
154
|
|
|
122
155
|
// Helper: expand an operation and its corresponding sidebar group
|
|
@@ -226,15 +259,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
226
259
|
if (value) headers[name] = value;
|
|
227
260
|
});
|
|
228
261
|
|
|
229
|
-
// Inject global auth header
|
|
262
|
+
// Inject global auth header from selected security scheme
|
|
230
263
|
var authType = document.getElementById('api-auth-type');
|
|
231
264
|
var authValue = document.getElementById('api-auth-value');
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
265
|
+
var useCookieAuth = false;
|
|
266
|
+
if (authType && authType.value !== 'none') {
|
|
267
|
+
var authOption = authType.options[authType.selectedIndex];
|
|
268
|
+
var schemeType = authOption.getAttribute('data-scheme-type');
|
|
269
|
+
var schemeName = authOption.getAttribute('data-scheme-name');
|
|
270
|
+
var schemeIn = authOption.getAttribute('data-scheme-in');
|
|
271
|
+
var schemeScheme = authOption.getAttribute('data-scheme-scheme');
|
|
272
|
+
if (schemeType === 'apiKey' && schemeIn === 'cookie') {
|
|
273
|
+
useCookieAuth = true;
|
|
274
|
+
} else if (authValue && authValue.value.trim()) {
|
|
275
|
+
var authVal = authValue.value.trim();
|
|
276
|
+
if (schemeType === 'apiKey' && schemeIn === 'header' && schemeName) {
|
|
277
|
+
headers[schemeName] = authVal;
|
|
278
|
+
} else if (schemeType === 'http' && schemeScheme === 'bearer') {
|
|
279
|
+
headers['Authorization'] = 'Bearer ' + authVal;
|
|
280
|
+
} else if (schemeType === 'apiKey' && schemeIn === 'query' && schemeName) {
|
|
281
|
+
queryParts.push(encodeURIComponent(schemeName) + '=' + encodeURIComponent(authVal));
|
|
282
|
+
queryString = '?' + queryParts.join('&');
|
|
283
|
+
}
|
|
238
284
|
}
|
|
239
285
|
}
|
|
240
286
|
|
|
@@ -247,6 +293,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
247
293
|
|
|
248
294
|
var url = baseUrl + path + queryString;
|
|
249
295
|
var fetchOptions = { method: method, headers: headers };
|
|
296
|
+
if (useCookieAuth) {
|
|
297
|
+
fetchOptions.credentials = 'include';
|
|
298
|
+
}
|
|
250
299
|
if (body && method !== 'GET' && method !== 'HEAD') {
|
|
251
300
|
fetchOptions.body = body;
|
|
252
301
|
}
|