@aravindc26/velu 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -6
- package/schema/velu.schema.json +1864 -30
- package/src/build.ts +1161 -180
- package/src/cli.ts +121 -16
- package/src/engine/_server.mjs +1708 -192
- package/src/engine/app/(docs)/[...slug]/layout.tsx +377 -0
- package/src/engine/app/(docs)/[...slug]/page.tsx +917 -0
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/api/proxy/route.ts +23 -0
- package/src/engine/app/copy-page.css +59 -1
- package/src/engine/app/global.css +3487 -6
- package/src/engine/app/layout.tsx +59 -8
- package/src/engine/app/llms-file/route.ts +87 -0
- package/src/engine/app/llms-full-file/route.ts +62 -0
- package/src/engine/app/md-file/[...slug]/route.ts +409 -0
- package/src/engine/app/page.tsx +45 -0
- package/src/engine/app/robots.txt/route.ts +61 -0
- package/src/engine/app/rss-file/[...slug]/route.ts +176 -0
- package/src/engine/app/search.css +20 -0
- package/src/engine/app/sitemap.xml/route.ts +80 -0
- package/src/engine/components/assistant.tsx +16 -5
- package/src/engine/components/changelog-filters.tsx +114 -0
- package/src/engine/components/code-group.tsx +383 -0
- package/src/engine/components/color.tsx +118 -0
- package/src/engine/components/expandable.tsx +77 -0
- package/src/engine/components/icon.tsx +136 -0
- package/src/engine/components/image-zoom-fallback.tsx +147 -0
- package/src/engine/components/image.tsx +111 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/manual-api-playground.tsx +154 -0
- package/src/engine/components/mermaid.tsx +142 -0
- package/src/engine/components/openapi-toc-sync.tsx +59 -0
- package/src/engine/components/openapi.tsx +1679 -0
- package/src/engine/components/page-feedback.tsx +153 -0
- package/src/engine/components/product-switcher.tsx +102 -0
- package/src/engine/components/prompt.tsx +90 -0
- package/src/engine/components/providers.tsx +21 -0
- package/src/engine/components/search.tsx +70 -3
- package/src/engine/components/sidebar-links.tsx +49 -0
- package/src/engine/components/synced-tabs.tsx +57 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/toc-examples.tsx +110 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/components/view.tsx +344 -0
- package/src/engine/generated/redirects.ts +3 -0
- package/src/engine/lib/changelog.ts +246 -0
- package/src/engine/lib/layout.shared.ts +57 -7
- package/src/engine/lib/llms.ts +444 -0
- package/src/engine/lib/navigation-normalize.mjs +525 -0
- package/src/engine/lib/navigation-normalize.ts +695 -0
- package/src/engine/lib/redirects.ts +194 -0
- package/src/engine/lib/source.ts +121 -4
- package/src/engine/lib/velu.ts +635 -5
- package/src/engine/mdx-components.tsx +648 -0
- package/src/engine/middleware.ts +66 -0
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/public/icons/cursor-dark.svg +12 -0
- package/src/engine/public/icons/cursor-light.svg +12 -0
- package/src/engine/source.config.ts +98 -1
- package/src/engine/src/components/PageTitle.astro +16 -5
- package/src/engine/src/lib/velu.ts +97 -16
- package/src/navigation-normalize.ts +686 -0
- package/src/themes.ts +6 -6
- package/src/validate.ts +235 -24
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -69
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import redirectRules from '@/generated/redirects';
|
|
4
|
+
import {
|
|
5
|
+
compileRedirectRules,
|
|
6
|
+
isExternalDestination,
|
|
7
|
+
normalizeRedirectRules,
|
|
8
|
+
resolveRedirect,
|
|
9
|
+
} from '@/lib/redirects';
|
|
10
|
+
|
|
11
|
+
const compiledRedirects = compileRedirectRules(normalizeRedirectRules(redirectRules));
|
|
12
|
+
|
|
13
|
+
export function middleware(request: NextRequest) {
|
|
14
|
+
const { pathname } = request.nextUrl;
|
|
15
|
+
|
|
16
|
+
if (pathname.startsWith('/rss-file')) {
|
|
17
|
+
return NextResponse.next();
|
|
18
|
+
}
|
|
19
|
+
if (pathname.startsWith('/llms-file') || pathname.startsWith('/llms-full-file')) {
|
|
20
|
+
return NextResponse.next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (pathname.endsWith('/rss.xml')) {
|
|
24
|
+
const rewritten = request.nextUrl.clone();
|
|
25
|
+
rewritten.pathname = `/rss-file${pathname.slice(0, -('/rss.xml'.length))}`;
|
|
26
|
+
return NextResponse.rewrite(rewritten);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (pathname === '/llms.txt') {
|
|
30
|
+
const rewritten = request.nextUrl.clone();
|
|
31
|
+
rewritten.pathname = '/llms-file';
|
|
32
|
+
return NextResponse.rewrite(rewritten);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (pathname === '/llms-full.txt') {
|
|
36
|
+
const rewritten = request.nextUrl.clone();
|
|
37
|
+
rewritten.pathname = '/llms-full-file';
|
|
38
|
+
return NextResponse.rewrite(rewritten);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (pathname.endsWith('.md')) {
|
|
42
|
+
const rewritten = request.nextUrl.clone();
|
|
43
|
+
rewritten.pathname = `/md-file${pathname}`;
|
|
44
|
+
return NextResponse.rewrite(rewritten);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const redirect = resolveRedirect(pathname, compiledRedirects);
|
|
48
|
+
if (redirect) {
|
|
49
|
+
if (isExternalDestination(redirect.destination)) {
|
|
50
|
+
return NextResponse.redirect(redirect.destination, redirect.statusCode);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const target = request.nextUrl.clone();
|
|
54
|
+
target.pathname = redirect.destination;
|
|
55
|
+
if (!target.search && request.nextUrl.search) {
|
|
56
|
+
target.search = request.nextUrl.search;
|
|
57
|
+
}
|
|
58
|
+
return NextResponse.redirect(target, redirect.statusCode);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return NextResponse.next();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const config = {
|
|
65
|
+
matcher: ['/((?!_next|favicon.ico|sitemap.xml|robots.txt|assets|images).*)'],
|
|
66
|
+
};
|
|
@@ -7,8 +7,8 @@ const withMDX = createMDX({
|
|
|
7
7
|
|
|
8
8
|
/** @type {import('next').NextConfig} */
|
|
9
9
|
const config = {
|
|
10
|
-
reactStrictMode:
|
|
11
|
-
output: 'export',
|
|
10
|
+
reactStrictMode: false,
|
|
11
|
+
output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
|
|
12
12
|
distDir: 'dist',
|
|
13
13
|
devIndicators: false,
|
|
14
14
|
turbopack: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 466.73 532.09">
|
|
3
|
+
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
|
|
4
|
+
<defs>
|
|
5
|
+
<style>
|
|
6
|
+
.st0 {
|
|
7
|
+
fill: #edecec;
|
|
8
|
+
}
|
|
9
|
+
</style>
|
|
10
|
+
</defs>
|
|
11
|
+
<path class="st0" d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 466.73 532.09">
|
|
3
|
+
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
|
|
4
|
+
<defs>
|
|
5
|
+
<style>
|
|
6
|
+
.st0 {
|
|
7
|
+
fill: #26251e;
|
|
8
|
+
}
|
|
9
|
+
</style>
|
|
10
|
+
</defs>
|
|
11
|
+
<path class="st0" d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/>
|
|
12
|
+
</svg>
|
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
|
2
2
|
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
|
|
3
|
+
import { transformerMetaHighlight } from '@shikijs/transformers';
|
|
4
|
+
|
|
5
|
+
function remarkCodeFilenameToTitle() {
|
|
6
|
+
const booleanMetaFlags = new Set([
|
|
7
|
+
'wrap',
|
|
8
|
+
'copy',
|
|
9
|
+
'nocopy',
|
|
10
|
+
'lineNumbers',
|
|
11
|
+
'linenumbers',
|
|
12
|
+
'showLineNumbers',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function quoteTitle(value: string): string {
|
|
16
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureTitleMeta(meta: string): string {
|
|
20
|
+
const trimmed = meta.trim();
|
|
21
|
+
if (!trimmed) return trimmed;
|
|
22
|
+
if (/\btitle\s*=/.test(trimmed)) return trimmed;
|
|
23
|
+
|
|
24
|
+
const fileWithRest = trimmed.match(/^([^\s]+?\.[a-z0-9_-]+)(\s+.*)?$/i);
|
|
25
|
+
if (fileWithRest) {
|
|
26
|
+
const file = fileWithRest[1];
|
|
27
|
+
const rest = (fileWithRest[2] ?? '').trim();
|
|
28
|
+
return rest ? `title="${quoteTitle(file)}" ${rest}` : `title="${quoteTitle(file)}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!trimmed.includes('=') && !trimmed.includes('{') && !trimmed.includes('}')) {
|
|
32
|
+
if (booleanMetaFlags.has(trimmed)) return trimmed;
|
|
33
|
+
return `title="${quoteTitle(trimmed)}"`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function visit(node: any) {
|
|
40
|
+
if (!node || typeof node !== 'object') return;
|
|
41
|
+
|
|
42
|
+
if (node.type === 'code' && typeof node.meta === 'string') {
|
|
43
|
+
let meta = node.meta.trim();
|
|
44
|
+
// Mint-style fence syntax: ```lang filename.ext
|
|
45
|
+
// Convert it into title metadata so code tabs can use file names.
|
|
46
|
+
meta = ensureTitleMeta(meta);
|
|
47
|
+
|
|
48
|
+
// Mint-style line highlight syntax: highlight=1 or highlight="1,3-5"
|
|
49
|
+
// Convert to Shiki meta-highlight format: {1,3-5}
|
|
50
|
+
const hlMatch = meta.match(/(?:^|\s)highlight=(?:"([^"]+)"|'([^']+)'|([^\s]+))/i);
|
|
51
|
+
if (hlMatch) {
|
|
52
|
+
const raw = (hlMatch[1] ?? hlMatch[2] ?? hlMatch[3] ?? '').trim();
|
|
53
|
+
const lineSpec = raw.replace(/[{}]/g, '');
|
|
54
|
+
meta = meta.replace(hlMatch[0], '').replace(/\s+/g, ' ').trim();
|
|
55
|
+
if (lineSpec && !/\{\s*\d[\d,\-\s]*\s*\}/.test(meta)) {
|
|
56
|
+
meta = `${meta} {${lineSpec}}`.trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// theme={null} is a Mint docs hint; remove it from fence meta.
|
|
61
|
+
meta = meta.replace(/\btheme=\{null\}\b/g, '').replace(/\s+/g, ' ').trim();
|
|
62
|
+
node.meta = meta;
|
|
63
|
+
}
|
|
64
|
+
if (node.type === 'code' && typeof node.meta !== 'string') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.type === 'code' && node.meta === '') {
|
|
69
|
+
delete node.meta;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node.type === 'code' && typeof node.meta === 'string') {
|
|
73
|
+
node.meta = node.meta.trim();
|
|
74
|
+
if (!node.meta) {
|
|
75
|
+
delete node.meta;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const children = node.children;
|
|
80
|
+
if (Array.isArray(children)) {
|
|
81
|
+
for (const child of children) visit(child);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (tree: any) => visit(tree);
|
|
86
|
+
}
|
|
3
87
|
|
|
4
88
|
export const docs = defineDocs({
|
|
5
89
|
dir: 'content/docs',
|
|
@@ -14,4 +98,17 @@ export const docs = defineDocs({
|
|
|
14
98
|
},
|
|
15
99
|
});
|
|
16
100
|
|
|
17
|
-
export default defineConfig(
|
|
101
|
+
export default defineConfig({
|
|
102
|
+
mdxOptions: {
|
|
103
|
+
remarkPlugins: [remarkCodeFilenameToTitle],
|
|
104
|
+
rehypeCodeOptions: ({
|
|
105
|
+
lazy: false,
|
|
106
|
+
fallbackLanguage: 'bash',
|
|
107
|
+
transformers: [transformerMetaHighlight()],
|
|
108
|
+
langAlias: {
|
|
109
|
+
gradle: 'groovy',
|
|
110
|
+
proguard: 'properties',
|
|
111
|
+
},
|
|
112
|
+
} as any),
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -405,19 +405,30 @@ const title = Astro.locals.starlightRoute.entry.data.title;
|
|
|
405
405
|
}
|
|
406
406
|
}, true);
|
|
407
407
|
|
|
408
|
-
// Hide ask bar only when
|
|
409
|
-
|
|
408
|
+
// Hide ask bar only when truly near the page bottom.
|
|
409
|
+
function syncAskBarVisibility() {
|
|
410
410
|
if (isPanelOpen()) return;
|
|
411
411
|
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
412
412
|
var docHeight = document.documentElement.scrollHeight;
|
|
413
413
|
var winHeight = window.innerHeight;
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
var bottomGap = docHeight - scrollTop - winHeight;
|
|
415
|
+
var nearBottomThreshold = 8;
|
|
416
|
+
|
|
417
|
+
if (docHeight <= winHeight + 2) {
|
|
418
|
+
askBar.classList.remove('velu-ask-bar-hidden');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (bottomGap <= nearBottomThreshold) {
|
|
416
423
|
askBar.classList.add('velu-ask-bar-hidden');
|
|
417
424
|
} else {
|
|
418
425
|
askBar.classList.remove('velu-ask-bar-hidden');
|
|
419
426
|
}
|
|
420
|
-
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
window.addEventListener('scroll', syncAskBarVisibility, { passive: true });
|
|
430
|
+
window.addEventListener('resize', syncAskBarVisibility, { passive: true });
|
|
431
|
+
syncAskBarVisibility();
|
|
421
432
|
|
|
422
433
|
document.onkeydown = function(e) {
|
|
423
434
|
if (e.key === 'Escape' && isPanelOpen()) { closePanel(); }
|
|
@@ -1,15 +1,54 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { normalizeConfigNavigation } from '../../lib/navigation-normalize';
|
|
4
|
+
const PRIMARY_CONFIG_NAME = 'docs.json';
|
|
5
|
+
const LEGACY_CONFIG_NAME = 'velu.json';
|
|
6
|
+
|
|
7
|
+
function resolveConfigPath(cwd: string): string {
|
|
8
|
+
const primary = resolve(cwd, PRIMARY_CONFIG_NAME);
|
|
9
|
+
if (existsSync(primary)) return primary;
|
|
10
|
+
return resolve(cwd, LEGACY_CONFIG_NAME);
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
5
14
|
|
|
15
|
+
export interface VeluSeparator {
|
|
16
|
+
separator: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VeluLink {
|
|
20
|
+
href: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VeluAnchor {
|
|
26
|
+
anchor: string;
|
|
27
|
+
href?: string;
|
|
28
|
+
icon?: string;
|
|
29
|
+
color?: {
|
|
30
|
+
light: string;
|
|
31
|
+
dark: string;
|
|
32
|
+
};
|
|
33
|
+
tabs?: VeluTab[];
|
|
34
|
+
hidden?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface VeluGlobalTab {
|
|
38
|
+
tab: string;
|
|
39
|
+
href: string;
|
|
40
|
+
icon?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
export interface VeluGroup {
|
|
7
44
|
group: string;
|
|
8
45
|
slug: string;
|
|
9
46
|
icon?: string;
|
|
10
47
|
tag?: string;
|
|
11
48
|
expanded?: boolean;
|
|
12
|
-
|
|
49
|
+
description?: string;
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
13
52
|
}
|
|
14
53
|
|
|
15
54
|
export interface VeluTab {
|
|
@@ -17,7 +56,7 @@ export interface VeluTab {
|
|
|
17
56
|
slug: string;
|
|
18
57
|
icon?: string;
|
|
19
58
|
href?: string;
|
|
20
|
-
pages?: string[];
|
|
59
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
21
60
|
groups?: VeluGroup[];
|
|
22
61
|
}
|
|
23
62
|
|
|
@@ -28,7 +67,12 @@ export interface VeluConfig {
|
|
|
28
67
|
appearance?: 'system' | 'light' | 'dark';
|
|
29
68
|
styling?: { codeblocks?: { theme?: string | { light: string; dark: string } } };
|
|
30
69
|
navigation: {
|
|
31
|
-
tabs
|
|
70
|
+
tabs?: VeluTab[];
|
|
71
|
+
anchors?: VeluAnchor[];
|
|
72
|
+
global?: {
|
|
73
|
+
anchors?: VeluAnchor[];
|
|
74
|
+
tabs?: VeluGlobalTab[];
|
|
75
|
+
};
|
|
32
76
|
};
|
|
33
77
|
}
|
|
34
78
|
|
|
@@ -46,9 +90,9 @@ let _cachedConfig: VeluConfig | null = null;
|
|
|
46
90
|
|
|
47
91
|
export function loadVeluConfig(): VeluConfig {
|
|
48
92
|
if (_cachedConfig) return _cachedConfig;
|
|
49
|
-
const configPath =
|
|
93
|
+
const configPath = resolveConfigPath(process.cwd());
|
|
50
94
|
const raw = readFileSync(configPath, 'utf-8');
|
|
51
|
-
_cachedConfig = JSON.parse(raw);
|
|
95
|
+
_cachedConfig = normalizeConfigNavigation(JSON.parse(raw));
|
|
52
96
|
return _cachedConfig!;
|
|
53
97
|
}
|
|
54
98
|
|
|
@@ -64,7 +108,7 @@ function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
|
|
|
64
108
|
for (const item of group.pages) {
|
|
65
109
|
if (typeof item === 'string') {
|
|
66
110
|
items.push(tabSlug + '/' + group.slug + '/' + pageBasename(item));
|
|
67
|
-
} else {
|
|
111
|
+
} else if (isGroup(item)) {
|
|
68
112
|
items.push(veluGroupToSidebar(item, tabSlug));
|
|
69
113
|
}
|
|
70
114
|
}
|
|
@@ -74,10 +118,18 @@ function veluGroupToSidebar(group: VeluGroup, tabSlug: string): any {
|
|
|
74
118
|
return result;
|
|
75
119
|
}
|
|
76
120
|
|
|
121
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
122
|
+
return typeof item === 'object' && item !== null && 'group' in item;
|
|
123
|
+
}
|
|
124
|
+
|
|
77
125
|
/** Get the first page dest path for a tab */
|
|
78
126
|
function firstTabPage(tab: VeluTab): string | undefined {
|
|
79
|
-
if (tab.pages
|
|
80
|
-
|
|
127
|
+
if (tab.pages) {
|
|
128
|
+
for (const item of tab.pages) {
|
|
129
|
+
if (typeof item === 'string') {
|
|
130
|
+
return tab.slug + '/' + pageBasename(item);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
81
133
|
}
|
|
82
134
|
if (tab.groups) {
|
|
83
135
|
for (const g of tab.groups) {
|
|
@@ -91,25 +143,29 @@ function firstTabPage(tab: VeluTab): string | undefined {
|
|
|
91
143
|
function firstGroupPage(group: VeluGroup, tabSlug: string): string | undefined {
|
|
92
144
|
for (const item of group.pages) {
|
|
93
145
|
if (typeof item === 'string') return tabSlug + '/' + group.slug + '/' + pageBasename(item);
|
|
94
|
-
|
|
95
|
-
|
|
146
|
+
if (isGroup(item)) {
|
|
147
|
+
const nested = firstGroupPage(item, tabSlug);
|
|
148
|
+
if (nested) return nested;
|
|
149
|
+
}
|
|
96
150
|
}
|
|
97
151
|
return undefined;
|
|
98
152
|
}
|
|
99
153
|
|
|
100
154
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
101
155
|
|
|
102
|
-
/** Build the full Starlight sidebar array from velu.json */
|
|
156
|
+
/** Build the full Starlight sidebar array from docs.json/velu.json */
|
|
103
157
|
export function getSidebar(): any[] {
|
|
104
158
|
const config = loadVeluConfig();
|
|
105
159
|
const sidebar: any[] = [];
|
|
106
160
|
|
|
107
|
-
for (const tab of config.navigation.tabs) {
|
|
161
|
+
for (const tab of config.navigation.tabs ?? []) {
|
|
108
162
|
if (tab.href) continue;
|
|
109
163
|
const items: any[] = [];
|
|
110
164
|
if (tab.groups) for (const g of tab.groups) items.push(veluGroupToSidebar(g, tab.slug));
|
|
111
165
|
if (tab.pages) {
|
|
112
|
-
for (const p of tab.pages)
|
|
166
|
+
for (const p of tab.pages) {
|
|
167
|
+
if (typeof p === 'string') items.push(tab.slug + '/' + pageBasename(p));
|
|
168
|
+
}
|
|
113
169
|
}
|
|
114
170
|
sidebar.push({ label: tab.tab, items });
|
|
115
171
|
}
|
|
@@ -122,7 +178,7 @@ export function getTabs(): TabMeta[] {
|
|
|
122
178
|
const config = loadVeluConfig();
|
|
123
179
|
const tabs: TabMeta[] = [];
|
|
124
180
|
|
|
125
|
-
for (const tab of config.navigation.tabs) {
|
|
181
|
+
for (const tab of config.navigation.tabs ?? []) {
|
|
126
182
|
if (tab.href) {
|
|
127
183
|
tabs.push({ label: tab.tab, icon: tab.icon, href: tab.href, slugs: [] });
|
|
128
184
|
} else {
|
|
@@ -144,10 +200,35 @@ export function getTabSidebarMap(): Record<string, string[]> {
|
|
|
144
200
|
const config = loadVeluConfig();
|
|
145
201
|
const map: Record<string, string[]> = {};
|
|
146
202
|
|
|
147
|
-
for (const tab of config.navigation.tabs) {
|
|
203
|
+
for (const tab of config.navigation.tabs ?? []) {
|
|
148
204
|
if (tab.href) continue;
|
|
149
205
|
map[tab.slug] = [tab.tab];
|
|
150
206
|
}
|
|
151
207
|
|
|
152
208
|
return map;
|
|
153
209
|
}
|
|
210
|
+
|
|
211
|
+
/** Get all anchors (navigation.anchors + navigation.global.anchors), excluding hidden ones */
|
|
212
|
+
export function getAnchors(): VeluAnchor[] {
|
|
213
|
+
const config = loadVeluConfig();
|
|
214
|
+
const anchors: VeluAnchor[] = [];
|
|
215
|
+
if (config.navigation.anchors) {
|
|
216
|
+
anchors.push(...config.navigation.anchors.filter((a) => typeof a.href === 'string' && a.href.length > 0 && !a.hidden));
|
|
217
|
+
}
|
|
218
|
+
if (config.navigation.global?.anchors) {
|
|
219
|
+
anchors.push(...config.navigation.global.anchors.filter((a) => typeof a.href === 'string' && a.href.length > 0 && !a.hidden));
|
|
220
|
+
}
|
|
221
|
+
return anchors;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Get external tab links for the navbar */
|
|
225
|
+
export function getExternalTabs(): { label: string; href: string; icon?: string }[] {
|
|
226
|
+
const config = loadVeluConfig();
|
|
227
|
+
const tabLinks = (config.navigation.tabs ?? [])
|
|
228
|
+
.filter((tab) => !!tab.href)
|
|
229
|
+
.map((tab) => ({ label: tab.tab, href: tab.href!, icon: tab.icon }));
|
|
230
|
+
const globalLinks = (config.navigation.global?.tabs ?? [])
|
|
231
|
+
.filter((tab) => !!tab.href)
|
|
232
|
+
.map((tab) => ({ label: tab.tab, href: tab.href, icon: tab.icon }));
|
|
233
|
+
return [...tabLinks, ...globalLinks];
|
|
234
|
+
}
|