docula 1.0.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 +43 -1
- package/dist/docula.js +107 -3
- package/package.json +1 -1
- package/templates/classic/css/multipage.css +26 -0
- package/templates/classic/includes/multipage/header.hbs +5 -0
- package/templates/classic/includes/multipage/sidebar.hbs +7 -0
- 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 +28 -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 = {
|
|
@@ -75,6 +97,11 @@ type DoculaCookieAuth = {
|
|
|
75
97
|
cookieName?: string;
|
|
76
98
|
logoutUrl?: string;
|
|
77
99
|
};
|
|
100
|
+
type DoculaHeaderLink = {
|
|
101
|
+
label: string;
|
|
102
|
+
url: string;
|
|
103
|
+
icon?: string;
|
|
104
|
+
};
|
|
78
105
|
type DoculaCacheOptions = {
|
|
79
106
|
github: {
|
|
80
107
|
ttl: number;
|
|
@@ -161,6 +188,11 @@ declare class DoculaOptions {
|
|
|
161
188
|
* in the header based on whether a JWT cookie is present.
|
|
162
189
|
*/
|
|
163
190
|
cookieAuth?: DoculaCookieAuth;
|
|
191
|
+
/**
|
|
192
|
+
* Additional links to display in the site header navigation.
|
|
193
|
+
* Each link requires a label and url.
|
|
194
|
+
*/
|
|
195
|
+
headerLinks?: DoculaHeaderLink[];
|
|
164
196
|
/**
|
|
165
197
|
* File extensions to copy as assets from docs/ and changelog/ directories.
|
|
166
198
|
* Override in docula.config to customize.
|
|
@@ -217,6 +249,13 @@ type DoculaData = {
|
|
|
217
249
|
cookieName?: string;
|
|
218
250
|
logoutUrl?: string;
|
|
219
251
|
};
|
|
252
|
+
headerLinks?: Array<{
|
|
253
|
+
label: string;
|
|
254
|
+
url: string;
|
|
255
|
+
icon?: string;
|
|
256
|
+
}>;
|
|
257
|
+
enableLlmsTxt?: boolean;
|
|
258
|
+
hasFeed?: boolean;
|
|
220
259
|
};
|
|
221
260
|
type DoculaTemplates = {
|
|
222
261
|
home: string;
|
|
@@ -258,11 +297,14 @@ declare class DoculaBuilder {
|
|
|
258
297
|
getTemplateFile(path: string, name: string): Promise<string | undefined>;
|
|
259
298
|
buildRobotsPage(options: DoculaOptions): Promise<void>;
|
|
260
299
|
buildSiteMapPage(data: DoculaData): Promise<void>;
|
|
300
|
+
buildFeedPage(data: DoculaData): Promise<void>;
|
|
261
301
|
buildLlmsFiles(data: DoculaData): Promise<void>;
|
|
262
302
|
private generateLlmsIndexContent;
|
|
263
303
|
private generateLlmsFullContent;
|
|
264
304
|
private buildAbsoluteSiteUrl;
|
|
265
305
|
private normalizePathForUrl;
|
|
306
|
+
private escapeXml;
|
|
307
|
+
private summarizeMarkdown;
|
|
266
308
|
private isRemoteUrl;
|
|
267
309
|
private resolveOpenApiSpecUrl;
|
|
268
310
|
private resolveLocalOpenApiPath;
|
|
@@ -392,4 +434,4 @@ declare class Docula {
|
|
|
392
434
|
serve(options: DoculaOptions): Promise<http.Server>;
|
|
393
435
|
}
|
|
394
436
|
|
|
395
|
-
export { type DoculaCacheOptions, type DoculaCookieAuth, DoculaOptions, Docula as default };
|
|
437
|
+
export { type DoculaCacheOptions, type DoculaCookieAuth, type DoculaHeaderLink, DoculaOptions, Docula as default };
|
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) {
|
|
@@ -920,6 +956,11 @@ var DoculaOptions = class {
|
|
|
920
956
|
* in the header based on whether a JWT cookie is present.
|
|
921
957
|
*/
|
|
922
958
|
cookieAuth;
|
|
959
|
+
/**
|
|
960
|
+
* Additional links to display in the site header navigation.
|
|
961
|
+
* Each link requires a label and url.
|
|
962
|
+
*/
|
|
963
|
+
headerLinks;
|
|
923
964
|
/**
|
|
924
965
|
* File extensions to copy as assets from docs/ and changelog/ directories.
|
|
925
966
|
* Override in docula.config to customize.
|
|
@@ -1027,6 +1068,14 @@ var DoculaOptions = class {
|
|
|
1027
1068
|
if (options.cookieAuth && typeof options.cookieAuth === "object" && typeof options.cookieAuth.loginUrl === "string") {
|
|
1028
1069
|
this.cookieAuth = options.cookieAuth;
|
|
1029
1070
|
}
|
|
1071
|
+
if (options.headerLinks && Array.isArray(options.headerLinks)) {
|
|
1072
|
+
const validLinks = options.headerLinks.filter(
|
|
1073
|
+
(link) => typeof link === "object" && link !== null && typeof link.label === "string" && typeof link.url === "string"
|
|
1074
|
+
);
|
|
1075
|
+
if (validLinks.length > 0) {
|
|
1076
|
+
this.headerLinks = validLinks;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1030
1079
|
}
|
|
1031
1080
|
};
|
|
1032
1081
|
|
|
@@ -1094,7 +1143,9 @@ var DoculaBuilder = class {
|
|
|
1094
1143
|
openApiUrl: this.options.openApiUrl,
|
|
1095
1144
|
homePage: this.options.homePage,
|
|
1096
1145
|
themeMode: this.options.themeMode,
|
|
1097
|
-
cookieAuth: this.options.cookieAuth
|
|
1146
|
+
cookieAuth: this.options.cookieAuth,
|
|
1147
|
+
headerLinks: this.options.headerLinks,
|
|
1148
|
+
enableLlmsTxt: this.options.enableLlmsTxt
|
|
1098
1149
|
};
|
|
1099
1150
|
if (!doculaData.openApiUrl && fs3.existsSync(`${doculaData.sitePath}/api/swagger.json`)) {
|
|
1100
1151
|
doculaData.openApiUrl = "/api/swagger.json";
|
|
@@ -1111,6 +1162,7 @@ var DoculaBuilder = class {
|
|
|
1111
1162
|
this.options
|
|
1112
1163
|
);
|
|
1113
1164
|
doculaData.hasDocuments = doculaData.documents?.length > 0;
|
|
1165
|
+
doculaData.hasFeed = doculaData.hasDocuments;
|
|
1114
1166
|
const changelogPath = `${doculaData.sitePath}/changelog`;
|
|
1115
1167
|
const fileChangelogEntries = this.getChangelogEntries(changelogPath);
|
|
1116
1168
|
const hasChangelogTemplate = await this.getTemplateFile(resolvedTemplatePath, "changelog") !== void 0;
|
|
@@ -1162,6 +1214,10 @@ var DoculaBuilder = class {
|
|
|
1162
1214
|
this._console.fileBuilt("sitemap.xml");
|
|
1163
1215
|
await this.buildRobotsPage(this.options);
|
|
1164
1216
|
this._console.fileBuilt("robots.txt");
|
|
1217
|
+
if (doculaData.hasDocuments) {
|
|
1218
|
+
await this.buildFeedPage(doculaData);
|
|
1219
|
+
this._console.fileBuilt("feed.xml");
|
|
1220
|
+
}
|
|
1165
1221
|
if (doculaData.hasDocuments) {
|
|
1166
1222
|
this._console.step("Building documentation pages...");
|
|
1167
1223
|
await this.buildDocsPages(doculaData);
|
|
@@ -1325,6 +1381,9 @@ var DoculaBuilder = class {
|
|
|
1325
1381
|
async buildSiteMapPage(data) {
|
|
1326
1382
|
const sitemapPath = `${data.output}/sitemap.xml`;
|
|
1327
1383
|
const urls = [{ url: data.siteUrl }];
|
|
1384
|
+
if (data.documents?.length) {
|
|
1385
|
+
urls.push({ url: `${data.siteUrl}/feed.xml` });
|
|
1386
|
+
}
|
|
1328
1387
|
if (data.openApiUrl && data.templates?.api) {
|
|
1329
1388
|
urls.push({ url: `${data.siteUrl}/api` });
|
|
1330
1389
|
}
|
|
@@ -1354,6 +1413,40 @@ var DoculaBuilder = class {
|
|
|
1354
1413
|
await fs3.promises.mkdir(data.output, { recursive: true });
|
|
1355
1414
|
await fs3.promises.writeFile(sitemapPath, xml, "utf8");
|
|
1356
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
|
+
}
|
|
1357
1450
|
async buildLlmsFiles(data) {
|
|
1358
1451
|
if (!this.options.enableLlmsTxt) {
|
|
1359
1452
|
return;
|
|
@@ -1531,6 +1624,16 @@ var DoculaBuilder = class {
|
|
|
1531
1624
|
}
|
|
1532
1625
|
return urlPath;
|
|
1533
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
|
+
}
|
|
1534
1637
|
isRemoteUrl(url) {
|
|
1535
1638
|
return /^https?:\/\//i.test(url);
|
|
1536
1639
|
}
|
|
@@ -2649,6 +2752,7 @@ export {
|
|
|
2649
2752
|
};
|
|
2650
2753
|
/* v8 ignore next -- @preserve */
|
|
2651
2754
|
/* v8 ignore next 3 -- @preserve */
|
|
2755
|
+
/* v8 ignore next 5 -- @preserve */
|
|
2652
2756
|
/* v8 ignore next 2 -- @preserve */
|
|
2653
2757
|
/* v8 ignore next 9 -- @preserve */
|
|
2654
2758
|
/* v8 ignore next 4 -- @preserve */
|
package/package.json
CHANGED
|
@@ -127,6 +127,32 @@
|
|
|
127
127
|
align-items: center;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
.sidebar-links {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media screen and (min-width: 992px) {
|
|
135
|
+
.sidebar-links {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 0.5rem;
|
|
139
|
+
padding: 1rem 0;
|
|
140
|
+
border-top: 1px solid var(--border);
|
|
141
|
+
margin-top: 1rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.sidebar-header-link {
|
|
145
|
+
color: var(--sidebar-text);
|
|
146
|
+
text-decoration: none;
|
|
147
|
+
font-size: 0.875rem;
|
|
148
|
+
padding: 0.25rem 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.sidebar-header-link:hover {
|
|
152
|
+
text-decoration: underline;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
.header-logo {
|
|
131
157
|
flex-shrink: 0;
|
|
132
158
|
margin-right: 1.5rem;
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
<a class="header-logo" href="/">
|
|
6
6
|
<img src="/logo.svg" alt="logo" />
|
|
7
7
|
</a>
|
|
8
|
+
{{#if headerLinks}}
|
|
9
|
+
{{#each headerLinks}}
|
|
10
|
+
<a class="header-link" href="{{this.url}}" target="_blank" rel="noopener noreferrer">{{#if this.icon}}{{{this.icon}}} {{/if}}{{this.label}}</a>
|
|
11
|
+
{{/each}}
|
|
12
|
+
{{/if}}
|
|
8
13
|
<button id="open-sidebar" class="icon menu-btn">
|
|
9
14
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
|
|
10
15
|
<span>Menu</span>
|
|
@@ -36,6 +36,13 @@
|
|
|
36
36
|
{{/forEach}}
|
|
37
37
|
</ul>
|
|
38
38
|
|
|
39
|
+
{{#if headerLinks}}
|
|
40
|
+
<div class="sidebar-links">
|
|
41
|
+
{{#each headerLinks}}
|
|
42
|
+
<a class="sidebar-header-link" href="{{this.url}}" target="_blank" rel="noopener noreferrer">{{#if this.icon}}{{{this.icon}}} {{/if}}{{this.label}}</a>
|
|
43
|
+
{{/each}}
|
|
44
|
+
</div>
|
|
45
|
+
{{/if}}
|
|
39
46
|
</div>
|
|
40
47
|
</section>
|
|
41
48
|
|
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>
|
|
@@ -26,12 +26,25 @@
|
|
|
26
26
|
<span>Changelog</span>
|
|
27
27
|
</a>
|
|
28
28
|
{{/if}}
|
|
29
|
+
{{#if headerLinks}}
|
|
30
|
+
{{#each headerLinks}}
|
|
31
|
+
<a class="header-bottom__item" href="{{this.url}}" target="_blank" rel="noopener noreferrer">
|
|
32
|
+
{{#if this.icon}}
|
|
33
|
+
{{{this.icon}}}
|
|
34
|
+
{{else}}
|
|
35
|
+
<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 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
|
36
|
+
{{/if}}
|
|
37
|
+
<span>{{this.label}}</span>
|
|
38
|
+
</a>
|
|
39
|
+
{{/each}}
|
|
40
|
+
{{/if}}
|
|
29
41
|
</nav>
|
|
30
|
-
{{#if cookieAuth}}
|
|
42
|
+
{{#if cookieAuth.loginUrl}}
|
|
31
43
|
<a href="{{cookieAuth.loginUrl}}" class="cookie-auth-btn" id="cookie-auth-login">
|
|
32
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>
|
|
33
45
|
<span>Log In</span>
|
|
34
46
|
</a>
|
|
47
|
+
<span class="cookie-auth-user" id="cookie-auth-user" style="display:none"></span>
|
|
35
48
|
<button class="cookie-auth-btn" id="cookie-auth-logout" style="display:none">
|
|
36
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>
|
|
37
50
|
<span>Log Out</span>
|
|
@@ -61,11 +74,24 @@
|
|
|
61
74
|
<span>Changelog</span>
|
|
62
75
|
</a>
|
|
63
76
|
{{/if}}
|
|
64
|
-
{{#if
|
|
77
|
+
{{#if headerLinks}}
|
|
78
|
+
{{#each headerLinks}}
|
|
79
|
+
<a class="mobile-nav__item" href="{{this.url}}" target="_blank" rel="noopener noreferrer">
|
|
80
|
+
{{#if this.icon}}
|
|
81
|
+
{{{this.icon}}}
|
|
82
|
+
{{else}}
|
|
83
|
+
<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 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
|
84
|
+
{{/if}}
|
|
85
|
+
<span>{{this.label}}</span>
|
|
86
|
+
</a>
|
|
87
|
+
{{/each}}
|
|
88
|
+
{{/if}}
|
|
89
|
+
{{#if cookieAuth.loginUrl}}
|
|
65
90
|
<a class="mobile-nav__item" href="{{cookieAuth.loginUrl}}" id="cookie-auth-login-mobile">
|
|
66
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>
|
|
67
92
|
<span>Log In</span>
|
|
68
93
|
</a>
|
|
94
|
+
<span class="cookie-auth-user" id="cookie-auth-user-mobile" style="display:none"></span>
|
|
69
95
|
<button class="mobile-nav__item cookie-auth-btn--mobile" id="cookie-auth-logout-mobile" style="display:none">
|
|
70
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>
|
|
71
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
|
}
|