radiant-docs-validator 0.1.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/chunk-652XVPHE.js +16 -0
- package/dist/chunk-SXKC5VM6.js +10 -0
- package/dist/frontmatter-schema.d.ts +14 -0
- package/dist/frontmatter-schema.js +6 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +2035 -0
- package/dist/shiki-theme-config.d.ts +10 -0
- package/dist/shiki-theme-config.js +12 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2035 @@
|
|
|
1
|
+
import {
|
|
2
|
+
docsSchema
|
|
3
|
+
} from "./chunk-SXKC5VM6.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_SHIKI_DARK_THEME,
|
|
6
|
+
DEFAULT_SHIKI_LIGHT_THEME,
|
|
7
|
+
SHIKI_BUNDLED_THEME_NAMES,
|
|
8
|
+
isBundledShikiThemeName
|
|
9
|
+
} from "./chunk-652XVPHE.js";
|
|
10
|
+
|
|
11
|
+
// src/validator.ts
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import { createRequire } from "module";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import pkg from "@stoplight/spectral-core";
|
|
16
|
+
import { oas } from "@stoplight/spectral-rulesets";
|
|
17
|
+
import { compile } from "@mdx-js/mdx";
|
|
18
|
+
import yaml from "yaml";
|
|
19
|
+
var { Spectral } = pkg;
|
|
20
|
+
var DOCS_DIR = "";
|
|
21
|
+
var CONFIG_PATH = "";
|
|
22
|
+
var require2 = createRequire(import.meta.url);
|
|
23
|
+
function assertConfigured() {
|
|
24
|
+
if (!DOCS_DIR) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Docs validator has not been configured. Call configureDocsValidator({ docsRoot }) before validating docs."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function configureDocsValidator({
|
|
31
|
+
docsRoot
|
|
32
|
+
}) {
|
|
33
|
+
const nextDocsDir = path.resolve(docsRoot);
|
|
34
|
+
if (DOCS_DIR === nextDocsDir) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
DOCS_DIR = nextDocsDir;
|
|
38
|
+
CONFIG_PATH = path.join(DOCS_DIR, "docs.json");
|
|
39
|
+
iconSets.clear();
|
|
40
|
+
openApiSpecCache.clear();
|
|
41
|
+
configCache = null;
|
|
42
|
+
lastMtime = 0;
|
|
43
|
+
}
|
|
44
|
+
var iconSets = /* @__PURE__ */ new Map();
|
|
45
|
+
function isUrl(str) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(str);
|
|
48
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function getIconSet(prefix) {
|
|
54
|
+
if (iconSets.has(prefix)) return iconSets.get(prefix);
|
|
55
|
+
try {
|
|
56
|
+
const iconsPath = require2.resolve(`@iconify-json/${prefix}/icons.json`);
|
|
57
|
+
const iconsData = JSON.parse(fs.readFileSync(iconsPath, "utf-8"));
|
|
58
|
+
const set = new Set(Object.keys(iconsData.icons));
|
|
59
|
+
iconSets.set(prefix, set);
|
|
60
|
+
return set;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`Failed to load icon set for prefix "${prefix}":`, error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function validateIcon(icon, currentPath) {
|
|
67
|
+
if (icon === void 0 || icon === null) return;
|
|
68
|
+
if (typeof icon !== "string") {
|
|
69
|
+
throwConfigError("Icon must be a string.", currentPath);
|
|
70
|
+
}
|
|
71
|
+
if (isUrl(icon)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (icon.includes(":")) {
|
|
75
|
+
const parts = icon.split(":");
|
|
76
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
77
|
+
throwConfigError(
|
|
78
|
+
`Invalid library icon format: "${icon}". Icons must follow the "library-prefix:name" format (e.g., "lucide:home") or be a local path.`,
|
|
79
|
+
currentPath
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const [prefix, name] = parts;
|
|
83
|
+
const icons = getIconSet(prefix);
|
|
84
|
+
if (icons) {
|
|
85
|
+
if (!icons.has(name)) {
|
|
86
|
+
throwConfigError(
|
|
87
|
+
`Invalid icon name: "${name}" for library "${prefix}". Is this a typo?`,
|
|
88
|
+
currentPath
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
throwConfigError(
|
|
93
|
+
`Invalid icon library: "${prefix}". Is this package installed in @iconify-json?`,
|
|
94
|
+
currentPath
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const localRelativePath = icon.startsWith("/") ? icon.slice(1) : icon;
|
|
100
|
+
const localPath = path.join(DOCS_DIR, localRelativePath);
|
|
101
|
+
if (!fs.existsSync(localPath)) {
|
|
102
|
+
throwConfigError(
|
|
103
|
+
`Icon not found: "${icon}". Local icons must exist in your repository. Did you mean to use an library icon like "lucide:home"?`,
|
|
104
|
+
currentPath
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
var AVAILABLE_COMPONENTS = [
|
|
109
|
+
"Callout",
|
|
110
|
+
"Tabs",
|
|
111
|
+
"Tab",
|
|
112
|
+
"Steps",
|
|
113
|
+
"Step",
|
|
114
|
+
"Accordion",
|
|
115
|
+
"AccordionGroup",
|
|
116
|
+
"Card",
|
|
117
|
+
"Column",
|
|
118
|
+
"Columns",
|
|
119
|
+
"Image",
|
|
120
|
+
"CodeGroup",
|
|
121
|
+
"ComponentPreview"
|
|
122
|
+
];
|
|
123
|
+
var INTERNAL_ONLY_COMPONENTS = /* @__PURE__ */ new Set(["ComponentPreview"]);
|
|
124
|
+
var BASE_COLOR_OPTIONS = [
|
|
125
|
+
"slate",
|
|
126
|
+
"gray",
|
|
127
|
+
"zinc",
|
|
128
|
+
"neutral",
|
|
129
|
+
"stone",
|
|
130
|
+
"taupe",
|
|
131
|
+
"mauve",
|
|
132
|
+
"mist",
|
|
133
|
+
"olive"
|
|
134
|
+
];
|
|
135
|
+
var DEFAULT_THEME_COLOR_LIGHT = "#171717";
|
|
136
|
+
var DEFAULT_THEME_COLOR_DARK = "#f5f5f5";
|
|
137
|
+
var throwConfigError = (message, currentPath) => {
|
|
138
|
+
const location = currentPath.length > 0 ? ` (at: ${currentPath.join(".")})` : "";
|
|
139
|
+
throw new Error(`${message}${location}
|
|
140
|
+
`);
|
|
141
|
+
};
|
|
142
|
+
function checkType(value, type, currentPath, label) {
|
|
143
|
+
if (value === void 0 || value === null) return;
|
|
144
|
+
if (type === "array") {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
throwConfigError(`${label} must be an array.`, currentPath);
|
|
147
|
+
} else if (type === "object") {
|
|
148
|
+
if (typeof value !== "object" || value === null)
|
|
149
|
+
throwConfigError(`${label} must be an object.`, currentPath);
|
|
150
|
+
} else {
|
|
151
|
+
if (typeof value !== type)
|
|
152
|
+
throwConfigError(`${label} must be a ${type}.`, currentPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function normalizeHexColor(value, currentPath, label) {
|
|
156
|
+
checkType(value, "string", currentPath, label);
|
|
157
|
+
if (typeof value !== "string") {
|
|
158
|
+
throwConfigError(`${label} must be a string.`, currentPath);
|
|
159
|
+
}
|
|
160
|
+
const trimmedValue = value.trim();
|
|
161
|
+
if (trimmedValue.length === 0) {
|
|
162
|
+
throwConfigError(`${label} cannot be empty.`, currentPath);
|
|
163
|
+
}
|
|
164
|
+
const normalizedValue = trimmedValue.startsWith("#") ? trimmedValue : `#${trimmedValue}`;
|
|
165
|
+
if (!/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(
|
|
166
|
+
normalizedValue
|
|
167
|
+
)) {
|
|
168
|
+
throwConfigError(
|
|
169
|
+
`${label} must be a valid hex color (for example: #1d4ed8).`,
|
|
170
|
+
currentPath
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return normalizedValue.toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
function normalizeThemeColorConfig(value, currentPath, label) {
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
return normalizeHexColor(value, currentPath, label);
|
|
178
|
+
}
|
|
179
|
+
checkType(value, "object", currentPath, label);
|
|
180
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
181
|
+
throwConfigError(
|
|
182
|
+
`${label} must be a string or an object with light/dark values.`,
|
|
183
|
+
currentPath
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const colorByMode = value;
|
|
187
|
+
const allowedKeys = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
188
|
+
for (const key of Object.keys(colorByMode)) {
|
|
189
|
+
if (!allowedKeys.has(key)) {
|
|
190
|
+
throwConfigError(`${label} object only supports 'light' and 'dark'.`, [
|
|
191
|
+
...currentPath,
|
|
192
|
+
key
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const light = colorByMode.light !== void 0 ? normalizeHexColor(colorByMode.light, [...currentPath, "light"], label) : void 0;
|
|
197
|
+
const dark = colorByMode.dark !== void 0 ? normalizeHexColor(colorByMode.dark, [...currentPath, "dark"], label) : void 0;
|
|
198
|
+
if (light === void 0 && dark === void 0) {
|
|
199
|
+
throwConfigError(
|
|
200
|
+
`${label} object must include 'light', 'dark', or both.`,
|
|
201
|
+
currentPath
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
...light !== void 0 ? { light } : {},
|
|
206
|
+
...dark !== void 0 ? { dark } : {}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function normalizeNavTagConfig(value, currentPath, label) {
|
|
210
|
+
if (value === void 0 || value === null) return void 0;
|
|
211
|
+
if (typeof value === "string") {
|
|
212
|
+
const trimmedText2 = value.trim();
|
|
213
|
+
if (trimmedText2.length === 0) {
|
|
214
|
+
throwConfigError(`${label} cannot be empty.`, currentPath);
|
|
215
|
+
}
|
|
216
|
+
return trimmedText2;
|
|
217
|
+
}
|
|
218
|
+
checkType(value, "object", currentPath, label);
|
|
219
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
220
|
+
throwConfigError(
|
|
221
|
+
`${label} must be a string or an object with text and optional color.`,
|
|
222
|
+
currentPath
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const tagConfig = value;
|
|
226
|
+
const allowedKeys = /* @__PURE__ */ new Set(["text", "color"]);
|
|
227
|
+
for (const key of Object.keys(tagConfig)) {
|
|
228
|
+
if (!allowedKeys.has(key)) {
|
|
229
|
+
throwConfigError(`${label} object only supports 'text' and 'color'.`, [
|
|
230
|
+
...currentPath,
|
|
231
|
+
key
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
checkType(tagConfig.text, "string", [...currentPath, "text"], `${label} text`);
|
|
236
|
+
if (typeof tagConfig.text !== "string") {
|
|
237
|
+
throwConfigError(`${label} text must be a string.`, [
|
|
238
|
+
...currentPath,
|
|
239
|
+
"text"
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
const trimmedText = tagConfig.text.trim();
|
|
243
|
+
if (trimmedText.length === 0) {
|
|
244
|
+
throwConfigError(`${label} text cannot be empty.`, [
|
|
245
|
+
...currentPath,
|
|
246
|
+
"text"
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
const color = tagConfig.color !== void 0 ? normalizeThemeColorConfig(
|
|
250
|
+
tagConfig.color,
|
|
251
|
+
[...currentPath, "color"],
|
|
252
|
+
`${label} color`
|
|
253
|
+
) : void 0;
|
|
254
|
+
return {
|
|
255
|
+
text: trimmedText,
|
|
256
|
+
...color !== void 0 ? { color } : {}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function normalizeHexColorArray(value, currentPath, label) {
|
|
260
|
+
checkType(value, "array", currentPath, label);
|
|
261
|
+
if (!Array.isArray(value)) {
|
|
262
|
+
throwConfigError(`${label} must be an array.`, currentPath);
|
|
263
|
+
}
|
|
264
|
+
const colors = value;
|
|
265
|
+
if (colors.length < 1 || colors.length > 4) {
|
|
266
|
+
throwConfigError(`${label} must include 1 to 4 colors.`, currentPath);
|
|
267
|
+
}
|
|
268
|
+
return colors.map(
|
|
269
|
+
(color, index) => normalizeHexColor(color, [...currentPath, index], `${label} ${index + 1}`)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
function normalizeSeedValue(value, currentPath, label) {
|
|
273
|
+
checkType(value, "string", currentPath, label);
|
|
274
|
+
if (typeof value !== "string") {
|
|
275
|
+
throwConfigError(`${label} must be a string.`, currentPath);
|
|
276
|
+
}
|
|
277
|
+
const trimmedValue = value.trim();
|
|
278
|
+
if (trimmedValue.length === 0) {
|
|
279
|
+
throwConfigError(`${label} cannot be empty.`, currentPath);
|
|
280
|
+
}
|
|
281
|
+
return trimmedValue;
|
|
282
|
+
}
|
|
283
|
+
function validateFileExistence(filePath, currentPath) {
|
|
284
|
+
const fullPath = path.join(DOCS_DIR, `${filePath}.mdx`);
|
|
285
|
+
if (!fs.existsSync(fullPath)) {
|
|
286
|
+
throwConfigError(
|
|
287
|
+
`Referenced file not found. Expected: ${filePath}`,
|
|
288
|
+
currentPath
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function normalizeDocsPagePath(value, currentPath, label = "Page path") {
|
|
293
|
+
checkType(value, "string", currentPath, label);
|
|
294
|
+
const trimmedPath = value.trim();
|
|
295
|
+
if (trimmedPath === "") {
|
|
296
|
+
throwConfigError(`${label} cannot be an empty string`, currentPath);
|
|
297
|
+
}
|
|
298
|
+
if (isUrl(trimmedPath)) {
|
|
299
|
+
throwConfigError(
|
|
300
|
+
`${label} must reference a documentation page path, not a URL`,
|
|
301
|
+
currentPath
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const normalizedPath = trimmedPath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
305
|
+
if (normalizedPath === "") {
|
|
306
|
+
throwConfigError(`${label} cannot be '/'`, currentPath);
|
|
307
|
+
}
|
|
308
|
+
return normalizedPath;
|
|
309
|
+
}
|
|
310
|
+
function splitHrefPathAndSuffix(href) {
|
|
311
|
+
const match = href.match(/^([^?#]*)(.*)$/);
|
|
312
|
+
return {
|
|
313
|
+
pathname: match?.[1] ?? href,
|
|
314
|
+
suffix: match?.[2] ?? ""
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function normalizeInternalPageHref(href, currentPath, label) {
|
|
318
|
+
const trimmedHref = href.trim();
|
|
319
|
+
if (trimmedHref === "") {
|
|
320
|
+
throwConfigError(`${label} cannot be an empty string`, currentPath);
|
|
321
|
+
}
|
|
322
|
+
if (isUrl(trimmedHref)) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
if (!trimmedHref.startsWith("/")) {
|
|
326
|
+
throwConfigError(
|
|
327
|
+
`${label} must be either a valid URL (http:// or https://) or an internal path (starting with /)`,
|
|
328
|
+
currentPath
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const { pathname, suffix } = splitHrefPathAndSuffix(trimmedHref);
|
|
332
|
+
const normalizedPathname = pathname.replace(/\/{2,}/g, "/");
|
|
333
|
+
if (normalizedPathname === "/" || normalizedPathname === "") {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const filePath = normalizeDocsPagePath(
|
|
337
|
+
normalizedPathname,
|
|
338
|
+
currentPath,
|
|
339
|
+
label
|
|
340
|
+
);
|
|
341
|
+
validateFileExistence(filePath, currentPath);
|
|
342
|
+
return {
|
|
343
|
+
filePath,
|
|
344
|
+
href: `/${filePath}`,
|
|
345
|
+
linkHref: `/${filePath}${suffix}`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
var openApiSpecCache = /* @__PURE__ */ new Map();
|
|
349
|
+
async function loadOpenApiSpec(filePathOrUrl) {
|
|
350
|
+
assertConfigured();
|
|
351
|
+
if (openApiSpecCache.has(filePathOrUrl)) {
|
|
352
|
+
return openApiSpecCache.get(filePathOrUrl);
|
|
353
|
+
}
|
|
354
|
+
const isUrlPath = isUrl(filePathOrUrl);
|
|
355
|
+
let fileContent;
|
|
356
|
+
if (isUrlPath) {
|
|
357
|
+
try {
|
|
358
|
+
const response = await fetch(filePathOrUrl);
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
fileContent = await response.text();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Failed to fetch OpenAPI spec from URL: ${error instanceof Error ? error.message : String(error)}`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
const fullPath = path.join(DOCS_DIR, filePathOrUrl);
|
|
372
|
+
fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
373
|
+
}
|
|
374
|
+
const trimmedContent = fileContent.trim();
|
|
375
|
+
if (trimmedContent.startsWith("<!DOCTYPE") || trimmedContent.startsWith("<html")) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"The URL does not return a valid OpenAPI specification. The URL appears to return HTML instead of JSON or YAML."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
let parsedSpec;
|
|
381
|
+
try {
|
|
382
|
+
if (filePathOrUrl.endsWith(".json") || isUrlPath && filePathOrUrl.includes(".json")) {
|
|
383
|
+
parsedSpec = JSON.parse(fileContent);
|
|
384
|
+
} else {
|
|
385
|
+
const yaml2 = await import("yaml");
|
|
386
|
+
parsedSpec = yaml2.parse(fileContent);
|
|
387
|
+
}
|
|
388
|
+
} catch (parseError) {
|
|
389
|
+
if (parseError instanceof SyntaxError) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`The URL does not return a valid OpenAPI specification. Failed to parse as JSON or YAML: ${parseError.message}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
throw parseError;
|
|
395
|
+
}
|
|
396
|
+
openApiSpecCache.set(filePathOrUrl, parsedSpec);
|
|
397
|
+
return parsedSpec;
|
|
398
|
+
}
|
|
399
|
+
async function validateOpenApiFile(filePathOrUrl, currentPath) {
|
|
400
|
+
const isUrlPath = isUrl(filePathOrUrl);
|
|
401
|
+
if (!isUrlPath) {
|
|
402
|
+
const validExtensions = [".json", ".yaml", ".yml"];
|
|
403
|
+
const hasValidExtension = validExtensions.some(
|
|
404
|
+
(ext) => filePathOrUrl.toLowerCase().endsWith(ext)
|
|
405
|
+
);
|
|
406
|
+
if (!hasValidExtension) {
|
|
407
|
+
throwConfigError(
|
|
408
|
+
`OpenAPI file must have a valid extension (.json, .yaml, or .yml). Found: ${filePathOrUrl}`,
|
|
409
|
+
currentPath
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const fullPath = path.join(DOCS_DIR, filePathOrUrl);
|
|
413
|
+
if (!fs.existsSync(fullPath)) {
|
|
414
|
+
throwConfigError(
|
|
415
|
+
`Referenced OpenAPI file not found. Expected: ${filePathOrUrl}`,
|
|
416
|
+
currentPath
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
try {
|
|
421
|
+
const url = new URL(filePathOrUrl);
|
|
422
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
423
|
+
throwConfigError(
|
|
424
|
+
`OpenAPI URL must use http:// or https:// protocol. Found: ${filePathOrUrl}`,
|
|
425
|
+
currentPath
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
throwConfigError(
|
|
430
|
+
`Invalid OpenAPI URL format: ${filePathOrUrl}`,
|
|
431
|
+
currentPath
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const document = await loadOpenApiSpec(filePathOrUrl);
|
|
437
|
+
const tolerantRuleset = {
|
|
438
|
+
...oas,
|
|
439
|
+
rules: {
|
|
440
|
+
...oas.rules,
|
|
441
|
+
"oas3-schema": {
|
|
442
|
+
...oas.rules["oas3-schema"],
|
|
443
|
+
severity: 1
|
|
444
|
+
},
|
|
445
|
+
"oas2-schema": {
|
|
446
|
+
...oas.rules["oas2-schema"],
|
|
447
|
+
severity: 1
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const spectral = new Spectral();
|
|
452
|
+
spectral.setRuleset(tolerantRuleset);
|
|
453
|
+
let results = await spectral.run(document);
|
|
454
|
+
const blockingResults = results.filter(
|
|
455
|
+
(result) => result.code === "unrecognized-format"
|
|
456
|
+
);
|
|
457
|
+
if (blockingResults.length > 0) {
|
|
458
|
+
const errorMessages = blockingResults.slice(0, 5).map((result) => {
|
|
459
|
+
const pathStr = result.path.length > 0 ? result.path.join(".") : "root";
|
|
460
|
+
return `${result.message} (at ${pathStr})`;
|
|
461
|
+
});
|
|
462
|
+
const errorText = errorMessages.join("; ");
|
|
463
|
+
const moreErrors = blockingResults.length > 5 ? ` (and ${blockingResults.length - 5} more errors)` : "";
|
|
464
|
+
throwConfigError(
|
|
465
|
+
`Invalid OpenAPI specification: ${errorText}${moreErrors}`,
|
|
466
|
+
currentPath
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const nonBlockingResults = results.filter(
|
|
470
|
+
(result) => result.severity !== 0
|
|
471
|
+
);
|
|
472
|
+
if (nonBlockingResults.length > 0) {
|
|
473
|
+
const warningMessages = nonBlockingResults.slice(0, 5).map((result) => {
|
|
474
|
+
const pathStr = result.path.length > 0 ? result.path.join(".") : "root";
|
|
475
|
+
return `${result.message} (at ${pathStr})`;
|
|
476
|
+
});
|
|
477
|
+
const warningText = warningMessages.join("; ");
|
|
478
|
+
const moreWarnings = nonBlockingResults.length > warningMessages.length ? ` (and ${nonBlockingResults.length - warningMessages.length} more warnings)` : "";
|
|
479
|
+
const sourcePath = currentPath.length > 0 ? ` (at: ${currentPath.join(".")})` : "";
|
|
480
|
+
console.warn(
|
|
481
|
+
`[OPENAPI_VALIDATION_WARNING] ${warningText}${moreWarnings}${sourcePath}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error instanceof SyntaxError) {
|
|
486
|
+
throwConfigError(
|
|
487
|
+
`Failed to parse OpenAPI file: ${error.message}`,
|
|
488
|
+
currentPath
|
|
489
|
+
);
|
|
490
|
+
} else if (error instanceof Error) {
|
|
491
|
+
const baseMessage = error.message || String(error);
|
|
492
|
+
const prefixedMessage = baseMessage.startsWith(
|
|
493
|
+
"Invalid OpenAPI specification:"
|
|
494
|
+
) ? baseMessage : `Invalid OpenAPI specification: ${baseMessage}`;
|
|
495
|
+
throwConfigError(prefixedMessage, currentPath);
|
|
496
|
+
} else {
|
|
497
|
+
throwConfigError(
|
|
498
|
+
`Invalid OpenAPI specification: ${String(error)}`,
|
|
499
|
+
currentPath
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function extractAvailableEndpoints(openApiDoc) {
|
|
505
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
506
|
+
const paths = openApiDoc.paths || {};
|
|
507
|
+
const httpMethods = [
|
|
508
|
+
"get",
|
|
509
|
+
"post",
|
|
510
|
+
"put",
|
|
511
|
+
"delete",
|
|
512
|
+
"patch",
|
|
513
|
+
"head",
|
|
514
|
+
"options",
|
|
515
|
+
"trace"
|
|
516
|
+
];
|
|
517
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
518
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
519
|
+
for (const method of httpMethods) {
|
|
520
|
+
const operation = pathItem[method];
|
|
521
|
+
if (operation) {
|
|
522
|
+
const normalizedMethod = method.toUpperCase();
|
|
523
|
+
const normalizedPath = pathStr.toLowerCase();
|
|
524
|
+
endpoints.add(`${normalizedMethod} ${normalizedPath}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return endpoints;
|
|
529
|
+
}
|
|
530
|
+
function parseEndpointString(endpointStr) {
|
|
531
|
+
const trimmed = endpointStr.trim();
|
|
532
|
+
const parts = trimmed.split(/\s+/);
|
|
533
|
+
if (parts.length !== 2) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
const method = parts[0].toUpperCase();
|
|
537
|
+
let path2 = parts[1];
|
|
538
|
+
if (!path2.startsWith("/")) {
|
|
539
|
+
path2 = "/" + path2;
|
|
540
|
+
}
|
|
541
|
+
const normalizedPath = path2.toLowerCase();
|
|
542
|
+
return { method, path: normalizedPath };
|
|
543
|
+
}
|
|
544
|
+
async function validateNavOpenApiPage(navOpenApiPage, currentPath) {
|
|
545
|
+
checkType(navOpenApiPage, "object", currentPath, "Open API page");
|
|
546
|
+
if (typeof navOpenApiPage.source !== "string") {
|
|
547
|
+
throwConfigError(
|
|
548
|
+
"Open API page must include a 'source' property that is a string.",
|
|
549
|
+
[...currentPath, "source"]
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (typeof navOpenApiPage.endpoint !== "string") {
|
|
553
|
+
throwConfigError(
|
|
554
|
+
`Open API page must include an 'endpoint' property that is a string in the format "METHOD /path".`,
|
|
555
|
+
[...currentPath, "endpoint"]
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const parsedEndpoint = parseEndpointString(navOpenApiPage.endpoint);
|
|
559
|
+
if (!parsedEndpoint) {
|
|
560
|
+
throwConfigError(
|
|
561
|
+
`Open API page endpoint must be in the format "METHOD /path". Found: ${navOpenApiPage.endpoint}`,
|
|
562
|
+
[...currentPath, "endpoint"]
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
await validateOpenApiFile(navOpenApiPage.source, [...currentPath, "source"]);
|
|
566
|
+
const openApiDoc = await loadOpenApiSpec(navOpenApiPage.source);
|
|
567
|
+
const availableEndpoints = extractAvailableEndpoints(openApiDoc);
|
|
568
|
+
const endpointKey = `${parsedEndpoint.method} ${parsedEndpoint.path}`;
|
|
569
|
+
if (!availableEndpoints.has(endpointKey)) {
|
|
570
|
+
throwConfigError(
|
|
571
|
+
`Open API page endpoint does not match any endpoint in the OpenAPI spec. Found: ${navOpenApiPage.endpoint}. Expected format: "METHOD /path".`,
|
|
572
|
+
[...currentPath, "endpoint"]
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
577
|
+
if (typeof item === "string") {
|
|
578
|
+
const normalizedPath = normalizeDocsPagePath(item, currentPath);
|
|
579
|
+
validateFileExistence(normalizedPath, currentPath);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
checkType(item, "object", currentPath, "Navigation item");
|
|
583
|
+
const isGroup = "group" in item;
|
|
584
|
+
const isPage = "page" in item;
|
|
585
|
+
const isOpenApiPage = "openapi" in item;
|
|
586
|
+
const typeCount = [isGroup, isPage, isOpenApiPage].filter(Boolean).length;
|
|
587
|
+
if (typeCount !== 1) {
|
|
588
|
+
throwConfigError(
|
|
589
|
+
"Object must contain exactly one key: 'page', 'group', or 'openapi'.",
|
|
590
|
+
currentPath
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (isGroup) {
|
|
594
|
+
const path2 = [...currentPath];
|
|
595
|
+
if (groupDepth >= 2) {
|
|
596
|
+
throwConfigError("Groups can only be nested up to 2 levels deep.", path2);
|
|
597
|
+
}
|
|
598
|
+
checkType(item.group, "string", [...path2, "group"], "Group name");
|
|
599
|
+
checkType(item.expanded, "boolean", [...path2, "expanded"], "Expanded");
|
|
600
|
+
validateIcon(item.icon, [...path2, "icon"]);
|
|
601
|
+
item.tag = normalizeNavTagConfig(item.tag, [...path2, "tag"], "Group tag");
|
|
602
|
+
if (!item.pages)
|
|
603
|
+
throwConfigError("Group must have a 'pages' array.", [...path2, "pages"]);
|
|
604
|
+
checkType(item.pages, "array", [...path2, "pages"], "Group pages");
|
|
605
|
+
for (const [i, child] of item.pages.entries()) {
|
|
606
|
+
if (typeof child === "string") {
|
|
607
|
+
const childPath = [...path2, "pages", i];
|
|
608
|
+
const normalizedPagePath = normalizeDocsPagePath(child, childPath);
|
|
609
|
+
item.pages[i] = normalizedPagePath;
|
|
610
|
+
validateFileExistence(normalizedPagePath, childPath);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
await validateNavigationNode(
|
|
614
|
+
child,
|
|
615
|
+
[...path2, "pages", i],
|
|
616
|
+
groupDepth + 1
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (isPage) {
|
|
622
|
+
const path2 = [...currentPath];
|
|
623
|
+
const normalizedPagePath = normalizeDocsPagePath(item.page, [
|
|
624
|
+
...path2,
|
|
625
|
+
"page"
|
|
626
|
+
]);
|
|
627
|
+
item.page = normalizedPagePath;
|
|
628
|
+
validateFileExistence(normalizedPagePath, [...path2, "page"]);
|
|
629
|
+
validateIcon(item.icon, [...path2, "icon"]);
|
|
630
|
+
checkType(item.title, "string", [...path2, "title"], "Page title");
|
|
631
|
+
item.tag = normalizeNavTagConfig(item.tag, [...path2, "tag"], "Page tag");
|
|
632
|
+
if ("expanded" in item)
|
|
633
|
+
throwConfigError("Page items cannot have 'expanded'.", [
|
|
634
|
+
...path2,
|
|
635
|
+
"expanded"
|
|
636
|
+
]);
|
|
637
|
+
if ("pages" in item)
|
|
638
|
+
throwConfigError("Page items cannot have children.", [...path2, "pages"]);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (isOpenApiPage) {
|
|
642
|
+
const path2 = [...currentPath];
|
|
643
|
+
if ("icon" in item) {
|
|
644
|
+
throwConfigError(
|
|
645
|
+
"Open API page items cannot have an 'icon'. Method badges are displayed automatically.",
|
|
646
|
+
[...path2, "icon"]
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
await validateNavOpenApiPage(item.openapi, [...path2, "openapi"]);
|
|
650
|
+
checkType(item.title, "string", [...path2, "title"], "Open API page title");
|
|
651
|
+
item.tag = normalizeNavTagConfig(
|
|
652
|
+
item.tag,
|
|
653
|
+
[...path2, "tag"],
|
|
654
|
+
"Open API page tag"
|
|
655
|
+
);
|
|
656
|
+
if ("expanded" in item)
|
|
657
|
+
throwConfigError("Open API page items cannot have 'expanded'.", [
|
|
658
|
+
...path2,
|
|
659
|
+
"expanded"
|
|
660
|
+
]);
|
|
661
|
+
if ("pages" in item)
|
|
662
|
+
throwConfigError("Open API page items cannot have children.", [
|
|
663
|
+
...path2,
|
|
664
|
+
"pages"
|
|
665
|
+
]);
|
|
666
|
+
if ("group" in item)
|
|
667
|
+
throwConfigError("Open API page items cannot have 'group'.", [
|
|
668
|
+
...path2,
|
|
669
|
+
"group"
|
|
670
|
+
]);
|
|
671
|
+
if ("page" in item)
|
|
672
|
+
throwConfigError("Open API page items cannot have 'page'.", [
|
|
673
|
+
...path2,
|
|
674
|
+
"page"
|
|
675
|
+
]);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function getFirstPagePathFromPageItems(items) {
|
|
680
|
+
for (const item of items) {
|
|
681
|
+
if (typeof item === "string") {
|
|
682
|
+
return item;
|
|
683
|
+
}
|
|
684
|
+
if ("page" in item) {
|
|
685
|
+
return item.page;
|
|
686
|
+
}
|
|
687
|
+
if ("group" in item) {
|
|
688
|
+
const nestedPath = getFirstPagePathFromPageItems(item.pages);
|
|
689
|
+
if (nestedPath) {
|
|
690
|
+
return nestedPath;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
function getFirstPagePathFromNavigation(navigation) {
|
|
697
|
+
if (navigation.pages) {
|
|
698
|
+
return getFirstPagePathFromPageItems(navigation.pages);
|
|
699
|
+
}
|
|
700
|
+
if (navigation.menu) {
|
|
701
|
+
for (const menuItem of navigation.menu.items) {
|
|
702
|
+
const submenuPages = menuItem.submenu.pages;
|
|
703
|
+
if (!submenuPages) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const firstPath = getFirstPagePathFromPageItems(submenuPages);
|
|
707
|
+
if (firstPath) {
|
|
708
|
+
return firstPath;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return void 0;
|
|
713
|
+
}
|
|
714
|
+
async function validateNavOpenApi(navOpenApi, currentPath) {
|
|
715
|
+
checkType(navOpenApi, "object", currentPath, "Open API object");
|
|
716
|
+
if (typeof navOpenApi.source !== "string") {
|
|
717
|
+
throwConfigError(
|
|
718
|
+
"Open API object must have an 'source' property that is a string.",
|
|
719
|
+
[...currentPath, "source"]
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
await validateOpenApiFile(navOpenApi.source, [...currentPath, "source"]);
|
|
723
|
+
const hasInclude = "include" in navOpenApi;
|
|
724
|
+
const hasExclude = "exclude" in navOpenApi;
|
|
725
|
+
if (hasInclude && hasExclude) {
|
|
726
|
+
throwConfigError(
|
|
727
|
+
"Open API object cannot have both 'include' and 'exclude' properties. They are mutually exclusive.",
|
|
728
|
+
currentPath
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (!hasInclude && !hasExclude) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const openApiDoc = await loadOpenApiSpec(navOpenApi.source);
|
|
735
|
+
const availableEndpoints = extractAvailableEndpoints(openApiDoc);
|
|
736
|
+
if (hasInclude) {
|
|
737
|
+
checkType(
|
|
738
|
+
navOpenApi.include,
|
|
739
|
+
"array",
|
|
740
|
+
[...currentPath, "include"],
|
|
741
|
+
"Include array"
|
|
742
|
+
);
|
|
743
|
+
if (navOpenApi.include.length === 0) {
|
|
744
|
+
throwConfigError("Include array cannot be empty.", [
|
|
745
|
+
...currentPath,
|
|
746
|
+
"include"
|
|
747
|
+
]);
|
|
748
|
+
}
|
|
749
|
+
for (const [i, entry] of navOpenApi.include.entries()) {
|
|
750
|
+
if (typeof entry !== "string") {
|
|
751
|
+
throwConfigError(
|
|
752
|
+
`Include entry at index ${i} must be a string in the format "METHOD /path".`,
|
|
753
|
+
[...currentPath, "include", i]
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
const parsed = parseEndpointString(entry);
|
|
757
|
+
if (!parsed) {
|
|
758
|
+
throwConfigError(
|
|
759
|
+
`Include entry at index ${i} must be in the format "METHOD /path". Found: ${entry}`,
|
|
760
|
+
[...currentPath, "include", i]
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
const endpointKey = `${parsed?.method} ${parsed?.path}`;
|
|
764
|
+
if (!availableEndpoints.has(endpointKey)) {
|
|
765
|
+
throwConfigError(
|
|
766
|
+
`Include entry at index ${i} does not match any endpoint in the OpenAPI spec. Found: ${entry}. Expected format: "METHOD /path".`,
|
|
767
|
+
[...currentPath, "include", i]
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (hasExclude) {
|
|
773
|
+
checkType(
|
|
774
|
+
navOpenApi.exclude,
|
|
775
|
+
"array",
|
|
776
|
+
[...currentPath, "exclude"],
|
|
777
|
+
"Exclude array"
|
|
778
|
+
);
|
|
779
|
+
if (navOpenApi.exclude.length === 0) {
|
|
780
|
+
throwConfigError("Exclude array cannot be empty.", [
|
|
781
|
+
...currentPath,
|
|
782
|
+
"exclude"
|
|
783
|
+
]);
|
|
784
|
+
}
|
|
785
|
+
for (const [i, entry] of navOpenApi.exclude.entries()) {
|
|
786
|
+
if (typeof entry !== "string") {
|
|
787
|
+
throwConfigError(
|
|
788
|
+
`Exclude entry at index ${i} must be a string in the format "METHOD /path".`,
|
|
789
|
+
[...currentPath, "exclude", i]
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
const parsed = parseEndpointString(entry);
|
|
793
|
+
if (!parsed) {
|
|
794
|
+
throwConfigError(
|
|
795
|
+
`Exclude entry at index ${i} must be in the format "METHOD /path" (e.g., "get /burgers"). Found: ${entry}`,
|
|
796
|
+
[...currentPath, "exclude", i]
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const endpointKey = `${parsed?.method} ${parsed?.path}`;
|
|
800
|
+
if (!availableEndpoints.has(endpointKey)) {
|
|
801
|
+
throwConfigError(
|
|
802
|
+
`Exclude entry at index ${i} does not match any endpoint in the OpenAPI spec. Found: ${entry}. Expected format: "METHOD /path" (case-insensitive).`,
|
|
803
|
+
[...currentPath, "exclude", i]
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
async function validateNavMenuItem(item, currentPath) {
|
|
810
|
+
checkType(item, "object", currentPath, "Menu item");
|
|
811
|
+
validateIcon(item.icon, [...currentPath, "icon"]);
|
|
812
|
+
if (!item.label) {
|
|
813
|
+
throwConfigError("Menu item must have a 'label' property.", [
|
|
814
|
+
...currentPath,
|
|
815
|
+
"label"
|
|
816
|
+
]);
|
|
817
|
+
}
|
|
818
|
+
checkType(item.label, "string", [...currentPath, "label"], "Label");
|
|
819
|
+
if (!item.submenu) {
|
|
820
|
+
throwConfigError("Menu item must have a 'submenu' property.", [
|
|
821
|
+
...currentPath,
|
|
822
|
+
"submenu"
|
|
823
|
+
]);
|
|
824
|
+
}
|
|
825
|
+
checkType(item.submenu, "object", [...currentPath, "submenu"], "Submenu");
|
|
826
|
+
const submenu = item.submenu;
|
|
827
|
+
const submenuKeys = Object.keys(submenu);
|
|
828
|
+
const validSubmenuKeys = ["pages", "openapi"];
|
|
829
|
+
const presentSubmenuKeys = submenuKeys.filter(
|
|
830
|
+
(key) => validSubmenuKeys.includes(key)
|
|
831
|
+
);
|
|
832
|
+
const invalidSubmenuKeys = submenuKeys.filter(
|
|
833
|
+
(key) => !validSubmenuKeys.includes(key)
|
|
834
|
+
);
|
|
835
|
+
if (submenuKeys.length !== 1) {
|
|
836
|
+
if (submenuKeys.length === 0) {
|
|
837
|
+
throwConfigError(
|
|
838
|
+
`Submenu must contain exactly one key (${validSubmenuKeys.join(
|
|
839
|
+
", "
|
|
840
|
+
)}). Found no keys.`,
|
|
841
|
+
[...currentPath, "submenu"]
|
|
842
|
+
);
|
|
843
|
+
} else {
|
|
844
|
+
throwConfigError(
|
|
845
|
+
`Submenu must contain exactly one key (${validSubmenuKeys.join(
|
|
846
|
+
", "
|
|
847
|
+
)}). Found ${submenuKeys.length} key(s): ${submenuKeys.join(", ")}.`,
|
|
848
|
+
[...currentPath, "submenu"]
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (presentSubmenuKeys.length !== 1) {
|
|
853
|
+
const invalidKey = invalidSubmenuKeys[0];
|
|
854
|
+
throwConfigError(
|
|
855
|
+
`Submenu must contain exactly one key (${validSubmenuKeys.join(
|
|
856
|
+
", "
|
|
857
|
+
)}). Found invalid key: ${invalidKey}.`,
|
|
858
|
+
[...currentPath, "submenu"]
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
const submenuKey = presentSubmenuKeys[0];
|
|
862
|
+
const submenuValue = submenu[submenuKey];
|
|
863
|
+
if (submenuKey === "pages") {
|
|
864
|
+
checkType(
|
|
865
|
+
submenuValue,
|
|
866
|
+
"array",
|
|
867
|
+
[...currentPath, "submenu", "pages"],
|
|
868
|
+
"Submenu pages"
|
|
869
|
+
);
|
|
870
|
+
const pages = submenuValue ?? [];
|
|
871
|
+
for (const [i, item2] of pages.entries()) {
|
|
872
|
+
const itemPath = [...currentPath, "submenu", "pages", i];
|
|
873
|
+
if (typeof item2 === "string") {
|
|
874
|
+
const normalizedPagePath = normalizeDocsPagePath(item2, itemPath);
|
|
875
|
+
submenuValue[i] = normalizedPagePath;
|
|
876
|
+
validateFileExistence(normalizedPagePath, itemPath);
|
|
877
|
+
} else {
|
|
878
|
+
await validateNavigationNode(item2, itemPath);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (submenuKey === "openapi") {
|
|
883
|
+
if (typeof submenuValue === "string") {
|
|
884
|
+
await validateOpenApiFile(submenuValue, [
|
|
885
|
+
...currentPath,
|
|
886
|
+
"submenu",
|
|
887
|
+
"openapi"
|
|
888
|
+
]);
|
|
889
|
+
} else if (typeof submenuValue === "object") {
|
|
890
|
+
await validateNavOpenApi(submenuValue, [
|
|
891
|
+
...currentPath,
|
|
892
|
+
"submenu",
|
|
893
|
+
"openapi"
|
|
894
|
+
]);
|
|
895
|
+
} else {
|
|
896
|
+
throwConfigError(
|
|
897
|
+
"OpenAPI must be either a string (file path or hosted file) or an object.",
|
|
898
|
+
[...currentPath, "submenu", "openapi"]
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async function validateNavMenu(menu, currentPath) {
|
|
904
|
+
checkType(menu, "object", currentPath, "Menu");
|
|
905
|
+
if (menu.type !== void 0) {
|
|
906
|
+
if (menu.type !== "dropdown" && menu.type !== "segmented") {
|
|
907
|
+
throwConfigError(
|
|
908
|
+
"Menu type must be 'dropdown' or 'segmented' if provided. Defaults to 'dropdown'",
|
|
909
|
+
[...currentPath, "type"]
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
checkType(menu.label, "string", [...currentPath, "label"], "Menu label");
|
|
914
|
+
if (!menu.items) {
|
|
915
|
+
throwConfigError("Menu must have an 'items' array.", [
|
|
916
|
+
...currentPath,
|
|
917
|
+
"items"
|
|
918
|
+
]);
|
|
919
|
+
}
|
|
920
|
+
checkType(menu.items, "array", [...currentPath, "items"], "Menu items");
|
|
921
|
+
for (const [i, item] of menu.items.entries()) {
|
|
922
|
+
await validateNavMenuItem(item, [...currentPath, "items", i]);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function validateNavbarItem(item, currentPath) {
|
|
926
|
+
if (item === void 0) return null;
|
|
927
|
+
checkType(item, "object", currentPath, "Navbar item");
|
|
928
|
+
if (typeof item.text !== "string") {
|
|
929
|
+
throwConfigError("Navbar item must have a 'text' property.", [
|
|
930
|
+
...currentPath,
|
|
931
|
+
"text"
|
|
932
|
+
]);
|
|
933
|
+
}
|
|
934
|
+
if (typeof item.href !== "string") {
|
|
935
|
+
throwConfigError("Navbar item must have an 'href' property.", [
|
|
936
|
+
...currentPath,
|
|
937
|
+
"href"
|
|
938
|
+
]);
|
|
939
|
+
}
|
|
940
|
+
const hiddenPageRoute = normalizeInternalPageHref(
|
|
941
|
+
item.href,
|
|
942
|
+
[...currentPath, "href"],
|
|
943
|
+
"Navbar item href"
|
|
944
|
+
);
|
|
945
|
+
if (hiddenPageRoute) {
|
|
946
|
+
item.href = hiddenPageRoute.linkHref;
|
|
947
|
+
}
|
|
948
|
+
validateIcon(item.icon, [...currentPath, "icon"]);
|
|
949
|
+
if (item.color !== void 0) {
|
|
950
|
+
if (currentPath[0] !== "navbar" || currentPath[1] !== "primary") {
|
|
951
|
+
throwConfigError(
|
|
952
|
+
"Navbar item color is only supported on navbar.primary.",
|
|
953
|
+
[...currentPath, "color"]
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
item.color = normalizeThemeColorConfig(
|
|
957
|
+
item.color,
|
|
958
|
+
[...currentPath, "color"],
|
|
959
|
+
"Navbar primary color"
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
return hiddenPageRoute;
|
|
963
|
+
}
|
|
964
|
+
function validateTitle(title) {
|
|
965
|
+
checkType(title, "string", ["title"], "Title");
|
|
966
|
+
if (!title) throwConfigError("Title is missing.", ["title"]);
|
|
967
|
+
}
|
|
968
|
+
function validateLogoPaddingValue(value, currentPath, label) {
|
|
969
|
+
if (value === void 0) return;
|
|
970
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
971
|
+
throwConfigError(`${label} must be a finite number.`, currentPath);
|
|
972
|
+
}
|
|
973
|
+
const numericValue = value;
|
|
974
|
+
if (numericValue < 0) {
|
|
975
|
+
throwConfigError(`${label} cannot be negative.`, currentPath);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function validateLogoImagePath(imagePath, currentPath, label) {
|
|
979
|
+
const validExtensions = [".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif"];
|
|
980
|
+
const hasValidExtension = validExtensions.some(
|
|
981
|
+
(ext) => imagePath.toLowerCase().endsWith(ext)
|
|
982
|
+
);
|
|
983
|
+
if (!hasValidExtension) {
|
|
984
|
+
throwConfigError(
|
|
985
|
+
`${label} must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif)`,
|
|
986
|
+
currentPath
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const normalizedPath = imagePath.startsWith("/") ? imagePath.slice(1) : imagePath;
|
|
990
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
991
|
+
if (!fs.existsSync(fullPath)) {
|
|
992
|
+
throwConfigError(
|
|
993
|
+
`${label} file not found. Expected: ${normalizedPath}`,
|
|
994
|
+
currentPath
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function validateAssistantIconSource(iconSource, currentPath) {
|
|
999
|
+
checkType(iconSource, "string", currentPath, "Assistant icon source");
|
|
1000
|
+
if (typeof iconSource !== "string") {
|
|
1001
|
+
throwConfigError("Assistant icon source must be a string.", currentPath);
|
|
1002
|
+
}
|
|
1003
|
+
const trimmedSource = iconSource.trim();
|
|
1004
|
+
if (trimmedSource.length === 0) {
|
|
1005
|
+
throwConfigError("Assistant icon source cannot be empty.", currentPath);
|
|
1006
|
+
}
|
|
1007
|
+
if (isUrl(trimmedSource)) {
|
|
1008
|
+
throwConfigError(
|
|
1009
|
+
"Assistant icon source must be a local image path relative to docs.json.",
|
|
1010
|
+
currentPath
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
if (trimmedSource.includes(":")) {
|
|
1014
|
+
throwConfigError(
|
|
1015
|
+
`Invalid assistant icon source: "${trimmedSource}". Assistant icons must be local image files, not Iconify icon names.`,
|
|
1016
|
+
currentPath
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (trimmedSource.startsWith("//") || trimmedSource.startsWith("#") || trimmedSource.startsWith("?") || trimmedSource.startsWith("./") || trimmedSource.startsWith("../")) {
|
|
1020
|
+
throwConfigError(
|
|
1021
|
+
"Assistant icon source must be a local image path relative to docs.json.",
|
|
1022
|
+
currentPath
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const parsed = new URL(trimmedSource, "https://docs.invalid/");
|
|
1026
|
+
const validExtensions = [
|
|
1027
|
+
".svg",
|
|
1028
|
+
".png",
|
|
1029
|
+
".jpg",
|
|
1030
|
+
".jpeg",
|
|
1031
|
+
".webp",
|
|
1032
|
+
".gif",
|
|
1033
|
+
".ico",
|
|
1034
|
+
".avif"
|
|
1035
|
+
];
|
|
1036
|
+
const hasValidExtension = validExtensions.some(
|
|
1037
|
+
(ext) => parsed.pathname.toLowerCase().endsWith(ext)
|
|
1038
|
+
);
|
|
1039
|
+
if (!hasValidExtension) {
|
|
1040
|
+
throwConfigError(
|
|
1041
|
+
"Assistant icon source must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif, .ico, .avif).",
|
|
1042
|
+
currentPath
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
const normalizedPath = parsed.pathname.replace(/^\/+/, "");
|
|
1046
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1047
|
+
if (!fs.existsSync(fullPath)) {
|
|
1048
|
+
throwConfigError(
|
|
1049
|
+
`Assistant icon source file not found. Expected: ${normalizedPath}`,
|
|
1050
|
+
currentPath
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
return trimmedSource;
|
|
1054
|
+
}
|
|
1055
|
+
function validateLogoVariant(variant, currentPath, mode) {
|
|
1056
|
+
if (variant === void 0) return;
|
|
1057
|
+
if (typeof variant === "string") {
|
|
1058
|
+
validateLogoImagePath(variant, currentPath, `Logo ${mode}`);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (typeof variant !== "object" || variant === null || Array.isArray(variant)) {
|
|
1062
|
+
throwConfigError(
|
|
1063
|
+
`Logo ${mode} must be a string path or an object with 'image' and optional 'padding'.`,
|
|
1064
|
+
currentPath
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const supportedKeys = /* @__PURE__ */ new Set(["image", "padding"]);
|
|
1068
|
+
for (const key of Object.keys(variant)) {
|
|
1069
|
+
if (!supportedKeys.has(key)) {
|
|
1070
|
+
throwConfigError(
|
|
1071
|
+
`Logo ${mode} object only supports 'image' and 'padding'.`,
|
|
1072
|
+
[...currentPath, key]
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (typeof variant.image !== "string") {
|
|
1077
|
+
throwConfigError(`Logo ${mode} object must include an 'image' string.`, [
|
|
1078
|
+
...currentPath,
|
|
1079
|
+
"image"
|
|
1080
|
+
]);
|
|
1081
|
+
}
|
|
1082
|
+
validateLogoImagePath(
|
|
1083
|
+
variant.image,
|
|
1084
|
+
[...currentPath, "image"],
|
|
1085
|
+
`Logo ${mode} image`
|
|
1086
|
+
);
|
|
1087
|
+
if (variant.padding === void 0) return;
|
|
1088
|
+
if (typeof variant.padding !== "object" || variant.padding === null || Array.isArray(variant.padding)) {
|
|
1089
|
+
throwConfigError(
|
|
1090
|
+
`Logo ${mode} padding must be an object with optional 'top' and 'bottom'.`,
|
|
1091
|
+
[...currentPath, "padding"]
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
const paddingKeys = Object.keys(variant.padding);
|
|
1095
|
+
for (const key of paddingKeys) {
|
|
1096
|
+
if (key !== "top" && key !== "bottom") {
|
|
1097
|
+
throwConfigError(
|
|
1098
|
+
`Logo ${mode} padding only supports 'top' and 'bottom'.`,
|
|
1099
|
+
[...currentPath, "padding", key]
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
validateLogoPaddingValue(
|
|
1104
|
+
variant.padding.top,
|
|
1105
|
+
[...currentPath, "padding", "top"],
|
|
1106
|
+
`Logo ${mode} padding top`
|
|
1107
|
+
);
|
|
1108
|
+
validateLogoPaddingValue(
|
|
1109
|
+
variant.padding.bottom,
|
|
1110
|
+
[...currentPath, "padding", "bottom"],
|
|
1111
|
+
`Logo ${mode} padding bottom`
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
function validateLogo(logo) {
|
|
1115
|
+
if (logo === void 0) return;
|
|
1116
|
+
checkType(logo, "object", ["logo"], "Logo configuration");
|
|
1117
|
+
validateLogoVariant(logo.light, ["logo", "light"], "light");
|
|
1118
|
+
validateLogoVariant(logo.dark, ["logo", "dark"], "dark");
|
|
1119
|
+
if (logo.href !== void 0) {
|
|
1120
|
+
checkType(logo.href, "string", ["logo", "href"], "Logo href");
|
|
1121
|
+
const trimmedHref = logo.href.trim();
|
|
1122
|
+
if (trimmedHref === "") {
|
|
1123
|
+
throwConfigError("Logo href cannot be an empty string", ["logo", "href"]);
|
|
1124
|
+
}
|
|
1125
|
+
const isUrl2 = trimmedHref.startsWith("http://") || trimmedHref.startsWith("https://");
|
|
1126
|
+
const isInternalPath = trimmedHref.startsWith("/");
|
|
1127
|
+
if (!isUrl2 && !isInternalPath) {
|
|
1128
|
+
throwConfigError(
|
|
1129
|
+
"Logo href must be either a valid URL (http:// or https://) or an internal path (starting with /)",
|
|
1130
|
+
["logo", "href"]
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (logo.pill !== void 0) {
|
|
1135
|
+
if (typeof logo.pill === "string") {
|
|
1136
|
+
if (logo.pill.trim() === "") {
|
|
1137
|
+
throwConfigError(
|
|
1138
|
+
"Logo pill text cannot be an empty string. Use false to hide the pill.",
|
|
1139
|
+
["logo", "pill"]
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
} else if (logo.pill !== false) {
|
|
1143
|
+
throwConfigError("Logo pill must be a string or false.", [
|
|
1144
|
+
"logo",
|
|
1145
|
+
"pill"
|
|
1146
|
+
]);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function validateTheme(theme) {
|
|
1151
|
+
if (theme === void 0) return;
|
|
1152
|
+
checkType(theme, "object", ["theme"], "Theme configuration");
|
|
1153
|
+
if (typeof theme !== "object" || theme === null || Array.isArray(theme)) {
|
|
1154
|
+
throwConfigError("Theme configuration must be an object.", ["theme"]);
|
|
1155
|
+
}
|
|
1156
|
+
const normalizeBaseColor = (value, currentPath, label) => {
|
|
1157
|
+
checkType(value, "string", currentPath, label);
|
|
1158
|
+
if (typeof value !== "string") {
|
|
1159
|
+
throwConfigError(`${label} must be a string.`, currentPath);
|
|
1160
|
+
}
|
|
1161
|
+
const normalizedBaseColor = value.trim().toLowerCase();
|
|
1162
|
+
if (normalizedBaseColor.length === 0) {
|
|
1163
|
+
throwConfigError(`${label} cannot be empty.`, currentPath);
|
|
1164
|
+
}
|
|
1165
|
+
if (!BASE_COLOR_OPTIONS.includes(normalizedBaseColor)) {
|
|
1166
|
+
throwConfigError(
|
|
1167
|
+
`${label} must be one of: ${BASE_COLOR_OPTIONS.join(", ")}.`,
|
|
1168
|
+
currentPath
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return normalizedBaseColor;
|
|
1172
|
+
};
|
|
1173
|
+
if (theme.baseColor !== void 0) {
|
|
1174
|
+
if (typeof theme.baseColor === "string") {
|
|
1175
|
+
theme.baseColor = normalizeBaseColor(
|
|
1176
|
+
theme.baseColor,
|
|
1177
|
+
["theme", "baseColor"],
|
|
1178
|
+
"Theme base color"
|
|
1179
|
+
);
|
|
1180
|
+
} else {
|
|
1181
|
+
checkType(
|
|
1182
|
+
theme.baseColor,
|
|
1183
|
+
"object",
|
|
1184
|
+
["theme", "baseColor"],
|
|
1185
|
+
"Theme base color"
|
|
1186
|
+
);
|
|
1187
|
+
if (typeof theme.baseColor !== "object" || theme.baseColor === null || Array.isArray(theme.baseColor)) {
|
|
1188
|
+
throwConfigError(
|
|
1189
|
+
"Theme base color must be a string or an object with light/dark values.",
|
|
1190
|
+
["theme", "baseColor"]
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
const baseColorByMode = theme.baseColor;
|
|
1194
|
+
const allowedKeys2 = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
1195
|
+
for (const key of Object.keys(baseColorByMode)) {
|
|
1196
|
+
if (!allowedKeys2.has(key)) {
|
|
1197
|
+
throwConfigError(
|
|
1198
|
+
"Theme base color object only supports 'light' and 'dark'.",
|
|
1199
|
+
["theme", "baseColor", key]
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const light2 = baseColorByMode.light !== void 0 ? normalizeBaseColor(
|
|
1204
|
+
baseColorByMode.light,
|
|
1205
|
+
["theme", "baseColor", "light"],
|
|
1206
|
+
"Theme base color light"
|
|
1207
|
+
) : void 0;
|
|
1208
|
+
const dark2 = baseColorByMode.dark !== void 0 ? normalizeBaseColor(
|
|
1209
|
+
baseColorByMode.dark,
|
|
1210
|
+
["theme", "baseColor", "dark"],
|
|
1211
|
+
"Theme base color dark"
|
|
1212
|
+
) : void 0;
|
|
1213
|
+
if (!light2 && !dark2) {
|
|
1214
|
+
throwConfigError(
|
|
1215
|
+
"Theme base color object must include 'light', 'dark', or both.",
|
|
1216
|
+
["theme", "baseColor"]
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const resolvedLight = light2 ?? "neutral";
|
|
1220
|
+
const resolvedDark = dark2 ?? "neutral";
|
|
1221
|
+
theme.baseColor = {
|
|
1222
|
+
light: resolvedLight,
|
|
1223
|
+
dark: resolvedDark
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const normalizeShikiThemeName = (value, currentPath, label) => {
|
|
1228
|
+
checkType(value, "string", currentPath, label);
|
|
1229
|
+
if (typeof value !== "string") {
|
|
1230
|
+
throwConfigError(`${label} must be a string.`, currentPath);
|
|
1231
|
+
}
|
|
1232
|
+
const normalizedThemeName = value.trim().toLowerCase();
|
|
1233
|
+
if (normalizedThemeName.length === 0) {
|
|
1234
|
+
throwConfigError(`${label} cannot be empty.`, currentPath);
|
|
1235
|
+
}
|
|
1236
|
+
if (!isBundledShikiThemeName(normalizedThemeName)) {
|
|
1237
|
+
throwConfigError(
|
|
1238
|
+
`${label} must be a bundled Shiki theme name. Supported themes include: ${SHIKI_BUNDLED_THEME_NAMES.join(", ")}.`,
|
|
1239
|
+
currentPath
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return normalizedThemeName;
|
|
1243
|
+
};
|
|
1244
|
+
if (theme.code !== void 0) {
|
|
1245
|
+
checkType(theme.code, "object", ["theme", "code"], "Theme code");
|
|
1246
|
+
if (typeof theme.code !== "object" || theme.code === null || Array.isArray(theme.code)) {
|
|
1247
|
+
throwConfigError("Theme code must be an object.", ["theme", "code"]);
|
|
1248
|
+
}
|
|
1249
|
+
const codeTheme = theme.code;
|
|
1250
|
+
const allowedCodeKeys = /* @__PURE__ */ new Set(["syntaxTheme"]);
|
|
1251
|
+
for (const key of Object.keys(codeTheme)) {
|
|
1252
|
+
if (!allowedCodeKeys.has(key)) {
|
|
1253
|
+
throwConfigError(
|
|
1254
|
+
"Theme code configuration only supports 'syntaxTheme'.",
|
|
1255
|
+
["theme", "code", key]
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (codeTheme.syntaxTheme !== void 0) {
|
|
1260
|
+
if (typeof codeTheme.syntaxTheme === "string") {
|
|
1261
|
+
const themeName = normalizeShikiThemeName(
|
|
1262
|
+
codeTheme.syntaxTheme,
|
|
1263
|
+
["theme", "code", "syntaxTheme"],
|
|
1264
|
+
"Theme code syntax theme"
|
|
1265
|
+
);
|
|
1266
|
+
codeTheme.syntaxTheme = {
|
|
1267
|
+
light: themeName,
|
|
1268
|
+
dark: themeName
|
|
1269
|
+
};
|
|
1270
|
+
} else {
|
|
1271
|
+
checkType(
|
|
1272
|
+
codeTheme.syntaxTheme,
|
|
1273
|
+
"object",
|
|
1274
|
+
["theme", "code", "syntaxTheme"],
|
|
1275
|
+
"Theme code syntax theme"
|
|
1276
|
+
);
|
|
1277
|
+
if (typeof codeTheme.syntaxTheme !== "object" || codeTheme.syntaxTheme === null || Array.isArray(codeTheme.syntaxTheme)) {
|
|
1278
|
+
throwConfigError(
|
|
1279
|
+
"Theme code syntax theme must be a string or an object with light/dark values.",
|
|
1280
|
+
["theme", "code", "syntaxTheme"]
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const syntaxThemeByMode = codeTheme.syntaxTheme;
|
|
1284
|
+
const allowedSyntaxThemeKeys = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
1285
|
+
for (const key of Object.keys(syntaxThemeByMode)) {
|
|
1286
|
+
if (!allowedSyntaxThemeKeys.has(key)) {
|
|
1287
|
+
throwConfigError(
|
|
1288
|
+
"Theme code syntax theme object only supports 'light' and 'dark'.",
|
|
1289
|
+
["theme", "code", "syntaxTheme", key]
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const light2 = syntaxThemeByMode.light !== void 0 ? normalizeShikiThemeName(
|
|
1294
|
+
syntaxThemeByMode.light,
|
|
1295
|
+
["theme", "code", "syntaxTheme", "light"],
|
|
1296
|
+
"Theme code syntax theme light"
|
|
1297
|
+
) : void 0;
|
|
1298
|
+
const dark2 = syntaxThemeByMode.dark !== void 0 ? normalizeShikiThemeName(
|
|
1299
|
+
syntaxThemeByMode.dark,
|
|
1300
|
+
["theme", "code", "syntaxTheme", "dark"],
|
|
1301
|
+
"Theme code syntax theme dark"
|
|
1302
|
+
) : void 0;
|
|
1303
|
+
if (!light2 && !dark2) {
|
|
1304
|
+
throwConfigError(
|
|
1305
|
+
"Theme code syntax theme object must include 'light', 'dark', or both.",
|
|
1306
|
+
["theme", "code", "syntaxTheme"]
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
codeTheme.syntaxTheme = {
|
|
1310
|
+
light: light2 ?? DEFAULT_SHIKI_LIGHT_THEME,
|
|
1311
|
+
dark: dark2 ?? DEFAULT_SHIKI_DARK_THEME
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (theme.tag !== void 0) {
|
|
1317
|
+
checkType(theme.tag, "object", ["theme", "tag"], "Theme tag");
|
|
1318
|
+
if (typeof theme.tag !== "object" || theme.tag === null || Array.isArray(theme.tag)) {
|
|
1319
|
+
throwConfigError("Theme tag must be an object.", ["theme", "tag"]);
|
|
1320
|
+
}
|
|
1321
|
+
const tagTheme = theme.tag;
|
|
1322
|
+
const allowedTagKeys = /* @__PURE__ */ new Set(["color"]);
|
|
1323
|
+
for (const key of Object.keys(tagTheme)) {
|
|
1324
|
+
if (!allowedTagKeys.has(key)) {
|
|
1325
|
+
throwConfigError("Theme tag configuration only supports 'color'.", [
|
|
1326
|
+
"theme",
|
|
1327
|
+
"tag",
|
|
1328
|
+
key
|
|
1329
|
+
]);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (tagTheme.color !== void 0) {
|
|
1333
|
+
tagTheme.color = normalizeThemeColorConfig(
|
|
1334
|
+
tagTheme.color,
|
|
1335
|
+
["theme", "tag", "color"],
|
|
1336
|
+
"Theme tag color"
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (theme.card !== void 0) {
|
|
1341
|
+
checkType(theme.card, "object", ["theme", "card"], "Theme card");
|
|
1342
|
+
if (typeof theme.card !== "object" || theme.card === null || Array.isArray(theme.card)) {
|
|
1343
|
+
throwConfigError("Theme card must be an object.", ["theme", "card"]);
|
|
1344
|
+
}
|
|
1345
|
+
const cardTheme = theme.card;
|
|
1346
|
+
const allowedCardKeys = /* @__PURE__ */ new Set(["cover", "button"]);
|
|
1347
|
+
for (const key of Object.keys(cardTheme)) {
|
|
1348
|
+
if (!allowedCardKeys.has(key)) {
|
|
1349
|
+
throwConfigError(
|
|
1350
|
+
"Theme card configuration only supports 'cover' and 'button'.",
|
|
1351
|
+
["theme", "card", key]
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (cardTheme.cover !== void 0) {
|
|
1356
|
+
checkType(
|
|
1357
|
+
cardTheme.cover,
|
|
1358
|
+
"object",
|
|
1359
|
+
["theme", "card", "cover"],
|
|
1360
|
+
"Theme card cover"
|
|
1361
|
+
);
|
|
1362
|
+
if (typeof cardTheme.cover !== "object" || cardTheme.cover === null || Array.isArray(cardTheme.cover)) {
|
|
1363
|
+
throwConfigError("Theme card cover must be an object.", [
|
|
1364
|
+
"theme",
|
|
1365
|
+
"card",
|
|
1366
|
+
"cover"
|
|
1367
|
+
]);
|
|
1368
|
+
}
|
|
1369
|
+
const coverTheme = cardTheme.cover;
|
|
1370
|
+
const allowedCoverKeys = /* @__PURE__ */ new Set(["colors", "colorSeed"]);
|
|
1371
|
+
for (const key of Object.keys(coverTheme)) {
|
|
1372
|
+
if (!allowedCoverKeys.has(key)) {
|
|
1373
|
+
throwConfigError(
|
|
1374
|
+
"Theme card cover configuration only supports 'colors' and 'colorSeed'.",
|
|
1375
|
+
["theme", "card", "cover", key]
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (coverTheme.colors !== void 0) {
|
|
1380
|
+
coverTheme.colors = normalizeHexColorArray(
|
|
1381
|
+
coverTheme.colors,
|
|
1382
|
+
["theme", "card", "cover", "colors"],
|
|
1383
|
+
"Theme card cover colors"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (coverTheme.colorSeed !== void 0) {
|
|
1387
|
+
coverTheme.colorSeed = normalizeSeedValue(
|
|
1388
|
+
coverTheme.colorSeed,
|
|
1389
|
+
["theme", "card", "cover", "colorSeed"],
|
|
1390
|
+
"Theme card cover color seed"
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (cardTheme.button !== void 0) {
|
|
1395
|
+
checkType(
|
|
1396
|
+
cardTheme.button,
|
|
1397
|
+
"object",
|
|
1398
|
+
["theme", "card", "button"],
|
|
1399
|
+
"Theme card button"
|
|
1400
|
+
);
|
|
1401
|
+
if (typeof cardTheme.button !== "object" || cardTheme.button === null || Array.isArray(cardTheme.button)) {
|
|
1402
|
+
throwConfigError("Theme card button must be an object.", [
|
|
1403
|
+
"theme",
|
|
1404
|
+
"card",
|
|
1405
|
+
"button"
|
|
1406
|
+
]);
|
|
1407
|
+
}
|
|
1408
|
+
const buttonTheme = cardTheme.button;
|
|
1409
|
+
const allowedButtonKeys = /* @__PURE__ */ new Set(["color"]);
|
|
1410
|
+
for (const key of Object.keys(buttonTheme)) {
|
|
1411
|
+
if (!allowedButtonKeys.has(key)) {
|
|
1412
|
+
throwConfigError(
|
|
1413
|
+
"Theme card button configuration only supports 'color'.",
|
|
1414
|
+
["theme", "card", "button", key]
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (buttonTheme.color !== void 0) {
|
|
1419
|
+
buttonTheme.color = normalizeThemeColorConfig(
|
|
1420
|
+
buttonTheme.color,
|
|
1421
|
+
["theme", "card", "button", "color"],
|
|
1422
|
+
"Theme card button color"
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (theme.themeColor === void 0) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (typeof theme.themeColor === "string") {
|
|
1431
|
+
theme.themeColor = normalizeHexColor(
|
|
1432
|
+
theme.themeColor,
|
|
1433
|
+
["theme", "themeColor"],
|
|
1434
|
+
"Theme color"
|
|
1435
|
+
);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
checkType(theme.themeColor, "object", ["theme", "themeColor"], "Theme color");
|
|
1439
|
+
if (typeof theme.themeColor !== "object" || theme.themeColor === null || Array.isArray(theme.themeColor)) {
|
|
1440
|
+
throwConfigError(
|
|
1441
|
+
"Theme color must be a string or an object with light/dark values.",
|
|
1442
|
+
["theme", "themeColor"]
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
const themeColorByMode = theme.themeColor;
|
|
1446
|
+
const allowedKeys = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
1447
|
+
for (const key of Object.keys(themeColorByMode)) {
|
|
1448
|
+
if (!allowedKeys.has(key)) {
|
|
1449
|
+
throwConfigError("Theme color object only supports 'light' and 'dark'.", [
|
|
1450
|
+
"theme",
|
|
1451
|
+
"themeColor",
|
|
1452
|
+
key
|
|
1453
|
+
]);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const light = themeColorByMode.light !== void 0 ? normalizeHexColor(
|
|
1457
|
+
themeColorByMode.light,
|
|
1458
|
+
["theme", "themeColor", "light"],
|
|
1459
|
+
"Theme color light"
|
|
1460
|
+
) : void 0;
|
|
1461
|
+
const dark = themeColorByMode.dark !== void 0 ? normalizeHexColor(
|
|
1462
|
+
themeColorByMode.dark,
|
|
1463
|
+
["theme", "themeColor", "dark"],
|
|
1464
|
+
"Theme color dark"
|
|
1465
|
+
) : void 0;
|
|
1466
|
+
if (!light && !dark) {
|
|
1467
|
+
throwConfigError(
|
|
1468
|
+
"Theme color object must include 'light', 'dark', or both.",
|
|
1469
|
+
["theme", "themeColor"]
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
theme.themeColor = {
|
|
1473
|
+
...light !== void 0 ? { light } : {},
|
|
1474
|
+
...dark !== void 0 ? { dark } : {}
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function validateAssistant(assistant) {
|
|
1478
|
+
if (assistant === void 0) return;
|
|
1479
|
+
checkType(assistant, "object", ["assistant"], "Assistant configuration");
|
|
1480
|
+
if (typeof assistant !== "object" || assistant === null || Array.isArray(assistant)) {
|
|
1481
|
+
throwConfigError("Assistant configuration must be an object.", [
|
|
1482
|
+
"assistant"
|
|
1483
|
+
]);
|
|
1484
|
+
}
|
|
1485
|
+
const allowedAssistantKeys = /* @__PURE__ */ new Set([
|
|
1486
|
+
"button",
|
|
1487
|
+
"navbarButton",
|
|
1488
|
+
"heading",
|
|
1489
|
+
"questions",
|
|
1490
|
+
"icon"
|
|
1491
|
+
]);
|
|
1492
|
+
for (const key of Object.keys(assistant)) {
|
|
1493
|
+
if (!allowedAssistantKeys.has(key)) {
|
|
1494
|
+
throwConfigError(
|
|
1495
|
+
"Assistant configuration only supports 'button', 'navbarButton', 'heading', 'questions', and 'icon'.",
|
|
1496
|
+
["assistant", key]
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (assistant.button !== void 0) {
|
|
1501
|
+
checkType(
|
|
1502
|
+
assistant.button,
|
|
1503
|
+
"object",
|
|
1504
|
+
["assistant", "button"],
|
|
1505
|
+
"Assistant button configuration"
|
|
1506
|
+
);
|
|
1507
|
+
if (typeof assistant.button !== "object" || assistant.button === null || Array.isArray(assistant.button)) {
|
|
1508
|
+
throwConfigError("Assistant button configuration must be an object.", [
|
|
1509
|
+
"assistant",
|
|
1510
|
+
"button"
|
|
1511
|
+
]);
|
|
1512
|
+
}
|
|
1513
|
+
const allowedButtonKeys = /* @__PURE__ */ new Set(["size", "color"]);
|
|
1514
|
+
for (const key of Object.keys(assistant.button)) {
|
|
1515
|
+
if (!allowedButtonKeys.has(key)) {
|
|
1516
|
+
throwConfigError(
|
|
1517
|
+
"Assistant button configuration only supports 'size' and 'color'.",
|
|
1518
|
+
["assistant", "button", key]
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (assistant.button.size !== void 0) {
|
|
1523
|
+
checkType(
|
|
1524
|
+
assistant.button.size,
|
|
1525
|
+
"string",
|
|
1526
|
+
["assistant", "button", "size"],
|
|
1527
|
+
"Assistant button size"
|
|
1528
|
+
);
|
|
1529
|
+
if (typeof assistant.button.size !== "string") {
|
|
1530
|
+
throwConfigError("Assistant button size must be a string.", [
|
|
1531
|
+
"assistant",
|
|
1532
|
+
"button",
|
|
1533
|
+
"size"
|
|
1534
|
+
]);
|
|
1535
|
+
}
|
|
1536
|
+
const trimmedSize = assistant.button.size.trim();
|
|
1537
|
+
if (trimmedSize !== "small" && trimmedSize !== "default") {
|
|
1538
|
+
throwConfigError(
|
|
1539
|
+
"Assistant button size must be either 'small' or 'default'.",
|
|
1540
|
+
["assistant", "button", "size"]
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
assistant.button.size = trimmedSize;
|
|
1544
|
+
}
|
|
1545
|
+
if (assistant.button.color !== void 0) {
|
|
1546
|
+
assistant.button.color = normalizeThemeColorConfig(
|
|
1547
|
+
assistant.button.color,
|
|
1548
|
+
["assistant", "button", "color"],
|
|
1549
|
+
"Assistant button color"
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (assistant.navbarButton !== void 0) {
|
|
1554
|
+
checkType(
|
|
1555
|
+
assistant.navbarButton,
|
|
1556
|
+
"object",
|
|
1557
|
+
["assistant", "navbarButton"],
|
|
1558
|
+
"Assistant navbar button configuration"
|
|
1559
|
+
);
|
|
1560
|
+
if (typeof assistant.navbarButton !== "object" || assistant.navbarButton === null || Array.isArray(assistant.navbarButton)) {
|
|
1561
|
+
throwConfigError(
|
|
1562
|
+
"Assistant navbar button configuration must be an object.",
|
|
1563
|
+
["assistant", "navbarButton"]
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
const allowedNavbarButtonKeys = /* @__PURE__ */ new Set(["enabled", "text", "color"]);
|
|
1567
|
+
for (const key of Object.keys(assistant.navbarButton)) {
|
|
1568
|
+
if (!allowedNavbarButtonKeys.has(key)) {
|
|
1569
|
+
throwConfigError(
|
|
1570
|
+
"Assistant navbar button configuration only supports 'enabled', 'text', and 'color'.",
|
|
1571
|
+
["assistant", "navbarButton", key]
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (assistant.navbarButton.enabled !== void 0) {
|
|
1576
|
+
checkType(
|
|
1577
|
+
assistant.navbarButton.enabled,
|
|
1578
|
+
"boolean",
|
|
1579
|
+
["assistant", "navbarButton", "enabled"],
|
|
1580
|
+
"Assistant navbar button enabled"
|
|
1581
|
+
);
|
|
1582
|
+
if (typeof assistant.navbarButton.enabled !== "boolean") {
|
|
1583
|
+
throwConfigError(
|
|
1584
|
+
"Assistant navbar button enabled must be a boolean.",
|
|
1585
|
+
["assistant", "navbarButton", "enabled"]
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (assistant.navbarButton.text !== void 0) {
|
|
1590
|
+
checkType(
|
|
1591
|
+
assistant.navbarButton.text,
|
|
1592
|
+
"string",
|
|
1593
|
+
["assistant", "navbarButton", "text"],
|
|
1594
|
+
"Assistant navbar button text"
|
|
1595
|
+
);
|
|
1596
|
+
if (typeof assistant.navbarButton.text !== "string") {
|
|
1597
|
+
throwConfigError("Assistant navbar button text must be a string.", [
|
|
1598
|
+
"assistant",
|
|
1599
|
+
"navbarButton",
|
|
1600
|
+
"text"
|
|
1601
|
+
]);
|
|
1602
|
+
}
|
|
1603
|
+
const trimmedText = assistant.navbarButton.text.trim();
|
|
1604
|
+
if (trimmedText.length === 0) {
|
|
1605
|
+
throwConfigError("Assistant navbar button text cannot be empty.", [
|
|
1606
|
+
"assistant",
|
|
1607
|
+
"navbarButton",
|
|
1608
|
+
"text"
|
|
1609
|
+
]);
|
|
1610
|
+
}
|
|
1611
|
+
assistant.navbarButton.text = trimmedText;
|
|
1612
|
+
}
|
|
1613
|
+
if (assistant.navbarButton.color !== void 0) {
|
|
1614
|
+
assistant.navbarButton.color = normalizeThemeColorConfig(
|
|
1615
|
+
assistant.navbarButton.color,
|
|
1616
|
+
["assistant", "navbarButton", "color"],
|
|
1617
|
+
"Assistant navbar button color"
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (assistant.heading !== void 0) {
|
|
1622
|
+
checkType(
|
|
1623
|
+
assistant.heading,
|
|
1624
|
+
"string",
|
|
1625
|
+
["assistant", "heading"],
|
|
1626
|
+
"Assistant heading"
|
|
1627
|
+
);
|
|
1628
|
+
if (typeof assistant.heading !== "string") {
|
|
1629
|
+
throwConfigError("Assistant heading must be a string.", [
|
|
1630
|
+
"assistant",
|
|
1631
|
+
"heading"
|
|
1632
|
+
]);
|
|
1633
|
+
}
|
|
1634
|
+
const trimmedHeading = assistant.heading.trim();
|
|
1635
|
+
if (trimmedHeading.length === 0) {
|
|
1636
|
+
throwConfigError("Assistant heading cannot be empty.", [
|
|
1637
|
+
"assistant",
|
|
1638
|
+
"heading"
|
|
1639
|
+
]);
|
|
1640
|
+
}
|
|
1641
|
+
assistant.heading = trimmedHeading;
|
|
1642
|
+
}
|
|
1643
|
+
if (assistant.questions !== void 0) {
|
|
1644
|
+
checkType(
|
|
1645
|
+
assistant.questions,
|
|
1646
|
+
"array",
|
|
1647
|
+
["assistant", "questions"],
|
|
1648
|
+
"Assistant questions"
|
|
1649
|
+
);
|
|
1650
|
+
if (!Array.isArray(assistant.questions)) {
|
|
1651
|
+
throwConfigError("Assistant questions must be an array.", [
|
|
1652
|
+
"assistant",
|
|
1653
|
+
"questions"
|
|
1654
|
+
]);
|
|
1655
|
+
}
|
|
1656
|
+
if (assistant.questions.length > 3) {
|
|
1657
|
+
throwConfigError("Assistant questions can include at most 3 questions.", [
|
|
1658
|
+
"assistant",
|
|
1659
|
+
"questions"
|
|
1660
|
+
]);
|
|
1661
|
+
}
|
|
1662
|
+
assistant.questions = assistant.questions.map((question, index) => {
|
|
1663
|
+
checkType(
|
|
1664
|
+
question,
|
|
1665
|
+
"string",
|
|
1666
|
+
["assistant", "questions", String(index)],
|
|
1667
|
+
"Assistant question"
|
|
1668
|
+
);
|
|
1669
|
+
if (typeof question !== "string") {
|
|
1670
|
+
throwConfigError("Assistant question must be a string.", [
|
|
1671
|
+
"assistant",
|
|
1672
|
+
"questions",
|
|
1673
|
+
String(index)
|
|
1674
|
+
]);
|
|
1675
|
+
}
|
|
1676
|
+
const trimmedQuestion = question.trim();
|
|
1677
|
+
if (trimmedQuestion.length === 0) {
|
|
1678
|
+
throwConfigError(
|
|
1679
|
+
"Assistant question cannot be empty.",
|
|
1680
|
+
["assistant", "questions", String(index)]
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
return trimmedQuestion;
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
if (assistant.icon === void 0) return;
|
|
1687
|
+
checkType(
|
|
1688
|
+
assistant.icon,
|
|
1689
|
+
"object",
|
|
1690
|
+
["assistant", "icon"],
|
|
1691
|
+
"Assistant icon"
|
|
1692
|
+
);
|
|
1693
|
+
if (typeof assistant.icon !== "object" || assistant.icon === null || Array.isArray(assistant.icon)) {
|
|
1694
|
+
throwConfigError("Assistant icon must be an object.", [
|
|
1695
|
+
"assistant",
|
|
1696
|
+
"icon"
|
|
1697
|
+
]);
|
|
1698
|
+
}
|
|
1699
|
+
const allowedIconKeys = /* @__PURE__ */ new Set(["src"]);
|
|
1700
|
+
for (const key of Object.keys(assistant.icon)) {
|
|
1701
|
+
if (!allowedIconKeys.has(key)) {
|
|
1702
|
+
throwConfigError(
|
|
1703
|
+
"Assistant icon only supports 'src'.",
|
|
1704
|
+
["assistant", "icon", key]
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (assistant.icon.src === void 0) {
|
|
1709
|
+
throwConfigError(
|
|
1710
|
+
"Assistant icon must include 'src'.",
|
|
1711
|
+
["assistant", "icon"]
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
if (assistant.icon.src !== void 0) {
|
|
1715
|
+
assistant.icon.src = validateAssistantIconSource(
|
|
1716
|
+
assistant.icon.src,
|
|
1717
|
+
["assistant", "icon", "src"]
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function validateHome(home) {
|
|
1722
|
+
if (home === void 0) return void 0;
|
|
1723
|
+
const normalizedHome = normalizeDocsPagePath(home, ["home"], "Home path");
|
|
1724
|
+
validateFileExistence(normalizedHome, ["home"]);
|
|
1725
|
+
return normalizedHome;
|
|
1726
|
+
}
|
|
1727
|
+
function validateNavbar(navbar) {
|
|
1728
|
+
const hiddenPageRoutes = [];
|
|
1729
|
+
if (navbar === void 0) return hiddenPageRoutes;
|
|
1730
|
+
checkType(navbar, "object", ["navbar"], "Navbar configuration");
|
|
1731
|
+
checkType(navbar.blur, "boolean", ["navbar", "blur"], "Navbar blur setting");
|
|
1732
|
+
const primaryPageRoute = validateNavbarItem(navbar.primary, [
|
|
1733
|
+
"navbar",
|
|
1734
|
+
"primary"
|
|
1735
|
+
]);
|
|
1736
|
+
if (primaryPageRoute) hiddenPageRoutes.push(primaryPageRoute);
|
|
1737
|
+
const secondaryPageRoute = validateNavbarItem(navbar.secondary, [
|
|
1738
|
+
"navbar",
|
|
1739
|
+
"secondary"
|
|
1740
|
+
]);
|
|
1741
|
+
if (secondaryPageRoute) hiddenPageRoutes.push(secondaryPageRoute);
|
|
1742
|
+
if (navbar.links !== void 0) {
|
|
1743
|
+
checkType(navbar.links, "array", ["navbar", "links"], "Navbar links");
|
|
1744
|
+
if (navbar.links.length > 3) {
|
|
1745
|
+
throwConfigError("Navbar links cannot have more than 3 items.", [
|
|
1746
|
+
"navbar",
|
|
1747
|
+
"links"
|
|
1748
|
+
]);
|
|
1749
|
+
}
|
|
1750
|
+
navbar.links.forEach((link, i) => {
|
|
1751
|
+
const hiddenPageRoute = validateNavbarItem(link, ["navbar", "links", i]);
|
|
1752
|
+
if (hiddenPageRoute) hiddenPageRoutes.push(hiddenPageRoute);
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
return hiddenPageRoutes;
|
|
1756
|
+
}
|
|
1757
|
+
function validateFooter(footer) {
|
|
1758
|
+
const hiddenPageRoutes = [];
|
|
1759
|
+
if (footer === void 0) return hiddenPageRoutes;
|
|
1760
|
+
checkType(footer, "object", ["footer"], "Footer configuration");
|
|
1761
|
+
if (footer.socials !== void 0) {
|
|
1762
|
+
checkType(
|
|
1763
|
+
footer.socials,
|
|
1764
|
+
"object",
|
|
1765
|
+
["footer", "socials"],
|
|
1766
|
+
"Footer socials"
|
|
1767
|
+
);
|
|
1768
|
+
const validSocials = [
|
|
1769
|
+
"x",
|
|
1770
|
+
"website",
|
|
1771
|
+
"facebook",
|
|
1772
|
+
"youtube",
|
|
1773
|
+
"discord",
|
|
1774
|
+
"slack",
|
|
1775
|
+
"github",
|
|
1776
|
+
"linkedin",
|
|
1777
|
+
"instagram",
|
|
1778
|
+
"hacker-news",
|
|
1779
|
+
"medium",
|
|
1780
|
+
"telegram",
|
|
1781
|
+
"bluesky",
|
|
1782
|
+
"threads",
|
|
1783
|
+
"reddit",
|
|
1784
|
+
"podcast"
|
|
1785
|
+
];
|
|
1786
|
+
for (const [key, value] of Object.entries(footer.socials)) {
|
|
1787
|
+
if (!validSocials.includes(key)) {
|
|
1788
|
+
throwConfigError(
|
|
1789
|
+
`Invalid social platform: ${key}. Valid options are: ${validSocials.join(
|
|
1790
|
+
", "
|
|
1791
|
+
)}`,
|
|
1792
|
+
["footer", "socials", key]
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
checkType(
|
|
1796
|
+
value,
|
|
1797
|
+
"string",
|
|
1798
|
+
["footer", "socials", key],
|
|
1799
|
+
`Social link for ${key}`
|
|
1800
|
+
);
|
|
1801
|
+
if (!isUrl(value)) {
|
|
1802
|
+
throwConfigError(`Social link for ${key} must be a valid URL.`, [
|
|
1803
|
+
"footer",
|
|
1804
|
+
"socials",
|
|
1805
|
+
key
|
|
1806
|
+
]);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (footer.links !== void 0) {
|
|
1811
|
+
checkType(footer.links, "array", ["footer", "links"], "Footer links");
|
|
1812
|
+
footer.links.forEach((link, i) => {
|
|
1813
|
+
checkType(link, "object", ["footer", "links", i], "Footer link");
|
|
1814
|
+
if (typeof link.text !== "string") {
|
|
1815
|
+
throwConfigError("Footer link must have a 'text' property.", [
|
|
1816
|
+
"footer",
|
|
1817
|
+
"links",
|
|
1818
|
+
i,
|
|
1819
|
+
"text"
|
|
1820
|
+
]);
|
|
1821
|
+
}
|
|
1822
|
+
if (typeof link.href !== "string") {
|
|
1823
|
+
throwConfigError("Footer link must have an 'href' property.", [
|
|
1824
|
+
"footer",
|
|
1825
|
+
"links",
|
|
1826
|
+
i,
|
|
1827
|
+
"href"
|
|
1828
|
+
]);
|
|
1829
|
+
}
|
|
1830
|
+
const hiddenPageRoute = normalizeInternalPageHref(
|
|
1831
|
+
link.href,
|
|
1832
|
+
["footer", "links", i, "href"],
|
|
1833
|
+
"Footer link href"
|
|
1834
|
+
);
|
|
1835
|
+
if (hiddenPageRoute) {
|
|
1836
|
+
link.href = hiddenPageRoute.linkHref;
|
|
1837
|
+
hiddenPageRoutes.push(hiddenPageRoute);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
return hiddenPageRoutes;
|
|
1842
|
+
}
|
|
1843
|
+
async function validateNavigation(navigation) {
|
|
1844
|
+
checkType(navigation, "object", ["navigation"], "Navigation");
|
|
1845
|
+
const keys = Object.keys(navigation);
|
|
1846
|
+
const validKeys = ["pages", "menu", "openapi"];
|
|
1847
|
+
const navKeys = keys.filter((key) => validKeys.includes(key));
|
|
1848
|
+
if (navKeys.length !== 1) {
|
|
1849
|
+
throwConfigError(
|
|
1850
|
+
`Navigation must contain exactly one top-level item (${validKeys.join(
|
|
1851
|
+
", "
|
|
1852
|
+
)}). Found ${navKeys.length}.`,
|
|
1853
|
+
["navigation"]
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
const navKey = navKeys[0];
|
|
1857
|
+
const navValue = navigation[navKey];
|
|
1858
|
+
if (navKey === "menu") {
|
|
1859
|
+
await validateNavMenu(navValue, ["navigation", "menu"]);
|
|
1860
|
+
} else {
|
|
1861
|
+
checkType(
|
|
1862
|
+
navValue,
|
|
1863
|
+
"array",
|
|
1864
|
+
["navigation", navKey],
|
|
1865
|
+
`Navigation container '${navKey}'`
|
|
1866
|
+
);
|
|
1867
|
+
for (const [i, item] of navValue.entries()) {
|
|
1868
|
+
const itemPath = ["navigation", navKey, i];
|
|
1869
|
+
if (typeof item === "string") {
|
|
1870
|
+
const normalizedPagePath = normalizeDocsPagePath(item, itemPath);
|
|
1871
|
+
navValue[i] = normalizedPagePath;
|
|
1872
|
+
validateFileExistence(normalizedPagePath, itemPath);
|
|
1873
|
+
} else {
|
|
1874
|
+
await validateNavigationNode(item, itemPath);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
async function validateConfig(config) {
|
|
1880
|
+
validateTitle(config.title);
|
|
1881
|
+
validateLogo(config.logo);
|
|
1882
|
+
validateTheme(config.theme);
|
|
1883
|
+
validateAssistant(config.assistant);
|
|
1884
|
+
await validateNavigation(config.navigation);
|
|
1885
|
+
config.home = validateHome(config.home);
|
|
1886
|
+
if (config.home === void 0) {
|
|
1887
|
+
const fallbackHome = getFirstPagePathFromNavigation(config.navigation);
|
|
1888
|
+
if (!fallbackHome) {
|
|
1889
|
+
throwConfigError(
|
|
1890
|
+
"Home is undefined and no documentation page exists in navigation to use as fallback.",
|
|
1891
|
+
["home"]
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
config.home = fallbackHome;
|
|
1895
|
+
}
|
|
1896
|
+
const hiddenPageRoutes = [
|
|
1897
|
+
...validateNavbar(config.navbar),
|
|
1898
|
+
...validateFooter(config.footer)
|
|
1899
|
+
];
|
|
1900
|
+
const dedupedHiddenPageRoutes = /* @__PURE__ */ new Map();
|
|
1901
|
+
for (const route of hiddenPageRoutes) {
|
|
1902
|
+
dedupedHiddenPageRoutes.set(route.href, route);
|
|
1903
|
+
}
|
|
1904
|
+
config.hiddenPageRoutes = Array.from(dedupedHiddenPageRoutes.values());
|
|
1905
|
+
if (config.playground !== void 0) {
|
|
1906
|
+
checkType(config.playground, "object", ["playground"], "Playground");
|
|
1907
|
+
if (config.playground.proxy !== void 0) {
|
|
1908
|
+
checkType(
|
|
1909
|
+
config.playground.proxy,
|
|
1910
|
+
"boolean",
|
|
1911
|
+
["playground", "proxy"],
|
|
1912
|
+
"Proxy"
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return config;
|
|
1917
|
+
}
|
|
1918
|
+
var configCache = null;
|
|
1919
|
+
var lastMtime = 0;
|
|
1920
|
+
async function getConfig() {
|
|
1921
|
+
assertConfigured();
|
|
1922
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
1923
|
+
throw new Error(
|
|
1924
|
+
"[USER_ERROR]: Invalid docs.json: `docs.json` missing at root of documentation repo."
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
const stats = fs.statSync(CONFIG_PATH);
|
|
1928
|
+
if (configCache && stats.mtimeMs === lastMtime) {
|
|
1929
|
+
return configCache;
|
|
1930
|
+
}
|
|
1931
|
+
lastMtime = stats.mtimeMs;
|
|
1932
|
+
configCache = (async () => {
|
|
1933
|
+
const fileContent = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
1934
|
+
let config;
|
|
1935
|
+
try {
|
|
1936
|
+
config = JSON.parse(fileContent);
|
|
1937
|
+
} catch (e) {
|
|
1938
|
+
throw new Error(
|
|
1939
|
+
`[USER_ERROR]: Invalid docs.json: Invalid JSON syntax: ${e instanceof Error ? e.message : e}`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
try {
|
|
1943
|
+
const validatedConfig = await validateConfig(config);
|
|
1944
|
+
return validatedConfig;
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
throw new Error(
|
|
1947
|
+
`[USER_ERROR]: Invalid docs.json: ${error instanceof Error ? error.message : error}`
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
})();
|
|
1951
|
+
return configCache;
|
|
1952
|
+
}
|
|
1953
|
+
function validateComponentUsage(content) {
|
|
1954
|
+
const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n?/, "");
|
|
1955
|
+
const allowedComponentSet = new Set(AVAILABLE_COMPONENTS);
|
|
1956
|
+
const contentWithoutCode = contentWithoutFrontmatter.replace(/````[\s\S]*?````/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/\{\s*(['"`])(?:\\.|(?!\1)[\s\S])*?\1\s*\}/g, "");
|
|
1957
|
+
const componentRegex = /<([A-Z][a-zA-Z0-9]*)/g;
|
|
1958
|
+
let match;
|
|
1959
|
+
const unknownComponents = [];
|
|
1960
|
+
while ((match = componentRegex.exec(contentWithoutCode)) !== null) {
|
|
1961
|
+
const componentName = match[1];
|
|
1962
|
+
if (!allowedComponentSet.has(componentName)) {
|
|
1963
|
+
if (!unknownComponents.includes(componentName)) {
|
|
1964
|
+
unknownComponents.push(componentName);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (unknownComponents.length > 0) {
|
|
1969
|
+
const componentList = unknownComponents.map((c) => `<${c}>`).join(", ");
|
|
1970
|
+
const visibleComponents = AVAILABLE_COMPONENTS.filter(
|
|
1971
|
+
(component) => !INTERNAL_ONLY_COMPONENTS.has(component)
|
|
1972
|
+
);
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
`Unknown component(s): ${componentList}. Available components are: ${visibleComponents.join(", ")}. If writing ABOUT a component, use literal backticks: \`<ComponentName>\` or a JSX string: \`{'<ComponentName />'}\`.`
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
function getMdxFiles(dir) {
|
|
1979
|
+
let results = [];
|
|
1980
|
+
const list = fs.readdirSync(dir);
|
|
1981
|
+
list.forEach((file) => {
|
|
1982
|
+
file = path.resolve(dir, file);
|
|
1983
|
+
const stat = fs.statSync(file);
|
|
1984
|
+
if (stat && stat.isDirectory()) {
|
|
1985
|
+
results = results.concat(getMdxFiles(file));
|
|
1986
|
+
} else if (file.endsWith(".mdx")) {
|
|
1987
|
+
results.push(file);
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
return results;
|
|
1991
|
+
}
|
|
1992
|
+
async function validateMdxContent() {
|
|
1993
|
+
assertConfigured();
|
|
1994
|
+
const files = getMdxFiles(DOCS_DIR);
|
|
1995
|
+
for (const file of files) {
|
|
1996
|
+
try {
|
|
1997
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
1998
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1999
|
+
if (match) {
|
|
2000
|
+
const frontmatter = yaml.parse(match[1]);
|
|
2001
|
+
const result = docsSchema.safeParse(frontmatter);
|
|
2002
|
+
if (!result.success) {
|
|
2003
|
+
const issue = result.error.issues[0];
|
|
2004
|
+
const pathStr = issue.path.join(".");
|
|
2005
|
+
throw new Error(
|
|
2006
|
+
`Frontmatter validation failed: ${issue.message} (at: ${pathStr})`
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
validateComponentUsage(content);
|
|
2011
|
+
await compile(content, { jsx: true });
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
const relativePath = path.relative(DOCS_DIR, file);
|
|
2014
|
+
const location = e.line ? `:${e.line}:${e.column}` : "";
|
|
2015
|
+
const reason = e.reason || e.message;
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
`[USER_ERROR]: Invalid MDX in ${relativePath}${location} -> ${reason}`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
export {
|
|
2023
|
+
BASE_COLOR_OPTIONS,
|
|
2024
|
+
DEFAULT_SHIKI_DARK_THEME,
|
|
2025
|
+
DEFAULT_SHIKI_LIGHT_THEME,
|
|
2026
|
+
DEFAULT_THEME_COLOR_DARK,
|
|
2027
|
+
DEFAULT_THEME_COLOR_LIGHT,
|
|
2028
|
+
SHIKI_BUNDLED_THEME_NAMES,
|
|
2029
|
+
configureDocsValidator,
|
|
2030
|
+
docsSchema,
|
|
2031
|
+
getConfig,
|
|
2032
|
+
isBundledShikiThemeName,
|
|
2033
|
+
loadOpenApiSpec,
|
|
2034
|
+
validateMdxContent
|
|
2035
|
+
};
|