docula 1.10.0 → 1.11.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/dist/docula.d.ts +18 -6
- package/dist/docula.js +315 -72
- 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/css/styles.css +5 -0
- package/templates/modern/includes/header-bar.hbs +3 -15
- package/templates/modern/includes/scripts.hbs +1 -0
- package/templates/modern/js/api.js +4 -2
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`;
|
|
@@ -1307,7 +1406,10 @@ import path5 from "path";
|
|
|
1307
1406
|
import { Writr as Writr4 } from "writr";
|
|
1308
1407
|
var writrOptions4 = {
|
|
1309
1408
|
throwOnEmitError: false,
|
|
1310
|
-
throwOnEmptyListeners: false
|
|
1409
|
+
throwOnEmptyListeners: false,
|
|
1410
|
+
renderOptions: {
|
|
1411
|
+
rawHtml: true
|
|
1412
|
+
}
|
|
1311
1413
|
};
|
|
1312
1414
|
function getChangelogEntries(changelogPath, options, hash, cachedEntries, previousHashes, currentHashes) {
|
|
1313
1415
|
const entries = [];
|
|
@@ -1407,7 +1509,53 @@ function generateChangelogPreview(markdown, maxLength = 500, mdx = false) {
|
|
|
1407
1509
|
if (cleaned.length <= minLength) {
|
|
1408
1510
|
return new Writr4(cleaned, writrOptions4).renderSync({ mdx });
|
|
1409
1511
|
}
|
|
1410
|
-
const
|
|
1512
|
+
const htmlBlocks = [];
|
|
1513
|
+
const tagPattern = /<\/?(\w+)\b[^>]*>/g;
|
|
1514
|
+
const blockStarts = [];
|
|
1515
|
+
for (const tagMatch of cleaned.matchAll(tagPattern)) {
|
|
1516
|
+
const fullMatch = tagMatch[0];
|
|
1517
|
+
const tagName = tagMatch[1];
|
|
1518
|
+
const isClosing = fullMatch.startsWith("</");
|
|
1519
|
+
if (isClosing) {
|
|
1520
|
+
for (let i = blockStarts.length - 1; i >= 0; i--) {
|
|
1521
|
+
if (blockStarts[i].tag === tagName) {
|
|
1522
|
+
if (blockStarts[i].depth === 0) {
|
|
1523
|
+
htmlBlocks.push({
|
|
1524
|
+
start: blockStarts[i].index,
|
|
1525
|
+
end: tagMatch.index + fullMatch.length
|
|
1526
|
+
});
|
|
1527
|
+
blockStarts.splice(i, 1);
|
|
1528
|
+
} else {
|
|
1529
|
+
blockStarts[i].depth--;
|
|
1530
|
+
}
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
} else if (!fullMatch.endsWith("/>")) {
|
|
1535
|
+
const existing = blockStarts.find((s) => s.tag === tagName);
|
|
1536
|
+
if (existing) {
|
|
1537
|
+
existing.depth++;
|
|
1538
|
+
} else {
|
|
1539
|
+
blockStarts.push({ tag: tagName, index: tagMatch.index, depth: 0 });
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
let effectiveMax = maxLength;
|
|
1544
|
+
let extended = true;
|
|
1545
|
+
while (extended) {
|
|
1546
|
+
extended = false;
|
|
1547
|
+
for (const block of htmlBlocks) {
|
|
1548
|
+
if (effectiveMax > block.start && effectiveMax < block.end) {
|
|
1549
|
+
let end = block.end;
|
|
1550
|
+
while (end < cleaned.length && cleaned[end] === "\n") {
|
|
1551
|
+
end++;
|
|
1552
|
+
}
|
|
1553
|
+
effectiveMax = end;
|
|
1554
|
+
extended = true;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const searchArea = cleaned.slice(0, effectiveMax);
|
|
1411
1559
|
let splitIndex = -1;
|
|
1412
1560
|
let pos = searchArea.lastIndexOf("\n\n");
|
|
1413
1561
|
while (pos >= 0) {
|
|
@@ -1434,7 +1582,7 @@ function generateChangelogPreview(markdown, maxLength = 500, mdx = false) {
|
|
|
1434
1582
|
let lastItemEnd = -1;
|
|
1435
1583
|
for (const line of lines) {
|
|
1436
1584
|
const lineEnd = charCount + line.length;
|
|
1437
|
-
if (lineEnd <=
|
|
1585
|
+
if (lineEnd <= effectiveMax && (/^[-*]\s/.test(line) || /^\d+\.\s/.test(line))) {
|
|
1438
1586
|
if (charCount > 0 && charCount >= minLength) {
|
|
1439
1587
|
lastItemEnd = charCount - 1;
|
|
1440
1588
|
}
|
|
@@ -1450,7 +1598,7 @@ function generateChangelogPreview(markdown, maxLength = 500, mdx = false) {
|
|
|
1450
1598
|
const truncated2 = cleaned.slice(0, splitIndex).trim();
|
|
1451
1599
|
return new Writr4(truncated2, writrOptions4).renderSync({ mdx });
|
|
1452
1600
|
}
|
|
1453
|
-
let truncated = cleaned.slice(0,
|
|
1601
|
+
let truncated = cleaned.slice(0, effectiveMax);
|
|
1454
1602
|
const lastSpace = truncated.lastIndexOf(" ");
|
|
1455
1603
|
if (lastSpace > 0) {
|
|
1456
1604
|
truncated = truncated.slice(0, lastSpace);
|
|
@@ -2233,7 +2381,13 @@ function generateLlmsIndexContent(data) {
|
|
|
2233
2381
|
}
|
|
2234
2382
|
lines.push("");
|
|
2235
2383
|
lines.push("## API Reference");
|
|
2236
|
-
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) {
|
|
2237
2391
|
lines.push(
|
|
2238
2392
|
`- [API Documentation](${buildAbsoluteSiteUrl(data.siteUrl, data.apiUrl)})`
|
|
2239
2393
|
);
|
|
@@ -2295,8 +2449,22 @@ async function generateLlmsFullContent(data) {
|
|
|
2295
2449
|
}
|
|
2296
2450
|
lines.push("");
|
|
2297
2451
|
lines.push("## API Reference");
|
|
2298
|
-
|
|
2299
|
-
|
|
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) {
|
|
2300
2468
|
lines.push("");
|
|
2301
2469
|
const localOpenApiSpec = await getSafeLocalOpenApiSpec(data);
|
|
2302
2470
|
if (localOpenApiSpec) {
|
|
@@ -2884,9 +3052,9 @@ var DoculaOptions = class {
|
|
|
2884
3052
|
*/
|
|
2885
3053
|
sections;
|
|
2886
3054
|
/**
|
|
2887
|
-
* OpenAPI specification
|
|
2888
|
-
*
|
|
2889
|
-
*
|
|
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.
|
|
2890
3058
|
*/
|
|
2891
3059
|
openApiUrl;
|
|
2892
3060
|
/**
|
|
@@ -3055,7 +3223,20 @@ var DoculaOptions = class {
|
|
|
3055
3223
|
this.port = options.port;
|
|
3056
3224
|
}
|
|
3057
3225
|
if (options.openApiUrl) {
|
|
3058
|
-
|
|
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
|
+
}
|
|
3059
3240
|
}
|
|
3060
3241
|
if (options.enableReleaseChangelog !== void 0 && typeof options.enableReleaseChangelog === "boolean") {
|
|
3061
3242
|
this.enableReleaseChangelog = options.enableReleaseChangelog;
|
|
@@ -3236,7 +3417,6 @@ var DoculaBuilder = class {
|
|
|
3236
3417
|
output: this.options.output,
|
|
3237
3418
|
githubPath: this.options.githubPath,
|
|
3238
3419
|
sections: this.options.sections,
|
|
3239
|
-
openApiUrl: this.options.openApiUrl,
|
|
3240
3420
|
hasReadme: fs11.existsSync(`${this.options.sitePath}/README.md`),
|
|
3241
3421
|
themeMode: this.options.themeMode,
|
|
3242
3422
|
cookieAuth: this.options.cookieAuth,
|
|
@@ -3260,12 +3440,63 @@ var DoculaBuilder = class {
|
|
|
3260
3440
|
if (doculaData.hasReadme) {
|
|
3261
3441
|
currentAssetHashes["README.md"] = hashFile(this._hash, readmePath);
|
|
3262
3442
|
}
|
|
3263
|
-
if (
|
|
3264
|
-
doculaData.
|
|
3265
|
-
|
|
3266
|
-
this.options.apiPath,
|
|
3267
|
-
"
|
|
3268
|
-
);
|
|
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
|
+
});
|
|
3269
3500
|
}
|
|
3270
3501
|
if (this.options.githubPath) {
|
|
3271
3502
|
doculaData.github = await this.getGithubData(this.options.githubPath);
|
|
@@ -3355,7 +3586,7 @@ var DoculaBuilder = class {
|
|
|
3355
3586
|
doculaData.hasChangelog
|
|
3356
3587
|
);
|
|
3357
3588
|
doculaData.hasApi = Boolean(
|
|
3358
|
-
doculaData.
|
|
3589
|
+
doculaData.openApiSpecs && doculaData.openApiSpecs.length > 0 && doculaData.templates?.api
|
|
3359
3590
|
);
|
|
3360
3591
|
doculaData.lastModified = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3361
3592
|
this._console.step("Building pages...");
|
|
@@ -3396,7 +3627,7 @@ var DoculaBuilder = class {
|
|
|
3396
3627
|
}
|
|
3397
3628
|
if (doculaData.hasApi) {
|
|
3398
3629
|
this._console.step("Building API page...");
|
|
3399
|
-
await this.
|
|
3630
|
+
await this.buildAllApiPages(doculaData);
|
|
3400
3631
|
this._console.fileBuilt(`${this.options.apiPath}/index.html`);
|
|
3401
3632
|
}
|
|
3402
3633
|
if (doculaData.hasChangelog) {
|
|
@@ -3498,6 +3729,27 @@ var DoculaBuilder = class {
|
|
|
3498
3729
|
swaggerPath
|
|
3499
3730
|
);
|
|
3500
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
|
+
}
|
|
3501
3753
|
copyPublicFolder(
|
|
3502
3754
|
this._console,
|
|
3503
3755
|
this._hash,
|
|
@@ -3582,19 +3834,6 @@ ${readmeContent}`;
|
|
|
3582
3834
|
}
|
|
3583
3835
|
await fs11.promises.mkdir(this._options.sitePath, { recursive: true });
|
|
3584
3836
|
await fs11.promises.writeFile(siteReadmePath, readmeContent, "utf8");
|
|
3585
|
-
const availableAssets = listContentAssets(this._options, cwdDir);
|
|
3586
|
-
for (const assetRelPath of availableAssets) {
|
|
3587
|
-
if (readmeContent.includes(assetRelPath)) {
|
|
3588
|
-
const source = path12.join(cwdDir, assetRelPath);
|
|
3589
|
-
const stat = await fs11.promises.lstat(source);
|
|
3590
|
-
if (stat.isSymbolicLink()) {
|
|
3591
|
-
continue;
|
|
3592
|
-
}
|
|
3593
|
-
const target = path12.join(this._options.sitePath, assetRelPath);
|
|
3594
|
-
await fs11.promises.mkdir(path12.dirname(target), { recursive: true });
|
|
3595
|
-
await fs11.promises.copyFile(source, target);
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
3837
|
}
|
|
3599
3838
|
async getGithubData(githubPath) {
|
|
3600
3839
|
const paths = githubPath.split("/");
|
|
@@ -3800,6 +4039,9 @@ ${readmeContent}`;
|
|
|
3800
4039
|
async buildApiPage(data) {
|
|
3801
4040
|
return buildApiPage(this._ecto, data);
|
|
3802
4041
|
}
|
|
4042
|
+
async buildAllApiPages(data) {
|
|
4043
|
+
return buildAllApiPages(this._ecto, data);
|
|
4044
|
+
}
|
|
3803
4045
|
async buildApiHomePage(data) {
|
|
3804
4046
|
return buildApiHomePage(this._ecto, data);
|
|
3805
4047
|
}
|
|
@@ -4331,6 +4573,7 @@ export {
|
|
|
4331
4573
|
/* v8 ignore start -- @preserve */
|
|
4332
4574
|
/* v8 ignore next -- @preserve */
|
|
4333
4575
|
/* v8 ignore next 3 -- @preserve */
|
|
4576
|
+
/* v8 ignore next 6 -- @preserve */
|
|
4334
4577
|
/* v8 ignore next 9 -- @preserve */
|
|
4335
4578
|
/* v8 ignore next 4 -- @preserve */
|
|
4336
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.0",
|
|
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>
|
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
function setAuthUI(loggedIn, displayName) {
|
|
135
135
|
window.__doculaAuth = { loggedIn: loggedIn, displayName: displayName };
|
|
136
136
|
try { localStorage.setItem('docula-auth-state', JSON.stringify(window.__doculaAuth)); } catch(e) {}
|
|
137
|
+
document.documentElement.classList.toggle('docula-auth-logged-in', loggedIn);
|
|
137
138
|
document.dispatchEvent(new CustomEvent('docula-auth-change'));
|
|
138
139
|
var els = [
|
|
139
140
|
{ login: document.getElementById('cookie-auth-login'), logout: document.getElementById('cookie-auth-logout'), user: document.getElementById('cookie-auth-user') },
|
|
@@ -99,9 +99,11 @@ 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
|