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 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 and robots.txt for your site.
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
- return { info, servers, groups };
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<${itemType}>`;
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Beautiful Website for Your Projects",
5
5
  "type": "module",
6
6
  "main": "./dist/docula.js",
@@ -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
- <option value="apikey">API Key (x-api-key)</option>
67
- <option value="bearer">Bearer Token</option>
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
- footer img {
747
- margin-left: 0.5rem;
748
- height: 1.5rem;
749
- width: auto;
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
- Powered by
3
- <a href="https://docula.org/" target="_blank" rel="noopener noreferrer">
4
- <img src="https://docula.org/logo_horizontal.png" alt="docula logo" width="140" height="30" />
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
- var mode = localStorage.getItem('theme') || {{#if themeMode}}'{{themeMode}}'{{else}}'system'{{/if}};
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('theme') || {{#if themeMode}}'{{themeMode}}'{{else}}'system'{{/if}};
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('theme', currentMode);
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 hasCookie() {
109
- return document.cookie.split(';').some(function(c) {
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 loggedIn = hasCookie();
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
- authTypeSelect.addEventListener('change', function() {
112
- if (this.value === 'none') {
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
- authValueInput.placeholder = this.value === 'apikey' ? 'Enter API key...' : 'Enter token...';
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
- if (authType && authValue && authValue.value.trim()) {
233
- var authVal = authValue.value.trim();
234
- if (authType.value === 'apikey') {
235
- headers['x-api-key'] = authVal;
236
- } else if (authType.value === 'bearer') {
237
- headers['Authorization'] = 'Bearer ' + authVal;
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
  }