@aravindc26/velu 0.11.0 → 0.11.3

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.
Files changed (60) hide show
  1. package/package.json +15 -6
  2. package/schema/velu.schema.json +1251 -115
  3. package/src/build.ts +1121 -304
  4. package/src/cli.ts +90 -26
  5. package/src/engine/_server.mjs +1684 -277
  6. package/src/engine/app/(docs)/[...slug]/layout.tsx +371 -0
  7. package/src/engine/app/(docs)/[...slug]/page.tsx +926 -0
  8. package/src/engine/app/api/proxy/route.ts +23 -0
  9. package/src/engine/app/copy-page.css +59 -1
  10. package/src/engine/app/global.css +3157 -3
  11. package/src/engine/app/layout.tsx +56 -1
  12. package/src/engine/app/llms-file/route.ts +87 -0
  13. package/src/engine/app/llms-full-file/route.ts +62 -0
  14. package/src/engine/app/md-file/[...slug]/route.ts +409 -0
  15. package/src/engine/app/page.tsx +45 -0
  16. package/src/engine/app/robots.txt/route.ts +63 -0
  17. package/src/engine/app/rss-file/[...slug]/route.ts +169 -0
  18. package/src/engine/app/sitemap.xml/route.ts +82 -0
  19. package/src/engine/components/assistant.tsx +16 -5
  20. package/src/engine/components/changelog-filters.tsx +114 -0
  21. package/src/engine/components/code-group.tsx +383 -0
  22. package/src/engine/components/color.tsx +118 -0
  23. package/src/engine/components/expandable.tsx +77 -0
  24. package/src/engine/components/icon.tsx +136 -0
  25. package/src/engine/components/image-zoom-fallback.tsx +147 -0
  26. package/src/engine/components/image.tsx +111 -0
  27. package/src/engine/components/manual-api-playground.tsx +154 -0
  28. package/src/engine/components/mermaid.tsx +142 -0
  29. package/src/engine/components/openapi-toc-sync.tsx +59 -0
  30. package/src/engine/components/openapi.tsx +1682 -0
  31. package/src/engine/components/page-feedback.tsx +153 -0
  32. package/src/engine/components/product-switcher.tsx +27 -3
  33. package/src/engine/components/prompt.tsx +90 -0
  34. package/src/engine/components/providers.tsx +1 -6
  35. package/src/engine/components/search.tsx +4 -0
  36. package/src/engine/components/sidebar-links.tsx +13 -15
  37. package/src/engine/components/synced-tabs.tsx +57 -0
  38. package/src/engine/components/toc-examples.tsx +110 -0
  39. package/src/engine/components/view.tsx +344 -0
  40. package/src/engine/generated/redirects.ts +3 -0
  41. package/src/engine/lib/changelog.ts +246 -0
  42. package/src/engine/lib/layout.shared.ts +30 -2
  43. package/src/engine/lib/llms.ts +444 -0
  44. package/src/engine/lib/navigation-normalize.mjs +481 -412
  45. package/src/engine/lib/navigation-normalize.ts +261 -54
  46. package/src/engine/lib/redirects.ts +194 -0
  47. package/src/engine/lib/source.ts +107 -4
  48. package/src/engine/lib/velu.ts +368 -2
  49. package/src/engine/mdx-components.tsx +648 -0
  50. package/src/engine/middleware.ts +66 -0
  51. package/src/engine/public/icons/cursor-dark.svg +12 -0
  52. package/src/engine/public/icons/cursor-light.svg +12 -0
  53. package/src/engine/source.config.ts +98 -1
  54. package/src/engine/src/components/PageTitle.astro +16 -5
  55. package/src/engine/src/lib/velu.ts +11 -3
  56. package/src/navigation-normalize.ts +252 -54
  57. package/src/themes.ts +6 -6
  58. package/src/validate.ts +119 -6
  59. package/src/engine/app/(docs)/[[...slug]]/layout.tsx +0 -87
  60. package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -146
@@ -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
+ };
@@ -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 user scrolls to the very bottom
409
- window.addEventListener('scroll', function() {
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
- if (docHeight <= winHeight + 10) return; // short pages: always show
415
- if (docHeight - scrollTop - winHeight < 60) {
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
- }, { passive: true });
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,6 +1,14 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
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
+ }
4
12
 
5
13
  // ── Types ───────────────────────────────────────────────────────────────────
6
14
 
@@ -82,7 +90,7 @@ let _cachedConfig: VeluConfig | null = null;
82
90
 
83
91
  export function loadVeluConfig(): VeluConfig {
84
92
  if (_cachedConfig) return _cachedConfig;
85
- const configPath = resolve(process.cwd(), 'velu.json');
93
+ const configPath = resolveConfigPath(process.cwd());
86
94
  const raw = readFileSync(configPath, 'utf-8');
87
95
  _cachedConfig = normalizeConfigNavigation(JSON.parse(raw));
88
96
  return _cachedConfig!;
@@ -145,7 +153,7 @@ function firstGroupPage(group: VeluGroup, tabSlug: string): string | undefined {
145
153
 
146
154
  // ── Public API ──────────────────────────────────────────────────────────────
147
155
 
148
- /** Build the full Starlight sidebar array from velu.json */
156
+ /** Build the full Starlight sidebar array from docs.json/velu.json */
149
157
  export function getSidebar(): any[] {
150
158
  const config = loadVeluConfig();
151
159
  const sidebar: any[] = [];