@aravindc26/velu 0.10.0 → 0.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/package.json +1 -1
- package/schema/velu.schema.json +714 -16
- package/src/build.ts +207 -43
- package/src/cli.ts +65 -2
- package/src/engine/_server.mjs +127 -18
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +87 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +83 -6
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/global.css +327 -0
- package/src/engine/app/layout.tsx +3 -7
- package/src/engine/app/search.css +20 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/product-switcher.tsx +78 -0
- package/src/engine/components/providers.tsx +26 -0
- package/src/engine/components/search.tsx +66 -3
- package/src/engine/components/sidebar-links.tsx +51 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/lib/layout.shared.ts +28 -6
- package/src/engine/lib/navigation-normalize.mjs +456 -0
- package/src/engine/lib/navigation-normalize.ts +488 -0
- package/src/engine/lib/source.ts +14 -0
- package/src/engine/lib/velu.ts +267 -3
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/src/lib/velu.ts +86 -13
- package/src/navigation-normalize.ts +488 -0
- package/src/validate.ts +116 -18
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
interface NavSeparator {
|
|
2
|
+
separator: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface NavLink {
|
|
6
|
+
href: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface NavGroup {
|
|
12
|
+
group: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
tag?: string;
|
|
16
|
+
expanded?: boolean;
|
|
17
|
+
description?: string;
|
|
18
|
+
hidden?: boolean;
|
|
19
|
+
pages: NavEntry[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NavTab {
|
|
23
|
+
tab: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
icon?: string;
|
|
26
|
+
href?: string;
|
|
27
|
+
pages?: Array<string | NavSeparator | NavLink>;
|
|
28
|
+
groups?: NavGroup[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type NavEntry = string | NavGroup | NavSeparator | NavLink;
|
|
32
|
+
|
|
33
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return typeof value === "object" && value !== null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSeparator(value: unknown): value is NavSeparator {
|
|
38
|
+
return isObject(value) && typeof value.separator === "string";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isLink(value: unknown): value is NavLink {
|
|
42
|
+
return isObject(value) && typeof value.href === "string" && typeof value.label === "string";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isGroupLike(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return isObject(value) && typeof value.group === "string";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isMenuItem(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return isObject(value) && typeof value.item === "string";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isTabLike(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return isObject(value) && typeof value.tab === "string";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isAnchorLike(value: unknown): value is Record<string, unknown> {
|
|
58
|
+
return isObject(value) && typeof value.anchor === "string";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isDropdownLike(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return isObject(value) && typeof value.dropdown === "string";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isGroupEntry(value: NavEntry): value is NavGroup {
|
|
66
|
+
return typeof value === "object" && value !== null && "group" in value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function slugify(input: string, fallback: string): string {
|
|
70
|
+
const slug = input
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.trim()
|
|
73
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
74
|
+
.replace(/^-+|-+$/g, "");
|
|
75
|
+
return slug || fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function uniqueSlug(base: string, used: Set<string>): string {
|
|
79
|
+
if (!used.has(base)) {
|
|
80
|
+
used.add(base);
|
|
81
|
+
return base;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let count = 2;
|
|
85
|
+
while (used.has(`${base}-${count}`)) count += 1;
|
|
86
|
+
|
|
87
|
+
const candidate = `${base}-${count}`;
|
|
88
|
+
used.add(candidate);
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
function normalizeLink(value: NavLink): NavLink {
|
|
94
|
+
const out: NavLink = { href: value.href, label: value.label };
|
|
95
|
+
if (typeof value.icon === "string" && value.icon.length > 0) out.icon = value.icon;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeAnchorLink(value: Record<string, unknown>): NavLink {
|
|
100
|
+
const href = typeof value.href === "string" ? value.href : "#";
|
|
101
|
+
const label = typeof value.anchor === "string" ? value.anchor : "Link";
|
|
102
|
+
const icon = typeof value.icon === "string" ? value.icon : undefined;
|
|
103
|
+
return icon ? { href, label, icon } : { href, label };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasContent(value: Record<string, unknown>): boolean {
|
|
107
|
+
const hasSimple =
|
|
108
|
+
(Array.isArray(value.pages) && value.pages.length > 0) ||
|
|
109
|
+
(Array.isArray(value.groups) && value.groups.length > 0) ||
|
|
110
|
+
(Array.isArray(value.menu) && value.menu.length > 0) ||
|
|
111
|
+
(Array.isArray(value.tabs) && value.tabs.length > 0) ||
|
|
112
|
+
(Array.isArray(value.dropdowns) && value.dropdowns.length > 0);
|
|
113
|
+
|
|
114
|
+
const hasAnchors =
|
|
115
|
+
Array.isArray(value.anchors) &&
|
|
116
|
+
value.anchors.some(
|
|
117
|
+
(a) =>
|
|
118
|
+
isAnchorLike(a) &&
|
|
119
|
+
((Array.isArray(a.tabs) && a.tabs.length > 0) ||
|
|
120
|
+
(Array.isArray(a.groups) && a.groups.length > 0) ||
|
|
121
|
+
(Array.isArray(a.pages) && a.pages.length > 0) ||
|
|
122
|
+
(Array.isArray(a.menu) && a.menu.length > 0) ||
|
|
123
|
+
(Array.isArray(a.anchors) && a.anchors.length > 0) ||
|
|
124
|
+
(Array.isArray(a.dropdowns) && a.dropdowns.length > 0))
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return hasSimple || hasAnchors;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeGroup(rawGroup: Record<string, unknown>, usedGroupSlugs: Set<string>): NavGroup {
|
|
131
|
+
const groupName = typeof rawGroup.group === "string" ? rawGroup.group : "Group";
|
|
132
|
+
const rawSlug = typeof rawGroup.slug === "string" ? rawGroup.slug : groupName;
|
|
133
|
+
const groupSlug = uniqueSlug(slugify(rawSlug, "group"), usedGroupSlugs);
|
|
134
|
+
|
|
135
|
+
const childUsedSlugs = new Set<string>();
|
|
136
|
+
const pages = collectEntries(rawGroup, childUsedSlugs);
|
|
137
|
+
|
|
138
|
+
const out: NavGroup = { group: groupName, slug: groupSlug, pages };
|
|
139
|
+
if (typeof rawGroup.icon === "string") out.icon = rawGroup.icon;
|
|
140
|
+
if (typeof rawGroup.tag === "string") out.tag = rawGroup.tag;
|
|
141
|
+
if (typeof rawGroup.expanded === "boolean") out.expanded = rawGroup.expanded;
|
|
142
|
+
if (typeof rawGroup.description === "string") out.description = rawGroup.description;
|
|
143
|
+
if (typeof rawGroup.hidden === "boolean") out.hidden = rawGroup.hidden;
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeMenuItem(rawItem: Record<string, unknown>, usedGroupSlugs: Set<string>): NavGroup {
|
|
148
|
+
const name = typeof rawItem.item === "string" ? rawItem.item : "Menu";
|
|
149
|
+
const rawSlug = typeof rawItem.slug === "string" ? rawItem.slug : name;
|
|
150
|
+
const slug = uniqueSlug(slugify(rawSlug, "menu"), usedGroupSlugs);
|
|
151
|
+
|
|
152
|
+
const nestedGroupSlugs = new Set<string>();
|
|
153
|
+
const pages = collectEntries(rawItem, nestedGroupSlugs);
|
|
154
|
+
const out: NavGroup = { group: name, slug, pages };
|
|
155
|
+
if (typeof rawItem.icon === "string") out.icon = rawItem.icon;
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeTabAsGroup(rawTab: Record<string, unknown>, usedGroupSlugs: Set<string>): NavGroup {
|
|
160
|
+
const tabName = typeof rawTab.tab === "string" ? rawTab.tab : "Tab";
|
|
161
|
+
const rawSlug = typeof rawTab.slug === "string" ? rawTab.slug : tabName;
|
|
162
|
+
const slug = uniqueSlug(slugify(rawSlug, "tab"), usedGroupSlugs);
|
|
163
|
+
const nestedGroupSlugs = new Set<string>();
|
|
164
|
+
const pages = collectEntries(rawTab, nestedGroupSlugs);
|
|
165
|
+
|
|
166
|
+
if (typeof rawTab.href === "string" && rawTab.href.length > 0 && !hasContent(rawTab)) {
|
|
167
|
+
pages.push({ href: rawTab.href, label: tabName, ...(typeof rawTab.icon === "string" ? { icon: rawTab.icon } : {}) });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const out: NavGroup = { group: tabName, slug, pages };
|
|
171
|
+
if (typeof rawTab.icon === "string") out.icon = rawTab.icon;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeDropdownAsGroup(rawDropdown: Record<string, unknown>, usedGroupSlugs: Set<string>): NavGroup {
|
|
176
|
+
return normalizeTabAsGroup(
|
|
177
|
+
{
|
|
178
|
+
tab: rawDropdown.dropdown,
|
|
179
|
+
slug: rawDropdown.slug,
|
|
180
|
+
icon: rawDropdown.icon,
|
|
181
|
+
href: rawDropdown.href,
|
|
182
|
+
groups: rawDropdown.groups,
|
|
183
|
+
pages: rawDropdown.pages,
|
|
184
|
+
menu: rawDropdown.menu,
|
|
185
|
+
anchors: rawDropdown.anchors,
|
|
186
|
+
dropdowns: rawDropdown.dropdowns,
|
|
187
|
+
tabs: rawDropdown.tabs,
|
|
188
|
+
},
|
|
189
|
+
usedGroupSlugs
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeAnchorAsGroup(rawAnchor: Record<string, unknown>, usedGroupSlugs: Set<string>): NavGroup {
|
|
194
|
+
const anchorName = typeof rawAnchor.anchor === "string" ? rawAnchor.anchor : "Anchor";
|
|
195
|
+
const rawSlug = typeof rawAnchor.slug === "string" ? rawAnchor.slug : anchorName;
|
|
196
|
+
const slug = uniqueSlug(slugify(rawSlug, "anchor"), usedGroupSlugs);
|
|
197
|
+
const nestedGroupSlugs = new Set<string>();
|
|
198
|
+
const pages = collectEntries(rawAnchor, nestedGroupSlugs);
|
|
199
|
+
|
|
200
|
+
const out: NavGroup = { group: anchorName, slug, pages };
|
|
201
|
+
if (typeof rawAnchor.icon === "string") out.icon = rawAnchor.icon;
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function collectEntries(rawSection: Record<string, unknown>, usedGroupSlugs: Set<string>): NavEntry[] {
|
|
206
|
+
const entries: NavEntry[] = [];
|
|
207
|
+
|
|
208
|
+
for (const item of Array.isArray(rawSection.menu) ? rawSection.menu : []) {
|
|
209
|
+
if (isMenuItem(item)) entries.push(normalizeMenuItem(item, usedGroupSlugs));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const group of Array.isArray(rawSection.groups) ? rawSection.groups : []) {
|
|
213
|
+
if (isGroupLike(group)) entries.push(normalizeGroup(group, usedGroupSlugs));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const item of Array.isArray(rawSection.pages) ? rawSection.pages : []) {
|
|
217
|
+
if (typeof item === "string") entries.push(item);
|
|
218
|
+
else if (isSeparator(item)) entries.push({ separator: item.separator });
|
|
219
|
+
else if (isLink(item)) entries.push(normalizeLink(item));
|
|
220
|
+
else if (isGroupLike(item)) entries.push(normalizeGroup(item, usedGroupSlugs));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const anchor of Array.isArray(rawSection.anchors) ? rawSection.anchors : []) {
|
|
224
|
+
if (!isAnchorLike(anchor)) continue;
|
|
225
|
+
|
|
226
|
+
const hrefOnly = typeof anchor.href === "string" && anchor.href.length > 0 && !hasContent(anchor);
|
|
227
|
+
if (hrefOnly) entries.push(normalizeAnchorLink(anchor));
|
|
228
|
+
else entries.push(normalizeAnchorAsGroup(anchor, usedGroupSlugs));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const dropdown of Array.isArray(rawSection.dropdowns) ? rawSection.dropdowns : []) {
|
|
232
|
+
if (isDropdownLike(dropdown)) entries.push(normalizeDropdownAsGroup(dropdown, usedGroupSlugs));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const tab of Array.isArray(rawSection.tabs) ? rawSection.tabs : []) {
|
|
236
|
+
if (isTabLike(tab)) entries.push(normalizeTabAsGroup(tab, usedGroupSlugs));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return entries;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeTab(rawTab: Record<string, unknown>, usedTabSlugs: Set<string>, slugPrefix: string): NavTab {
|
|
243
|
+
const tabName = typeof rawTab.tab === "string" ? rawTab.tab : "Tab";
|
|
244
|
+
const rawSlug = typeof rawTab.slug === "string" ? rawTab.slug : tabName;
|
|
245
|
+
const tabSlugPart = slugify(rawSlug, "tab");
|
|
246
|
+
const fullSlug = slugPrefix ? `${slugPrefix}/${tabSlugPart}` : tabSlugPart;
|
|
247
|
+
const slug = uniqueSlug(fullSlug, usedTabSlugs);
|
|
248
|
+
|
|
249
|
+
const out: NavTab = { tab: tabName, slug };
|
|
250
|
+
if (typeof rawTab.icon === "string") out.icon = rawTab.icon;
|
|
251
|
+
|
|
252
|
+
if (typeof rawTab.href === "string" && rawTab.href.length > 0 && !hasContent(rawTab)) {
|
|
253
|
+
out.href = rawTab.href;
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const groupSlugSet = new Set<string>();
|
|
258
|
+
const entries = collectEntries(rawTab, groupSlugSet);
|
|
259
|
+
const groups: NavGroup[] = [];
|
|
260
|
+
const pages: Array<string | NavSeparator | NavLink> = [];
|
|
261
|
+
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (isGroupEntry(entry)) groups.push(entry);
|
|
264
|
+
else pages.push(entry);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (groups.length > 0) out.groups = groups;
|
|
268
|
+
if (pages.length > 0) out.pages = pages;
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeDropdownToTab(rawDropdown: Record<string, unknown>, usedTabSlugs: Set<string>, slugPrefix: string): NavTab {
|
|
273
|
+
return normalizeTab(
|
|
274
|
+
{
|
|
275
|
+
tab: rawDropdown.dropdown,
|
|
276
|
+
slug: rawDropdown.slug,
|
|
277
|
+
icon: rawDropdown.icon,
|
|
278
|
+
href: rawDropdown.href,
|
|
279
|
+
groups: rawDropdown.groups,
|
|
280
|
+
pages: rawDropdown.pages,
|
|
281
|
+
menu: rawDropdown.menu,
|
|
282
|
+
anchors: rawDropdown.anchors,
|
|
283
|
+
dropdowns: rawDropdown.dropdowns,
|
|
284
|
+
tabs: rawDropdown.tabs,
|
|
285
|
+
},
|
|
286
|
+
usedTabSlugs,
|
|
287
|
+
slugPrefix
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeTabList(rawTabs: unknown[], usedTabSlugs: Set<string>, slugPrefix = ""): NavTab[] {
|
|
292
|
+
const tabs: NavTab[] = [];
|
|
293
|
+
for (const item of rawTabs) {
|
|
294
|
+
if (isTabLike(item)) tabs.push(normalizeTab(item, usedTabSlugs, slugPrefix));
|
|
295
|
+
}
|
|
296
|
+
return tabs;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeDropdownList(rawDropdowns: unknown[], usedTabSlugs: Set<string>, slugPrefix = ""): NavTab[] {
|
|
300
|
+
const tabs: NavTab[] = [];
|
|
301
|
+
for (const item of rawDropdowns) {
|
|
302
|
+
if (isDropdownLike(item)) tabs.push(normalizeDropdownToTab(item, usedTabSlugs, slugPrefix));
|
|
303
|
+
}
|
|
304
|
+
return tabs;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeNavigationTabs(navigation: unknown, usedTabSlugs = new Set<string>()): NavTab[] {
|
|
308
|
+
if (!isObject(navigation)) return [];
|
|
309
|
+
|
|
310
|
+
const tabs: NavTab[] = [];
|
|
311
|
+
|
|
312
|
+
tabs.push(...normalizeTabList(Array.isArray(navigation.tabs) ? navigation.tabs : [], usedTabSlugs));
|
|
313
|
+
tabs.push(...normalizeDropdownList(Array.isArray(navigation.dropdowns) ? navigation.dropdowns : [], usedTabSlugs));
|
|
314
|
+
|
|
315
|
+
if (Array.isArray(navigation.products)) {
|
|
316
|
+
navigation.products.forEach((product, index) => {
|
|
317
|
+
if (!isObject(product)) return;
|
|
318
|
+
const productName = typeof product.product === "string" ? product.product : `Product ${index + 1}`;
|
|
319
|
+
const prefix = slugify(productName, `product-${index + 1}`);
|
|
320
|
+
|
|
321
|
+
tabs.push(...normalizeTabList(Array.isArray(product.tabs) ? product.tabs : [], usedTabSlugs, prefix));
|
|
322
|
+
tabs.push(...normalizeDropdownList(Array.isArray(product.dropdowns) ? product.dropdowns : [], usedTabSlugs, prefix));
|
|
323
|
+
|
|
324
|
+
if (!Array.isArray(product.tabs) && !Array.isArray(product.dropdowns)) {
|
|
325
|
+
if (hasContent(product)) {
|
|
326
|
+
tabs.push(
|
|
327
|
+
normalizeTab(
|
|
328
|
+
{
|
|
329
|
+
tab: productName,
|
|
330
|
+
slug: prefix,
|
|
331
|
+
icon: product.icon,
|
|
332
|
+
groups: product.groups,
|
|
333
|
+
pages: product.pages,
|
|
334
|
+
menu: product.menu,
|
|
335
|
+
anchors: product.anchors,
|
|
336
|
+
dropdowns: product.dropdowns,
|
|
337
|
+
tabs: product.tabs,
|
|
338
|
+
},
|
|
339
|
+
usedTabSlugs,
|
|
340
|
+
""
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
} else if (typeof product.href === "string" && product.href.length > 0) {
|
|
344
|
+
tabs.push(
|
|
345
|
+
normalizeTab(
|
|
346
|
+
{
|
|
347
|
+
tab: productName,
|
|
348
|
+
slug: prefix,
|
|
349
|
+
icon: product.icon,
|
|
350
|
+
href: product.href,
|
|
351
|
+
},
|
|
352
|
+
usedTabSlugs,
|
|
353
|
+
""
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (Array.isArray(navigation.versions)) {
|
|
362
|
+
navigation.versions.forEach((version, index) => {
|
|
363
|
+
if (!isObject(version)) return;
|
|
364
|
+
const versionName = typeof version.version === "string" ? version.version : `Version ${index + 1}`;
|
|
365
|
+
const prefix = slugify(versionName, `version-${index + 1}`);
|
|
366
|
+
|
|
367
|
+
tabs.push(...normalizeTabList(Array.isArray(version.tabs) ? version.tabs : [], usedTabSlugs, prefix));
|
|
368
|
+
tabs.push(...normalizeDropdownList(Array.isArray(version.dropdowns) ? version.dropdowns : [], usedTabSlugs, prefix));
|
|
369
|
+
|
|
370
|
+
if (!Array.isArray(version.tabs) && !Array.isArray(version.dropdowns)) {
|
|
371
|
+
if (hasContent(version)) {
|
|
372
|
+
tabs.push(
|
|
373
|
+
normalizeTab(
|
|
374
|
+
{
|
|
375
|
+
tab: versionName,
|
|
376
|
+
slug: prefix,
|
|
377
|
+
groups: version.groups,
|
|
378
|
+
pages: version.pages,
|
|
379
|
+
menu: version.menu,
|
|
380
|
+
anchors: version.anchors,
|
|
381
|
+
dropdowns: version.dropdowns,
|
|
382
|
+
tabs: version.tabs,
|
|
383
|
+
},
|
|
384
|
+
usedTabSlugs,
|
|
385
|
+
""
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
} else if (typeof version.href === "string" && version.href.length > 0) {
|
|
389
|
+
tabs.push(
|
|
390
|
+
normalizeTab(
|
|
391
|
+
{
|
|
392
|
+
tab: versionName,
|
|
393
|
+
slug: prefix,
|
|
394
|
+
href: version.href,
|
|
395
|
+
},
|
|
396
|
+
usedTabSlugs,
|
|
397
|
+
""
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (Array.isArray(navigation.anchors)) {
|
|
406
|
+
navigation.anchors.forEach((anchor, index) => {
|
|
407
|
+
if (!isAnchorLike(anchor)) return;
|
|
408
|
+
|
|
409
|
+
const anchorName = typeof anchor.anchor === "string" ? anchor.anchor : `Anchor ${index + 1}`;
|
|
410
|
+
const prefix = slugify(anchorName, `anchor-${index + 1}`);
|
|
411
|
+
|
|
412
|
+
if (Array.isArray(anchor.tabs)) {
|
|
413
|
+
tabs.push(...normalizeTabList(anchor.tabs, usedTabSlugs, prefix));
|
|
414
|
+
} else if (hasContent(anchor)) {
|
|
415
|
+
tabs.push(
|
|
416
|
+
normalizeTab(
|
|
417
|
+
{
|
|
418
|
+
tab: anchorName,
|
|
419
|
+
slug: prefix,
|
|
420
|
+
icon: anchor.icon,
|
|
421
|
+
groups: anchor.groups,
|
|
422
|
+
pages: anchor.pages,
|
|
423
|
+
menu: anchor.menu,
|
|
424
|
+
anchors: anchor.anchors,
|
|
425
|
+
dropdowns: anchor.dropdowns,
|
|
426
|
+
tabs: anchor.tabs,
|
|
427
|
+
},
|
|
428
|
+
usedTabSlugs,
|
|
429
|
+
""
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const hasRootGroups = Array.isArray(navigation.groups) && navigation.groups.length > 0;
|
|
437
|
+
const hasRootPages = Array.isArray(navigation.pages) && navigation.pages.length > 0;
|
|
438
|
+
const hasRootMenu = Array.isArray(navigation.menu) && navigation.menu.length > 0;
|
|
439
|
+
|
|
440
|
+
if (tabs.length === 0 && (hasRootGroups || hasRootPages || hasRootMenu)) {
|
|
441
|
+
tabs.push(
|
|
442
|
+
normalizeTab(
|
|
443
|
+
{
|
|
444
|
+
tab: "Documentation",
|
|
445
|
+
slug: "documentation",
|
|
446
|
+
groups: navigation.groups,
|
|
447
|
+
pages: navigation.pages,
|
|
448
|
+
menu: navigation.menu,
|
|
449
|
+
anchors: navigation.anchors,
|
|
450
|
+
dropdowns: navigation.dropdowns,
|
|
451
|
+
tabs: navigation.tabs,
|
|
452
|
+
},
|
|
453
|
+
usedTabSlugs,
|
|
454
|
+
""
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return tabs;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function normalizeLanguageEntries(languages: unknown): Array<Record<string, unknown>> {
|
|
463
|
+
if (!Array.isArray(languages)) return [];
|
|
464
|
+
|
|
465
|
+
return languages
|
|
466
|
+
.filter(isObject)
|
|
467
|
+
.map((entry) => {
|
|
468
|
+
const usedTabSlugs = new Set<string>();
|
|
469
|
+
return {
|
|
470
|
+
...entry,
|
|
471
|
+
tabs: normalizeNavigationTabs(entry, usedTabSlugs),
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function normalizeConfigNavigation<T extends { navigation?: unknown }>(config: T): T {
|
|
477
|
+
const nav = isObject(config.navigation) ? config.navigation : {};
|
|
478
|
+
return {
|
|
479
|
+
...config,
|
|
480
|
+
navigation: {
|
|
481
|
+
...nav,
|
|
482
|
+
tabs: normalizeNavigationTabs(nav),
|
|
483
|
+
languages: normalizeLanguageEntries(nav.languages),
|
|
484
|
+
products: Array.isArray(nav.products) ? nav.products : [],
|
|
485
|
+
versions: Array.isArray(nav.versions) ? nav.versions : [],
|
|
486
|
+
},
|
|
487
|
+
} as T;
|
|
488
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -2,23 +2,79 @@ import Ajv, { type AnySchema } from "ajv";
|
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { resolve, join } from "node:path";
|
|
5
|
+
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
6
|
+
|
|
7
|
+
interface VeluSeparator {
|
|
8
|
+
separator: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface VeluLink {
|
|
12
|
+
href: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VeluAnchor {
|
|
18
|
+
anchor: string;
|
|
19
|
+
href?: string;
|
|
20
|
+
icon?: string;
|
|
21
|
+
color?: {
|
|
22
|
+
light: string;
|
|
23
|
+
dark: string;
|
|
24
|
+
};
|
|
25
|
+
tabs?: VeluTab[];
|
|
26
|
+
hidden?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface VeluGlobalTab {
|
|
30
|
+
tab: string;
|
|
31
|
+
href: string;
|
|
32
|
+
icon?: string;
|
|
33
|
+
}
|
|
5
34
|
|
|
6
35
|
interface VeluGroup {
|
|
7
36
|
group: string;
|
|
8
|
-
slug
|
|
37
|
+
slug?: string;
|
|
9
38
|
icon?: string;
|
|
10
39
|
tag?: string;
|
|
11
40
|
expanded?: boolean;
|
|
12
|
-
|
|
41
|
+
description?: string;
|
|
42
|
+
hidden?: boolean;
|
|
43
|
+
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface VeluMenuItem {
|
|
47
|
+
item: string;
|
|
48
|
+
icon?: string;
|
|
49
|
+
groups?: VeluGroup[];
|
|
50
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
13
51
|
}
|
|
14
52
|
|
|
15
53
|
interface VeluTab {
|
|
16
54
|
tab: string;
|
|
17
|
-
slug
|
|
55
|
+
slug?: string;
|
|
18
56
|
icon?: string;
|
|
19
57
|
href?: string;
|
|
20
|
-
pages?: string[];
|
|
58
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
21
59
|
groups?: VeluGroup[];
|
|
60
|
+
menu?: VeluMenuItem[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface VeluLanguageNav {
|
|
64
|
+
language: string;
|
|
65
|
+
tabs: VeluTab[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface VeluProductNav {
|
|
69
|
+
product: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
tabs?: VeluTab[];
|
|
72
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface VeluVersionNav {
|
|
76
|
+
version: string;
|
|
77
|
+
tabs: VeluTab[];
|
|
22
78
|
}
|
|
23
79
|
|
|
24
80
|
interface VeluConfig {
|
|
@@ -28,7 +84,15 @@ interface VeluConfig {
|
|
|
28
84
|
appearance?: "system" | "light" | "dark";
|
|
29
85
|
styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
|
|
30
86
|
navigation: {
|
|
31
|
-
tabs
|
|
87
|
+
tabs?: VeluTab[];
|
|
88
|
+
languages?: VeluLanguageNav[];
|
|
89
|
+
products?: VeluProductNav[];
|
|
90
|
+
versions?: VeluVersionNav[];
|
|
91
|
+
anchors?: VeluAnchor[];
|
|
92
|
+
global?: {
|
|
93
|
+
anchors?: VeluAnchor[];
|
|
94
|
+
tabs?: VeluGlobalTab[];
|
|
95
|
+
};
|
|
32
96
|
};
|
|
33
97
|
}
|
|
34
98
|
|
|
@@ -37,22 +101,34 @@ function loadJson(filePath: string): unknown {
|
|
|
37
101
|
return JSON.parse(raw);
|
|
38
102
|
}
|
|
39
103
|
|
|
40
|
-
function
|
|
104
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
105
|
+
return typeof item === "object" && item !== null && "group" in item;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isPageString(item: unknown): item is string {
|
|
109
|
+
return typeof item === "string";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectPagesFromTabs(tabs: VeluTab[]): string[] {
|
|
41
113
|
const pages: string[] = [];
|
|
42
114
|
|
|
43
115
|
function collectFromGroup(group: VeluGroup) {
|
|
44
116
|
for (const item of group.pages) {
|
|
45
|
-
if (
|
|
117
|
+
if (isPageString(item)) {
|
|
46
118
|
pages.push(item);
|
|
47
|
-
} else {
|
|
119
|
+
} else if (isGroup(item)) {
|
|
48
120
|
collectFromGroup(item);
|
|
49
121
|
}
|
|
50
122
|
}
|
|
51
123
|
}
|
|
52
124
|
|
|
53
|
-
for (const tab of
|
|
125
|
+
for (const tab of tabs) {
|
|
54
126
|
if (tab.pages) {
|
|
55
|
-
|
|
127
|
+
for (const item of tab.pages) {
|
|
128
|
+
if (isPageString(item)) {
|
|
129
|
+
pages.push(item);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
56
132
|
}
|
|
57
133
|
if (tab.groups) {
|
|
58
134
|
for (const group of tab.groups) {
|
|
@@ -64,6 +140,13 @@ function collectPages(config: VeluConfig): string[] {
|
|
|
64
140
|
return pages;
|
|
65
141
|
}
|
|
66
142
|
|
|
143
|
+
function collectPages(config: VeluConfig): string[] {
|
|
144
|
+
const tabs = config.navigation.languages && config.navigation.languages.length > 0
|
|
145
|
+
? config.navigation.languages.flatMap((lang) => lang.tabs)
|
|
146
|
+
: (config.navigation.tabs ?? []);
|
|
147
|
+
return collectPagesFromTabs(tabs);
|
|
148
|
+
}
|
|
149
|
+
|
|
67
150
|
function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boolean; errors: string[] } {
|
|
68
151
|
const errors: string[] = [];
|
|
69
152
|
|
|
@@ -77,13 +160,13 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
77
160
|
}
|
|
78
161
|
|
|
79
162
|
const schema = loadJson(schemaPath) as AnySchema;
|
|
80
|
-
const
|
|
163
|
+
const rawConfig = loadJson(configPath) as VeluConfig;
|
|
81
164
|
|
|
82
165
|
// Validate against JSON schema
|
|
83
166
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
84
167
|
addFormats(ajv);
|
|
85
168
|
const validate = ajv.compile(schema);
|
|
86
|
-
const schemaValid = validate(
|
|
169
|
+
const schemaValid = validate(rawConfig);
|
|
87
170
|
|
|
88
171
|
if (!schemaValid && validate.errors) {
|
|
89
172
|
for (const err of validate.errors) {
|
|
@@ -91,6 +174,8 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
91
174
|
}
|
|
92
175
|
}
|
|
93
176
|
|
|
177
|
+
const config = normalizeConfigNavigation(rawConfig);
|
|
178
|
+
|
|
94
179
|
// Validate that all referenced .md files exist
|
|
95
180
|
const pages = collectPages(config);
|
|
96
181
|
for (const page of pages) {
|
|
@@ -101,15 +186,28 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
101
186
|
}
|
|
102
187
|
|
|
103
188
|
// Check for duplicate page references
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
if (config.navigation.languages && config.navigation.languages.length > 0) {
|
|
190
|
+
for (const lang of config.navigation.languages) {
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
const langPages = collectPagesFromTabs(lang.tabs);
|
|
193
|
+
for (const page of langPages) {
|
|
194
|
+
if (seen.has(page)) {
|
|
195
|
+
errors.push(`Duplicate page reference in language '${lang.language}': ${page}`);
|
|
196
|
+
}
|
|
197
|
+
seen.add(page);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const seen = new Set<string>();
|
|
202
|
+
for (const page of pages) {
|
|
203
|
+
if (seen.has(page)) {
|
|
204
|
+
errors.push(`Duplicate page reference: ${page}`);
|
|
205
|
+
}
|
|
206
|
+
seen.add(page);
|
|
108
207
|
}
|
|
109
|
-
seen.add(page);
|
|
110
208
|
}
|
|
111
209
|
|
|
112
210
|
return { valid: errors.length === 0, errors };
|
|
113
211
|
}
|
|
114
212
|
|
|
115
|
-
export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab };
|
|
213
|
+
export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor };
|