docula 1.10.1 → 1.11.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/dist/docula.d.ts +18 -6
- package/dist/docula.js +262 -68
- package/package.json +3 -2
- package/templates/classic/api.hbs +25 -12
- package/templates/classic/css/api.css +14 -0
- package/templates/classic/includes/multipage/api-reference.hbs +1 -1
- package/templates/classic/includes/multipage/header.hbs +1 -1
- package/templates/classic/includes/multipage/sidebar.hbs +1 -1
- package/templates/modern/api.hbs +28 -52
- package/templates/modern/css/api.css +14 -0
- package/templates/modern/includes/header-bar.hbs +3 -15
- package/templates/modern/js/api.js +10 -7
package/dist/docula.d.ts
CHANGED
|
@@ -129,6 +129,12 @@ type ApiSpecData = {
|
|
|
129
129
|
securitySchemes: ApiSecurityScheme[];
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
type DoculaOpenApiSpecEntry = {
|
|
133
|
+
name: string;
|
|
134
|
+
url: string;
|
|
135
|
+
order?: number;
|
|
136
|
+
apiSpec?: ApiSpecData;
|
|
137
|
+
};
|
|
132
138
|
type DoculaChangelogEntry = {
|
|
133
139
|
title: string;
|
|
134
140
|
date: string;
|
|
@@ -160,9 +166,9 @@ type DoculaData = {
|
|
|
160
166
|
documents?: DoculaDocument[];
|
|
161
167
|
sidebarItems?: DoculaSection[];
|
|
162
168
|
announcement?: string;
|
|
163
|
-
openApiUrl?: string;
|
|
164
169
|
hasApi?: boolean;
|
|
165
170
|
apiSpec?: ApiSpecData;
|
|
171
|
+
openApiSpecs?: DoculaOpenApiSpecEntry[];
|
|
166
172
|
changelogEntries?: DoculaChangelogEntry[];
|
|
167
173
|
hasReadme?: boolean;
|
|
168
174
|
themeMode?: string;
|
|
@@ -236,6 +242,11 @@ type DoculaHeaderLink = {
|
|
|
236
242
|
url: string;
|
|
237
243
|
icon?: string;
|
|
238
244
|
};
|
|
245
|
+
type DoculaOpenApiSpec = {
|
|
246
|
+
name: string;
|
|
247
|
+
url: string;
|
|
248
|
+
order?: number;
|
|
249
|
+
};
|
|
239
250
|
type DoculaOpenGraph = {
|
|
240
251
|
title?: string;
|
|
241
252
|
description?: string;
|
|
@@ -297,11 +308,11 @@ declare class DoculaOptions {
|
|
|
297
308
|
*/
|
|
298
309
|
sections?: DoculaSection[];
|
|
299
310
|
/**
|
|
300
|
-
* OpenAPI specification
|
|
301
|
-
*
|
|
302
|
-
*
|
|
311
|
+
* OpenAPI specification for API documentation.
|
|
312
|
+
* Pass a string URL for a single spec, or an array of DoculaOpenApiSpec for multiple specs.
|
|
313
|
+
* When provided, creates a dedicated /api page.
|
|
303
314
|
*/
|
|
304
|
-
openApiUrl?: string;
|
|
315
|
+
openApiUrl?: string | DoculaOpenApiSpec[];
|
|
305
316
|
/**
|
|
306
317
|
* When true, GitHub releases are converted to changelog entries and merged
|
|
307
318
|
* with file-based changelog entries. Requires a changelog template to exist.
|
|
@@ -442,6 +453,7 @@ declare class DoculaBuilder {
|
|
|
442
453
|
buildDocsPages(data: DoculaData): Promise<void>;
|
|
443
454
|
renderApiContent(data: DoculaData): Promise<string>;
|
|
444
455
|
buildApiPage(data: DoculaData): Promise<void>;
|
|
456
|
+
buildAllApiPages(data: DoculaData): Promise<void>;
|
|
445
457
|
buildApiHomePage(data: DoculaData): Promise<void>;
|
|
446
458
|
getChangelogEntries(changelogPath: string, cachedEntries?: Map<string, DoculaChangelogEntry>, previousHashes?: Record<string, string>, currentHashes?: Record<string, string>): DoculaChangelogEntry[];
|
|
447
459
|
parseChangelogEntry(filePath: string): DoculaChangelogEntry;
|
|
@@ -575,4 +587,4 @@ declare class Docula {
|
|
|
575
587
|
serve(options: DoculaOptions): Promise<http.Server>;
|
|
576
588
|
}
|
|
577
589
|
|
|
578
|
-
export { type DoculaAIOptions, type DoculaCacheOptions, type DoculaChangelogEntry, DoculaConsole, type DoculaCookieAuth, type DoculaHeaderLink, DoculaOptions, Docula as default };
|
|
590
|
+
export { type DoculaAIOptions, type DoculaCacheOptions, type DoculaChangelogEntry, DoculaConsole, type DoculaCookieAuth, type DoculaHeaderLink, type DoculaOpenApiSpec, DoculaOptions, Docula as default };
|
package/dist/docula.js
CHANGED
|
@@ -222,7 +222,7 @@ function parseOpenApiSpec(specJson) {
|
|
|
222
222
|
if (!operation) {
|
|
223
223
|
continue;
|
|
224
224
|
}
|
|
225
|
-
const tags = Array.isArray(operation.tags) && operation.tags.length > 0 ? operation.tags : ["
|
|
225
|
+
const tags = Array.isArray(operation.tags) && operation.tags.length > 0 ? operation.tags : [""];
|
|
226
226
|
const parameters = extractParameters(operation, pathItem, spec);
|
|
227
227
|
const requestBody = extractRequestBody(operation, spec);
|
|
228
228
|
const responses = extractResponses(operation, spec);
|
|
@@ -832,7 +832,7 @@ async function buildSiteMapPage(data, options) {
|
|
|
832
832
|
url: `${data.siteUrl}${data.baseUrl}/changelog-latest.json`
|
|
833
833
|
});
|
|
834
834
|
}
|
|
835
|
-
if (data.
|
|
835
|
+
if (data.hasApi || data.openApiSpecs?.[0] && data.templates?.api) {
|
|
836
836
|
urls.push({
|
|
837
837
|
url: `${data.siteUrl}${data.apiUrl}`
|
|
838
838
|
});
|
|
@@ -926,26 +926,16 @@ var writrOptions3 = {
|
|
|
926
926
|
throwOnEmptyListeners: false
|
|
927
927
|
};
|
|
928
928
|
function resolveOpenApiSpecUrl(data) {
|
|
929
|
-
|
|
929
|
+
const specUrl = data.openApiSpecs?.[0]?.url;
|
|
930
|
+
if (!specUrl) {
|
|
930
931
|
return void 0;
|
|
931
932
|
}
|
|
932
|
-
if (isRemoteUrl(
|
|
933
|
-
return
|
|
933
|
+
if (isRemoteUrl(specUrl)) {
|
|
934
|
+
return specUrl;
|
|
934
935
|
}
|
|
935
|
-
const normalizedPath =
|
|
936
|
+
const normalizedPath = specUrl.startsWith("/") ? specUrl : `/${specUrl}`;
|
|
936
937
|
return buildAbsoluteSiteUrl(data.siteUrl, normalizedPath);
|
|
937
938
|
}
|
|
938
|
-
function resolveLocalOpenApiPath(data) {
|
|
939
|
-
if (!data.openApiUrl || isRemoteUrl(data.openApiUrl)) {
|
|
940
|
-
return void 0;
|
|
941
|
-
}
|
|
942
|
-
const openApiPathWithoutQuery = data.openApiUrl.split(/[?#]/)[0];
|
|
943
|
-
if (!openApiPathWithoutQuery) {
|
|
944
|
-
return void 0;
|
|
945
|
-
}
|
|
946
|
-
const normalizedPath = openApiPathWithoutQuery.startsWith("/") ? openApiPathWithoutQuery.slice(1) : openApiPathWithoutQuery;
|
|
947
|
-
return path3.join(data.sitePath, normalizedPath);
|
|
948
|
-
}
|
|
949
939
|
async function getSafeSiteOverrideFileContent(sitePath, fileName) {
|
|
950
940
|
const resolvedSitePath = path3.resolve(sitePath);
|
|
951
941
|
const candidatePath = path3.resolve(sitePath, fileName);
|
|
@@ -975,45 +965,152 @@ async function getSafeSiteOverrideFileContent(sitePath, fileName) {
|
|
|
975
965
|
return fs3.promises.readFile(realCandidatePath, "utf8");
|
|
976
966
|
}
|
|
977
967
|
async function getSafeLocalOpenApiSpec(data) {
|
|
978
|
-
const
|
|
979
|
-
if (!
|
|
968
|
+
const specUrl = data.openApiSpecs?.[0]?.url;
|
|
969
|
+
if (!specUrl) {
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
return getSafeLocalOpenApiSpecForSpec(data, specUrl);
|
|
973
|
+
}
|
|
974
|
+
function resolveLocalOpenApiPathForSpec(data, specUrl) {
|
|
975
|
+
if (isRemoteUrl(specUrl)) {
|
|
976
|
+
return void 0;
|
|
977
|
+
}
|
|
978
|
+
const urlWithoutQuery = specUrl.split(/[?#]/)[0];
|
|
979
|
+
if (!urlWithoutQuery) {
|
|
980
|
+
return void 0;
|
|
981
|
+
}
|
|
982
|
+
const normalizedPath = urlWithoutQuery.startsWith("/") ? urlWithoutQuery.slice(1) : urlWithoutQuery;
|
|
983
|
+
return path3.join(data.sitePath, normalizedPath);
|
|
984
|
+
}
|
|
985
|
+
async function getSafeLocalOpenApiSpecForSpec(data, specUrl) {
|
|
986
|
+
const localPath = resolveLocalOpenApiPathForSpec(data, specUrl);
|
|
987
|
+
if (!localPath) {
|
|
980
988
|
return void 0;
|
|
981
989
|
}
|
|
982
990
|
const resolvedSitePath = path3.resolve(data.sitePath);
|
|
983
|
-
const
|
|
984
|
-
if (!isPathWithinBasePath(
|
|
991
|
+
const resolvedLocalPath = path3.resolve(localPath);
|
|
992
|
+
if (!isPathWithinBasePath(resolvedLocalPath, resolvedSitePath)) {
|
|
985
993
|
return void 0;
|
|
986
994
|
}
|
|
987
|
-
let
|
|
995
|
+
let localStats;
|
|
988
996
|
try {
|
|
989
|
-
|
|
997
|
+
localStats = await fs3.promises.lstat(resolvedLocalPath);
|
|
990
998
|
} catch {
|
|
991
999
|
return void 0;
|
|
992
1000
|
}
|
|
993
|
-
if (!
|
|
1001
|
+
if (!localStats.isFile() || localStats.isSymbolicLink()) {
|
|
994
1002
|
return void 0;
|
|
995
1003
|
}
|
|
996
1004
|
let realSitePath;
|
|
997
|
-
let
|
|
1005
|
+
let realLocalPath;
|
|
998
1006
|
try {
|
|
999
1007
|
realSitePath = await fs3.promises.realpath(resolvedSitePath);
|
|
1000
|
-
|
|
1008
|
+
realLocalPath = await fs3.promises.realpath(resolvedLocalPath);
|
|
1001
1009
|
} catch {
|
|
1002
1010
|
return void 0;
|
|
1003
1011
|
}
|
|
1004
|
-
if (!isPathWithinBasePath(
|
|
1012
|
+
if (!isPathWithinBasePath(realLocalPath, realSitePath)) {
|
|
1005
1013
|
return void 0;
|
|
1006
1014
|
}
|
|
1007
|
-
const
|
|
1008
|
-
return {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1015
|
+
const content = (await fs3.promises.readFile(realLocalPath, "utf8")).trim();
|
|
1016
|
+
return { sourcePath: realLocalPath, content };
|
|
1017
|
+
}
|
|
1018
|
+
function resolveSpecUrl(data, specUrl) {
|
|
1019
|
+
if (isRemoteUrl(specUrl)) {
|
|
1020
|
+
return specUrl;
|
|
1021
|
+
}
|
|
1022
|
+
const normalizedPath = specUrl.startsWith("/") ? specUrl : `/${specUrl}`;
|
|
1023
|
+
return buildAbsoluteSiteUrl(data.siteUrl, normalizedPath);
|
|
1024
|
+
}
|
|
1025
|
+
async function parseAndRenderSpec(data, specUrl) {
|
|
1026
|
+
let apiSpec;
|
|
1027
|
+
const localSpec = await getSafeLocalOpenApiSpecForSpec(data, specUrl);
|
|
1028
|
+
if (localSpec) {
|
|
1029
|
+
apiSpec = parseOpenApiSpec(localSpec.content);
|
|
1030
|
+
} else if (isRemoteUrl(specUrl)) {
|
|
1031
|
+
try {
|
|
1032
|
+
const response = await fetch(specUrl);
|
|
1033
|
+
const specContent = await response.text();
|
|
1034
|
+
apiSpec = parseOpenApiSpec(specContent);
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (apiSpec) {
|
|
1039
|
+
apiSpec.info.description = new Writr3(
|
|
1040
|
+
apiSpec.info.description,
|
|
1041
|
+
writrOptions3
|
|
1042
|
+
).renderSync();
|
|
1043
|
+
for (const group of apiSpec.groups) {
|
|
1044
|
+
group.description = new Writr3(
|
|
1045
|
+
group.description,
|
|
1046
|
+
writrOptions3
|
|
1047
|
+
).renderSync();
|
|
1048
|
+
for (const op of group.operations) {
|
|
1049
|
+
op.description = new Writr3(op.description, writrOptions3).renderSync();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return apiSpec;
|
|
1054
|
+
}
|
|
1055
|
+
async function copySpecSourceFile(data, specUrl, outputDir) {
|
|
1056
|
+
const safeSpec = await getSafeLocalOpenApiSpecForSpec(data, specUrl);
|
|
1057
|
+
if (safeSpec) {
|
|
1058
|
+
await fs3.promises.mkdir(outputDir, { recursive: true });
|
|
1059
|
+
await fs3.promises.copyFile(
|
|
1060
|
+
safeSpec.sourcePath,
|
|
1061
|
+
`${outputDir}/swagger.json`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function renderCombinedApiContent(ecto, data) {
|
|
1066
|
+
if (!data.templates?.api) {
|
|
1067
|
+
throw new Error("No API template found");
|
|
1068
|
+
}
|
|
1069
|
+
const specs = data.openApiSpecs ?? [];
|
|
1070
|
+
const apiSpecs = [];
|
|
1071
|
+
for (const spec of specs) {
|
|
1072
|
+
const urlDir = spec.url.replace(/[?#].*$/, "").replace(/\/[^/]+$/, "");
|
|
1073
|
+
const specOutputDir = `${data.output}/${urlDir}`;
|
|
1074
|
+
await copySpecSourceFile(data, spec.url, specOutputDir);
|
|
1075
|
+
const apiSpec = await parseAndRenderSpec(data, spec.url);
|
|
1076
|
+
apiSpecs.push({
|
|
1077
|
+
specName: spec.name,
|
|
1078
|
+
apiSpec,
|
|
1079
|
+
specUrl: resolveSpecUrl(data, spec.url)
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
const apiTemplate = `${data.templatePath}/${data.templates.api}`;
|
|
1083
|
+
return ecto.renderFromFile(
|
|
1084
|
+
apiTemplate,
|
|
1085
|
+
{
|
|
1086
|
+
...data,
|
|
1087
|
+
apiSpecs,
|
|
1088
|
+
// Backward compat: set single apiSpec for single-spec sites
|
|
1089
|
+
apiSpec: apiSpecs[0]?.apiSpec,
|
|
1090
|
+
specUrl: apiSpecs[0]?.specUrl,
|
|
1091
|
+
...resolveOpenGraphData(data, `/${data.apiPath}/`),
|
|
1092
|
+
jsonLd: resolveJsonLd("api", data, `/${data.apiPath}/`)
|
|
1093
|
+
},
|
|
1094
|
+
data.templatePath
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
async function buildAllApiPages(ecto, data) {
|
|
1098
|
+
if (!data.openApiSpecs || data.openApiSpecs.length === 0 || !data.templates?.api) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const outputDir = `${data.output}/${data.apiPath}`;
|
|
1102
|
+
await fs3.promises.mkdir(outputDir, { recursive: true });
|
|
1103
|
+
const content = await renderCombinedApiContent(ecto, data);
|
|
1104
|
+
await fs3.promises.writeFile(`${outputDir}/index.html`, content, "utf8");
|
|
1012
1105
|
}
|
|
1013
1106
|
async function renderApiContent(ecto, data) {
|
|
1014
|
-
|
|
1107
|
+
const firstSpecUrl = data.openApiSpecs?.[0]?.url;
|
|
1108
|
+
if (!firstSpecUrl || !data.templates?.api) {
|
|
1015
1109
|
throw new Error("No API template or openApiUrl found");
|
|
1016
1110
|
}
|
|
1111
|
+
if (data.openApiSpecs && data.openApiSpecs.length > 0) {
|
|
1112
|
+
return renderCombinedApiContent(ecto, data);
|
|
1113
|
+
}
|
|
1017
1114
|
const swaggerSource = `${data.sitePath}/api/swagger.json`;
|
|
1018
1115
|
const apiOutputPath = `${data.output}/${data.apiPath}`;
|
|
1019
1116
|
await fs3.promises.mkdir(apiOutputPath, { recursive: true });
|
|
@@ -1024,9 +1121,9 @@ async function renderApiContent(ecto, data) {
|
|
|
1024
1121
|
const localSpec = await getSafeLocalOpenApiSpec(data);
|
|
1025
1122
|
if (localSpec) {
|
|
1026
1123
|
apiSpec = parseOpenApiSpec(localSpec.content);
|
|
1027
|
-
} else if (
|
|
1124
|
+
} else if (firstSpecUrl && isRemoteUrl(firstSpecUrl)) {
|
|
1028
1125
|
try {
|
|
1029
|
-
const response = await fetch(
|
|
1126
|
+
const response = await fetch(firstSpecUrl);
|
|
1030
1127
|
const specContent = await response.text();
|
|
1031
1128
|
apiSpec = parseOpenApiSpec(specContent);
|
|
1032
1129
|
} catch {
|
|
@@ -1052,8 +1149,9 @@ async function renderApiContent(ecto, data) {
|
|
|
1052
1149
|
apiTemplate,
|
|
1053
1150
|
{
|
|
1054
1151
|
...data,
|
|
1055
|
-
specUrl:
|
|
1152
|
+
specUrl: firstSpecUrl,
|
|
1056
1153
|
apiSpec,
|
|
1154
|
+
apiSpecs: [{ specName: "API Reference", apiSpec, specUrl: firstSpecUrl }],
|
|
1057
1155
|
...resolveOpenGraphData(data, `/${data.apiPath}/`),
|
|
1058
1156
|
jsonLd: resolveJsonLd("api", data, `/${data.apiPath}/`)
|
|
1059
1157
|
},
|
|
@@ -1061,12 +1159,13 @@ async function renderApiContent(ecto, data) {
|
|
|
1061
1159
|
);
|
|
1062
1160
|
}
|
|
1063
1161
|
async function buildApiPage(ecto, data) {
|
|
1064
|
-
if (!data.
|
|
1162
|
+
if (!data.openApiSpecs?.[0]?.url || !data.templates?.api) {
|
|
1065
1163
|
return;
|
|
1066
1164
|
}
|
|
1067
|
-
const
|
|
1165
|
+
const apiDir = `${data.output}/${data.apiPath}`;
|
|
1166
|
+
await fs3.promises.mkdir(apiDir, { recursive: true });
|
|
1068
1167
|
const apiContent = await renderApiContent(ecto, data);
|
|
1069
|
-
await fs3.promises.writeFile(
|
|
1168
|
+
await fs3.promises.writeFile(`${apiDir}/index.html`, apiContent, "utf8");
|
|
1070
1169
|
}
|
|
1071
1170
|
async function buildApiHomePage(ecto, data) {
|
|
1072
1171
|
const indexPath = `${data.output}/index.html`;
|
|
@@ -2282,7 +2381,13 @@ function generateLlmsIndexContent(data) {
|
|
|
2282
2381
|
}
|
|
2283
2382
|
lines.push("");
|
|
2284
2383
|
lines.push("## API Reference");
|
|
2285
|
-
if (data.
|
|
2384
|
+
if (data.openApiSpecs && data.openApiSpecs.length > 1) {
|
|
2385
|
+
for (const spec of data.openApiSpecs) {
|
|
2386
|
+
lines.push(
|
|
2387
|
+
`- [${spec.name}](${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)})`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
} else if (data.hasApi) {
|
|
2286
2391
|
lines.push(
|
|
2287
2392
|
`- [API Documentation](${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)})`
|
|
2288
2393
|
);
|
|
@@ -2344,8 +2449,22 @@ async function generateLlmsFullContent(data) {
|
|
|
2344
2449
|
}
|
|
2345
2450
|
lines.push("");
|
|
2346
2451
|
lines.push("## API Reference");
|
|
2347
|
-
|
|
2348
|
-
|
|
2452
|
+
lines.push(`URL: ${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)}`);
|
|
2453
|
+
if (data.openApiSpecs && data.openApiSpecs.length > 1) {
|
|
2454
|
+
for (const spec of data.openApiSpecs) {
|
|
2455
|
+
lines.push("");
|
|
2456
|
+
lines.push(`### ${spec.name}`);
|
|
2457
|
+
lines.push("");
|
|
2458
|
+
const localSpec = await getSafeLocalOpenApiSpecForSpec(data, spec.url);
|
|
2459
|
+
if (localSpec) {
|
|
2460
|
+
lines.push(`OpenAPI Spec Source: ${toPosixPath(localSpec.sourcePath)}`);
|
|
2461
|
+
lines.push("");
|
|
2462
|
+
lines.push(localSpec.content || "_No content_");
|
|
2463
|
+
} else {
|
|
2464
|
+
lines.push(`OpenAPI Spec URL: ${resolveSpecUrl(data, spec.url)}`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
} else if (data.hasApi) {
|
|
2349
2468
|
lines.push("");
|
|
2350
2469
|
const localOpenApiSpec = await getSafeLocalOpenApiSpec(data);
|
|
2351
2470
|
if (localOpenApiSpec) {
|
|
@@ -2933,9 +3052,9 @@ var DoculaOptions = class {
|
|
|
2933
3052
|
*/
|
|
2934
3053
|
sections;
|
|
2935
3054
|
/**
|
|
2936
|
-
* OpenAPI specification
|
|
2937
|
-
*
|
|
2938
|
-
*
|
|
3055
|
+
* OpenAPI specification for API documentation.
|
|
3056
|
+
* Pass a string URL for a single spec, or an array of DoculaOpenApiSpec for multiple specs.
|
|
3057
|
+
* When provided, creates a dedicated /api page.
|
|
2939
3058
|
*/
|
|
2940
3059
|
openApiUrl;
|
|
2941
3060
|
/**
|
|
@@ -3104,7 +3223,20 @@ var DoculaOptions = class {
|
|
|
3104
3223
|
this.port = options.port;
|
|
3105
3224
|
}
|
|
3106
3225
|
if (options.openApiUrl) {
|
|
3107
|
-
|
|
3226
|
+
if (typeof options.openApiUrl === "string") {
|
|
3227
|
+
this.openApiUrl = options.openApiUrl;
|
|
3228
|
+
} else if (Array.isArray(options.openApiUrl)) {
|
|
3229
|
+
const validSpecs = options.openApiUrl.filter(
|
|
3230
|
+
(spec) => typeof spec === "object" && spec !== null && typeof spec.name === "string" && typeof spec.url === "string"
|
|
3231
|
+
);
|
|
3232
|
+
if (validSpecs.length > 0) {
|
|
3233
|
+
this.openApiUrl = validSpecs.map((spec) => ({
|
|
3234
|
+
name: spec.name,
|
|
3235
|
+
url: spec.url,
|
|
3236
|
+
...typeof spec.order === "number" && { order: spec.order }
|
|
3237
|
+
}));
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3108
3240
|
}
|
|
3109
3241
|
if (options.enableReleaseChangelog !== void 0 && typeof options.enableReleaseChangelog === "boolean") {
|
|
3110
3242
|
this.enableReleaseChangelog = options.enableReleaseChangelog;
|
|
@@ -3285,7 +3417,6 @@ var DoculaBuilder = class {
|
|
|
3285
3417
|
output: this.options.output,
|
|
3286
3418
|
githubPath: this.options.githubPath,
|
|
3287
3419
|
sections: this.options.sections,
|
|
3288
|
-
openApiUrl: this.options.openApiUrl,
|
|
3289
3420
|
hasReadme: fs11.existsSync(`${this.options.sitePath}/README.md`),
|
|
3290
3421
|
themeMode: this.options.themeMode,
|
|
3291
3422
|
cookieAuth: this.options.cookieAuth,
|
|
@@ -3309,12 +3440,63 @@ var DoculaBuilder = class {
|
|
|
3309
3440
|
if (doculaData.hasReadme) {
|
|
3310
3441
|
currentAssetHashes["README.md"] = hashFile(this._hash, readmePath);
|
|
3311
3442
|
}
|
|
3312
|
-
if (
|
|
3313
|
-
doculaData.
|
|
3314
|
-
|
|
3315
|
-
this.options.apiPath,
|
|
3316
|
-
"
|
|
3317
|
-
);
|
|
3443
|
+
if (Array.isArray(this.options.openApiUrl)) {
|
|
3444
|
+
doculaData.openApiSpecs = this.options.openApiUrl.map((spec) => ({
|
|
3445
|
+
name: spec.name,
|
|
3446
|
+
url: isRemoteUrl(spec.url) ? spec.url : buildUrlPath(this.options.apiPath, spec.url),
|
|
3447
|
+
...typeof spec.order === "number" && { order: spec.order }
|
|
3448
|
+
}));
|
|
3449
|
+
} else if (typeof this.options.openApiUrl === "string") {
|
|
3450
|
+
doculaData.openApiSpecs = [
|
|
3451
|
+
{ name: "API Reference", url: this.options.openApiUrl }
|
|
3452
|
+
];
|
|
3453
|
+
} else {
|
|
3454
|
+
const detectedSpecs = [];
|
|
3455
|
+
if (fs11.existsSync(`${doculaData.sitePath}/api/swagger.json`)) {
|
|
3456
|
+
const rootUrl = buildUrlPath(
|
|
3457
|
+
this.options.baseUrl,
|
|
3458
|
+
this.options.apiPath,
|
|
3459
|
+
"swagger.json"
|
|
3460
|
+
);
|
|
3461
|
+
detectedSpecs.push({
|
|
3462
|
+
name: "API Reference",
|
|
3463
|
+
url: rootUrl
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
const apiDir = `${doculaData.sitePath}/api`;
|
|
3467
|
+
if (fs11.existsSync(apiDir)) {
|
|
3468
|
+
try {
|
|
3469
|
+
const entries = await fs11.promises.readdir(apiDir, {
|
|
3470
|
+
withFileTypes: true
|
|
3471
|
+
});
|
|
3472
|
+
for (const entry of entries) {
|
|
3473
|
+
if (entry.isDirectory() && fs11.existsSync(`${apiDir}/${entry.name}/swagger.json`)) {
|
|
3474
|
+
const specUrl = buildUrlPath(
|
|
3475
|
+
this.options.baseUrl,
|
|
3476
|
+
this.options.apiPath,
|
|
3477
|
+
entry.name,
|
|
3478
|
+
"swagger.json"
|
|
3479
|
+
);
|
|
3480
|
+
const displayName = entry.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3481
|
+
detectedSpecs.push({
|
|
3482
|
+
name: displayName,
|
|
3483
|
+
url: specUrl
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
if (detectedSpecs.length > 0) {
|
|
3491
|
+
doculaData.openApiSpecs = detectedSpecs;
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
if (doculaData.openApiSpecs && doculaData.openApiSpecs.length > 1) {
|
|
3495
|
+
doculaData.openApiSpecs.sort((a, b) => {
|
|
3496
|
+
const aOrder = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
3497
|
+
const bOrder = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
3498
|
+
return aOrder - bOrder;
|
|
3499
|
+
});
|
|
3318
3500
|
}
|
|
3319
3501
|
if (this.options.githubPath) {
|
|
3320
3502
|
doculaData.github = await this.getGithubData(this.options.githubPath);
|
|
@@ -3404,7 +3586,7 @@ var DoculaBuilder = class {
|
|
|
3404
3586
|
doculaData.hasChangelog
|
|
3405
3587
|
);
|
|
3406
3588
|
doculaData.hasApi = Boolean(
|
|
3407
|
-
doculaData.
|
|
3589
|
+
doculaData.openApiSpecs && doculaData.openApiSpecs.length > 0 && doculaData.templates?.api
|
|
3408
3590
|
);
|
|
3409
3591
|
doculaData.lastModified = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3410
3592
|
this._console.step("Building pages...");
|
|
@@ -3445,7 +3627,7 @@ var DoculaBuilder = class {
|
|
|
3445
3627
|
}
|
|
3446
3628
|
if (doculaData.hasApi) {
|
|
3447
3629
|
this._console.step("Building API page...");
|
|
3448
|
-
await this.
|
|
3630
|
+
await this.buildAllApiPages(doculaData);
|
|
3449
3631
|
this._console.fileBuilt(`${this.options.apiPath}/index.html`);
|
|
3450
3632
|
}
|
|
3451
3633
|
if (doculaData.hasChangelog) {
|
|
@@ -3547,6 +3729,27 @@ var DoculaBuilder = class {
|
|
|
3547
3729
|
swaggerPath
|
|
3548
3730
|
);
|
|
3549
3731
|
}
|
|
3732
|
+
const apiDirPath = `${siteRelativePath}/api`;
|
|
3733
|
+
if (fs11.existsSync(apiDirPath)) {
|
|
3734
|
+
try {
|
|
3735
|
+
const apiEntries = await fs11.promises.readdir(apiDirPath, {
|
|
3736
|
+
withFileTypes: true
|
|
3737
|
+
});
|
|
3738
|
+
for (const entry of apiEntries) {
|
|
3739
|
+
if (entry.isDirectory()) {
|
|
3740
|
+
const subSwaggerPath = `${apiDirPath}/${entry.name}/swagger.json`;
|
|
3741
|
+
if (fs11.existsSync(subSwaggerPath)) {
|
|
3742
|
+
const hashKey = `api/${entry.name}/swagger.json`;
|
|
3743
|
+
currentAssetHashes[hashKey] = hashFile(
|
|
3744
|
+
this._hash,
|
|
3745
|
+
subSwaggerPath
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
} catch {
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3550
3753
|
copyPublicFolder(
|
|
3551
3754
|
this._console,
|
|
3552
3755
|
this._hash,
|
|
@@ -3631,19 +3834,6 @@ ${readmeContent}`;
|
|
|
3631
3834
|
}
|
|
3632
3835
|
await fs11.promises.mkdir(this._options.sitePath, { recursive: true });
|
|
3633
3836
|
await fs11.promises.writeFile(siteReadmePath, readmeContent, "utf8");
|
|
3634
|
-
const availableAssets = listContentAssets(this._options, cwdDir);
|
|
3635
|
-
for (const assetRelPath of availableAssets) {
|
|
3636
|
-
if (readmeContent.includes(assetRelPath)) {
|
|
3637
|
-
const source = path12.join(cwdDir, assetRelPath);
|
|
3638
|
-
const stat = await fs11.promises.lstat(source);
|
|
3639
|
-
if (stat.isSymbolicLink()) {
|
|
3640
|
-
continue;
|
|
3641
|
-
}
|
|
3642
|
-
const target = path12.join(this._options.sitePath, assetRelPath);
|
|
3643
|
-
await fs11.promises.mkdir(path12.dirname(target), { recursive: true });
|
|
3644
|
-
await fs11.promises.copyFile(source, target);
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
3837
|
}
|
|
3648
3838
|
async getGithubData(githubPath) {
|
|
3649
3839
|
const paths = githubPath.split("/");
|
|
@@ -3849,6 +4039,9 @@ ${readmeContent}`;
|
|
|
3849
4039
|
async buildApiPage(data) {
|
|
3850
4040
|
return buildApiPage(this._ecto, data);
|
|
3851
4041
|
}
|
|
4042
|
+
async buildAllApiPages(data) {
|
|
4043
|
+
return buildAllApiPages(this._ecto, data);
|
|
4044
|
+
}
|
|
3852
4045
|
async buildApiHomePage(data) {
|
|
3853
4046
|
return buildApiHomePage(this._ecto, data);
|
|
3854
4047
|
}
|
|
@@ -4380,6 +4573,7 @@ export {
|
|
|
4380
4573
|
/* v8 ignore start -- @preserve */
|
|
4381
4574
|
/* v8 ignore next -- @preserve */
|
|
4382
4575
|
/* v8 ignore next 3 -- @preserve */
|
|
4576
|
+
/* v8 ignore next 6 -- @preserve */
|
|
4383
4577
|
/* v8 ignore next 9 -- @preserve */
|
|
4384
4578
|
/* v8 ignore next 4 -- @preserve */
|
|
4385
4579
|
/* v8 ignore next 5 -- @preserve */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docula",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Beautiful Website for Your Projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/docula.js",
|
|
@@ -87,7 +87,8 @@
|
|
|
87
87
|
"generate-init-file": "tsx scripts/generate-init-file.ts",
|
|
88
88
|
"website:build": "node bin/docula.js build -s ./site -o ./site/dist",
|
|
89
89
|
"website:serve": "pnpm build && node bin/docula.js dev -s ./site -p 3333",
|
|
90
|
-
"website:
|
|
90
|
+
"website:mapi": "pnpm build && node bin/docula.js dev -s ./test/fixtures/multi-api-site -p 3333",
|
|
91
|
+
"website:mega": "node bin/docula.js dev -s ./test/fixtures/mega-page-site",
|
|
91
92
|
"website:mega:custom": "node bin/docula.js serve -s ./test/fixtures/mega-custom-template --watch",
|
|
92
93
|
"website:singlepage": "node bin/docula.js dev -s ./test/fixtures/single-page-site --clean",
|
|
93
94
|
"website:nohome": "node bin/docula.js serve -s ./test/fixtures/mega-page-site-no-home-page --watch --clean",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</style>
|
|
30
30
|
{{/if}}
|
|
31
31
|
|
|
32
|
-
{{#if
|
|
32
|
+
{{#if apiSpecs}}
|
|
33
33
|
<div class="api-mobile-toggle">
|
|
34
34
|
<button id="api-sidebar-toggle">
|
|
35
35
|
<span>API Navigation</span>
|
|
@@ -41,12 +41,17 @@
|
|
|
41
41
|
<aside class="api-sidebar" id="api-sidebar">
|
|
42
42
|
<input type="text" class="api-search" id="api-search" placeholder="Search endpoints..." />
|
|
43
43
|
|
|
44
|
-
{{#each
|
|
44
|
+
{{#each apiSpecs}}
|
|
45
|
+
{{#if this.apiSpec}}
|
|
46
|
+
<div class="api-sidebar__spec-heading">{{this.specName}}</div>
|
|
47
|
+
{{#each this.apiSpec.groups}}
|
|
45
48
|
<div class="api-sidebar__group" data-group="{{this.id}}">
|
|
49
|
+
{{#if this.name}}
|
|
46
50
|
<button class="api-sidebar__group-toggle">
|
|
47
51
|
<span>{{this.name}}</span>
|
|
48
52
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
|
49
53
|
</button>
|
|
54
|
+
{{/if}}
|
|
50
55
|
<div class="api-sidebar__group-items">
|
|
51
56
|
{{#each this.operations}}
|
|
52
57
|
<a href="#{{this.id}}" class="api-sidebar__item" data-method="{{this.method}}" data-path="{{this.path}}">
|
|
@@ -57,21 +62,25 @@
|
|
|
57
62
|
</div>
|
|
58
63
|
</div>
|
|
59
64
|
{{/each}}
|
|
65
|
+
{{/if}}
|
|
66
|
+
{{/each}}
|
|
60
67
|
</aside>
|
|
61
68
|
|
|
62
69
|
<main class="api-content">
|
|
70
|
+
{{#each apiSpecs}}
|
|
71
|
+
{{#if this.apiSpec}}
|
|
63
72
|
<section class="api-info">
|
|
64
|
-
<h1 class="api-info__title">{{apiSpec.info.title}}</h1>
|
|
65
|
-
{{#if apiSpec.info.version}}
|
|
66
|
-
<span class="api-info__version">v{{apiSpec.info.version}}</span>
|
|
73
|
+
<h1 class="api-info__title">{{this.apiSpec.info.title}}</h1>
|
|
74
|
+
{{#if this.apiSpec.info.version}}
|
|
75
|
+
<span class="api-info__version">v{{this.apiSpec.info.version}}</span>
|
|
67
76
|
{{/if}}
|
|
68
|
-
{{#if apiSpec.info.description}}
|
|
69
|
-
<div class="api-info__description">{{{apiSpec.info.description}}}</div>
|
|
77
|
+
{{#if this.apiSpec.info.description}}
|
|
78
|
+
<div class="api-info__description">{{{this.apiSpec.info.description}}}</div>
|
|
70
79
|
{{/if}}
|
|
71
|
-
{{#if apiSpec.servers}}
|
|
80
|
+
{{#if this.apiSpec.servers}}
|
|
72
81
|
<div class="api-info__servers">
|
|
73
82
|
<div class="api-info__server-label">Server</div>
|
|
74
|
-
{{#each apiSpec.servers}}
|
|
83
|
+
{{#each this.apiSpec.servers}}
|
|
75
84
|
<code class="api-info__server-url">{{this.url}}</code>
|
|
76
85
|
{{/each}}
|
|
77
86
|
</div>
|
|
@@ -79,24 +88,26 @@
|
|
|
79
88
|
<div class="api-auth">
|
|
80
89
|
<div class="api-auth__label">Authorization</div>
|
|
81
90
|
<div class="api-auth__controls">
|
|
82
|
-
<select class="api-auth__type" id="api-auth-type">
|
|
91
|
+
<select class="api-auth__type" id="api-auth-type-{{@index}}">
|
|
83
92
|
<option value="none">None</option>
|
|
84
93
|
<option value="apikey">API Key (x-api-key)</option>
|
|
85
94
|
<option value="bearer">Bearer Token</option>
|
|
86
95
|
</select>
|
|
87
|
-
<input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value" placeholder="Enter value..." />
|
|
96
|
+
<input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value-{{@index}}" placeholder="Enter value..." />
|
|
88
97
|
</div>
|
|
89
98
|
</div>
|
|
90
99
|
</section>
|
|
91
100
|
|
|
92
|
-
{{#each apiSpec.groups}}
|
|
101
|
+
{{#each this.apiSpec.groups}}
|
|
93
102
|
<div class="api-group" id="group-{{this.id}}">
|
|
103
|
+
{{#if this.name}}
|
|
94
104
|
<div class="api-group__header">
|
|
95
105
|
<h2 class="api-group__title">{{this.name}}</h2>
|
|
96
106
|
{{#if this.description}}
|
|
97
107
|
<div class="api-group__description">{{{this.description}}}</div>
|
|
98
108
|
{{/if}}
|
|
99
109
|
</div>
|
|
110
|
+
{{/if}}
|
|
100
111
|
|
|
101
112
|
{{#each this.operations}}
|
|
102
113
|
<div class="api-operation api-operation--collapsed" id="{{this.id}}">
|
|
@@ -202,6 +213,8 @@
|
|
|
202
213
|
{{/each}}
|
|
203
214
|
</div>
|
|
204
215
|
{{/each}}
|
|
216
|
+
{{/if}}
|
|
217
|
+
{{/each}}
|
|
205
218
|
</main>
|
|
206
219
|
</div>
|
|
207
220
|
{{/if}}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
/* API Reference - Classic Template */
|
|
2
2
|
|
|
3
|
+
.api-sidebar__spec-heading {
|
|
4
|
+
font-size: 13px;
|
|
5
|
+
font-weight: 700;
|
|
6
|
+
color: var(--text-primary, #333);
|
|
7
|
+
padding: 10px 10px 4px;
|
|
8
|
+
margin-top: 8px;
|
|
9
|
+
border-top: 1px solid var(--border-color, #e0e0e0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.api-sidebar__spec-heading:first-child {
|
|
13
|
+
margin-top: 0;
|
|
14
|
+
border-top: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
.api-reference {
|
|
4
18
|
display: flex;
|
|
5
19
|
max-width: 90%;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<a href="
|
|
1
|
+
<a href="{{apiUrl}}" class="home-docs-button">API Reference</a>
|
package/templates/modern/api.hbs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<body>
|
|
13
13
|
{{> header-bar }}
|
|
14
14
|
|
|
15
|
-
{{#if
|
|
15
|
+
{{#if apiSpecs}}
|
|
16
16
|
<div class="api-mobile-toggle">
|
|
17
17
|
<button id="api-sidebar-toggle">
|
|
18
18
|
<span>API Navigation</span>
|
|
@@ -24,12 +24,17 @@
|
|
|
24
24
|
<aside class="api-sidebar" id="api-sidebar">
|
|
25
25
|
<input type="text" class="api-search" id="api-search" placeholder="Search endpoints..." />
|
|
26
26
|
|
|
27
|
-
{{#each
|
|
27
|
+
{{#each apiSpecs}}
|
|
28
|
+
{{#if this.apiSpec}}
|
|
29
|
+
<div class="api-sidebar__spec-heading">{{this.specName}}</div>
|
|
30
|
+
{{#each this.apiSpec.groups}}
|
|
28
31
|
<div class="api-sidebar__group" data-group="{{this.id}}">
|
|
32
|
+
{{#if this.name}}
|
|
29
33
|
<button class="api-sidebar__group-toggle">
|
|
30
34
|
<span>{{this.name}}</span>
|
|
31
35
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
|
32
36
|
</button>
|
|
37
|
+
{{/if}}
|
|
33
38
|
<div class="api-sidebar__group-items">
|
|
34
39
|
{{#each this.operations}}
|
|
35
40
|
<a href="#{{this.id}}" class="api-sidebar__item" data-method="{{this.method}}" data-path="{{this.path}}">
|
|
@@ -40,87 +45,56 @@
|
|
|
40
45
|
</div>
|
|
41
46
|
</div>
|
|
42
47
|
{{/each}}
|
|
48
|
+
{{/if}}
|
|
49
|
+
{{/each}}
|
|
43
50
|
</aside>
|
|
44
51
|
|
|
45
52
|
<main class="api-content">
|
|
53
|
+
{{#each apiSpecs}}
|
|
54
|
+
{{#if this.apiSpec}}
|
|
46
55
|
<section class="api-info">
|
|
47
|
-
<h1 class="api-info__title">{{apiSpec.info.title}}</h1>
|
|
48
|
-
{{#if apiSpec.info.version}}
|
|
49
|
-
<span class="api-info__version">v{{apiSpec.info.version}}</span>
|
|
56
|
+
<h1 class="api-info__title">{{this.apiSpec.info.title}}</h1>
|
|
57
|
+
{{#if this.apiSpec.info.version}}
|
|
58
|
+
<span class="api-info__version">v{{this.apiSpec.info.version}}</span>
|
|
50
59
|
{{/if}}
|
|
51
|
-
{{#if apiSpec.info.description}}
|
|
52
|
-
<div class="api-info__description">{{{apiSpec.info.description}}}</div>
|
|
60
|
+
{{#if this.apiSpec.info.description}}
|
|
61
|
+
<div class="api-info__description">{{{this.apiSpec.info.description}}}</div>
|
|
53
62
|
{{/if}}
|
|
54
|
-
{{#if apiSpec.servers}}
|
|
63
|
+
{{#if this.apiSpec.servers}}
|
|
55
64
|
<div class="api-info__servers">
|
|
56
65
|
<div class="api-info__server-label">Server</div>
|
|
57
|
-
{{#each apiSpec.servers}}
|
|
66
|
+
{{#each this.apiSpec.servers}}
|
|
58
67
|
<code class="api-info__server-url">{{this.url}}</code>
|
|
59
68
|
{{/each}}
|
|
60
69
|
</div>
|
|
61
70
|
{{/if}}
|
|
62
|
-
{{#if apiSpec.securitySchemes.length}}
|
|
71
|
+
{{#if this.apiSpec.securitySchemes.length}}
|
|
63
72
|
<div class="api-auth">
|
|
64
73
|
<div class="api-auth__label">Authorization</div>
|
|
65
74
|
<div class="api-auth__controls">
|
|
66
|
-
<select class="api-auth__type" id="api-auth-type">
|
|
75
|
+
<select class="api-auth__type" id="api-auth-type-{{@index}}">
|
|
67
76
|
<option value="none">None</option>
|
|
68
|
-
{{#each apiSpec.securitySchemes}}
|
|
77
|
+
{{#each this.apiSpec.securitySchemes}}
|
|
69
78
|
<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>
|
|
70
79
|
{{/each}}
|
|
71
80
|
</select>
|
|
72
|
-
<input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value" placeholder="Enter value..." />
|
|
73
|
-
<span class="api-auth__cookie-status api-auth__cookie-status--hidden" id="api-auth-cookie-status"></span>
|
|
74
|
-
<script>
|
|
75
|
-
(function() {
|
|
76
|
-
var authTypeSelect = document.getElementById('api-auth-type');
|
|
77
|
-
if (!authTypeSelect) return;
|
|
78
|
-
|
|
79
|
-
// Restore selection from localStorage
|
|
80
|
-
var savedAuthType = localStorage.getItem('docula-api-auth-type');
|
|
81
|
-
if (savedAuthType) {
|
|
82
|
-
for (var i = 0; i < authTypeSelect.options.length; i++) {
|
|
83
|
-
if (authTypeSelect.options[i].value === savedAuthType) {
|
|
84
|
-
authTypeSelect.selectedIndex = i;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Update UI based on selection
|
|
91
|
-
var selectedOption = authTypeSelect.options[authTypeSelect.selectedIndex];
|
|
92
|
-
var authValueInput = document.getElementById('api-auth-value');
|
|
93
|
-
var cookieStatusBadge = document.getElementById('api-auth-cookie-status');
|
|
94
|
-
|
|
95
|
-
if (selectedOption.value === 'none') return;
|
|
96
|
-
|
|
97
|
-
var isCookieAuth = selectedOption.getAttribute('data-scheme-type') === 'apiKey'
|
|
98
|
-
&& selectedOption.getAttribute('data-scheme-in') === 'cookie';
|
|
99
|
-
|
|
100
|
-
if (isCookieAuth) {
|
|
101
|
-
if (cookieStatusBadge) {
|
|
102
|
-
var auth = window.__doculaAuth || { loggedIn: false };
|
|
103
|
-
cookieStatusBadge.textContent = auth.loggedIn ? 'Logged in' : 'Not logged in — use Login button above';
|
|
104
|
-
cookieStatusBadge.className = 'api-auth__cookie-status' + (auth.loggedIn ? ' api-auth__cookie-status--ok' : ' api-auth__cookie-status--warn');
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
if (authValueInput) authValueInput.classList.remove('api-auth__value--hidden');
|
|
108
|
-
}
|
|
109
|
-
})();
|
|
110
|
-
</script>
|
|
81
|
+
<input type="password" class="api-auth__value api-auth__value--hidden" id="api-auth-value-{{@index}}" placeholder="Enter value..." />
|
|
82
|
+
<span class="api-auth__cookie-status api-auth__cookie-status--hidden" id="api-auth-cookie-status-{{@index}}"></span>
|
|
111
83
|
</div>
|
|
112
84
|
</div>
|
|
113
85
|
{{/if}}
|
|
114
86
|
</section>
|
|
115
87
|
|
|
116
|
-
{{#each apiSpec.groups}}
|
|
88
|
+
{{#each this.apiSpec.groups}}
|
|
117
89
|
<div class="api-group" id="group-{{this.id}}">
|
|
90
|
+
{{#if this.name}}
|
|
118
91
|
<div class="api-group__header">
|
|
119
92
|
<h2 class="api-group__title">{{this.name}}</h2>
|
|
120
93
|
{{#if this.description}}
|
|
121
94
|
<div class="api-group__description">{{{this.description}}}</div>
|
|
122
95
|
{{/if}}
|
|
123
96
|
</div>
|
|
97
|
+
{{/if}}
|
|
124
98
|
|
|
125
99
|
{{#each this.operations}}
|
|
126
100
|
<div class="api-operation api-operation--collapsed" id="{{this.id}}">
|
|
@@ -244,6 +218,8 @@
|
|
|
244
218
|
{{/each}}
|
|
245
219
|
</div>
|
|
246
220
|
{{/each}}
|
|
221
|
+
{{/if}}
|
|
222
|
+
{{/each}}
|
|
247
223
|
</main>
|
|
248
224
|
</div>
|
|
249
225
|
{{/if}}
|
|
@@ -50,6 +50,20 @@
|
|
|
50
50
|
color: var(--muted-fg);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
.api-sidebar__spec-heading {
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
color: var(--fg);
|
|
57
|
+
padding: 10px 10px 4px;
|
|
58
|
+
margin-top: 8px;
|
|
59
|
+
border-top: 1px solid var(--border);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.api-sidebar__spec-heading:first-child {
|
|
63
|
+
margin-top: 0;
|
|
64
|
+
border-top: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
.api-sidebar__group {
|
|
54
68
|
margin-bottom: 4px;
|
|
55
69
|
}
|
|
@@ -4,24 +4,18 @@
|
|
|
4
4
|
<button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle navigation menu">
|
|
5
5
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
|
6
6
|
</button>
|
|
7
|
-
<a href="{{baseUrl}}/" class="logo-link">
|
|
7
|
+
<a href="{{#if homeUrl}}{{homeUrl}}{{else}}{{baseUrl}}/{{/if}}" class="logo-link">
|
|
8
8
|
<img alt="{{siteTitle}}" class="logo__img" src="{{baseUrl}}/logo.svg">
|
|
9
9
|
<span class="logo__text">{{siteTitle}}</span>
|
|
10
10
|
</a>
|
|
11
11
|
<nav class="header-bottom__nav">
|
|
12
|
-
{{#if homeUrl}}
|
|
13
|
-
<a class="header-bottom__item header-bottom__item--home" href="{{homeUrl}}">
|
|
14
|
-
<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="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
|
|
15
|
-
<span>Back to Home</span>
|
|
16
|
-
</a>
|
|
17
|
-
{{/if}}
|
|
18
12
|
{{#if hasDocuments}}
|
|
19
13
|
<a class="header-bottom__item" href="{{docsUrl}}/" id="nav-docs">
|
|
20
14
|
<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="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
|
21
15
|
<span>Documentation</span>
|
|
22
16
|
</a>
|
|
23
17
|
{{/if}}
|
|
24
|
-
{{#if
|
|
18
|
+
{{#if hasApi}}
|
|
25
19
|
<a class="header-bottom__item" href="{{apiUrl}}" id="nav-api">
|
|
26
20
|
<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="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
|
|
27
21
|
<span>API Reference</span>
|
|
@@ -74,19 +68,13 @@
|
|
|
74
68
|
</header>
|
|
75
69
|
<aside class="mobile-sidebar" id="mobile-sidebar">
|
|
76
70
|
<nav class="mobile-nav">
|
|
77
|
-
{{#if homeUrl}}
|
|
78
|
-
<a class="mobile-nav__item" href="{{homeUrl}}">
|
|
79
|
-
<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="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
|
|
80
|
-
<span>Back to Home</span>
|
|
81
|
-
</a>
|
|
82
|
-
{{/if}}
|
|
83
71
|
{{#if hasDocuments}}
|
|
84
72
|
<a class="mobile-nav__item" href="{{docsUrl}}/">
|
|
85
73
|
<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="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
|
86
74
|
<span>Documentation</span>
|
|
87
75
|
</a>
|
|
88
76
|
{{/if}}
|
|
89
|
-
{{#if
|
|
77
|
+
{{#if hasApi}}
|
|
90
78
|
<a class="mobile-nav__item" href="{{apiUrl}}">
|
|
91
79
|
<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="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
|
|
92
80
|
<span>API Reference</span>
|
|
@@ -99,15 +99,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
// Collapse
|
|
102
|
+
// Collapse sidebar groups that have a toggle button (named groups only)
|
|
103
103
|
document.querySelectorAll('.api-sidebar__group').forEach(function(group) {
|
|
104
|
-
group.
|
|
104
|
+
if (group.querySelector('.api-sidebar__group-toggle')) {
|
|
105
|
+
group.classList.add('api-sidebar__group--collapsed');
|
|
106
|
+
}
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
// Auth type selector: show/hide value input based on OpenAPI securitySchemes
|
|
108
|
-
|
|
109
|
-
var
|
|
110
|
-
var
|
|
110
|
+
// IDs are indexed for multi-spec (e.g. api-auth-type-0), so find by class
|
|
111
|
+
var authTypeSelect = document.querySelector('.api-auth__type');
|
|
112
|
+
var authValueInput = document.querySelector('.api-auth__value');
|
|
113
|
+
var cookieStatusEl = document.querySelector('.api-auth__cookie-status');
|
|
111
114
|
if (authTypeSelect && authValueInput) {
|
|
112
115
|
function getSelectedSchemeData() {
|
|
113
116
|
var option = authTypeSelect.options[authTypeSelect.selectedIndex];
|
|
@@ -271,8 +274,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
271
274
|
});
|
|
272
275
|
|
|
273
276
|
// Inject global auth header from selected security scheme
|
|
274
|
-
var authType = document.
|
|
275
|
-
var authValue = document.
|
|
277
|
+
var authType = document.querySelector('.api-auth__type');
|
|
278
|
+
var authValue = document.querySelector('.api-auth__value');
|
|
276
279
|
var useCookieAuth = false;
|
|
277
280
|
if (authType && authType.value !== 'none') {
|
|
278
281
|
var authOption = authType.options[authType.selectedIndex];
|