@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.
Files changed (65) hide show
  1. package/package.json +15 -6
  2. package/schema/velu.schema.json +1864 -30
  3. package/src/build.ts +1161 -180
  4. package/src/cli.ts +121 -16
  5. package/src/engine/_server.mjs +1708 -192
  6. package/src/engine/app/(docs)/[...slug]/layout.tsx +377 -0
  7. package/src/engine/app/(docs)/[...slug]/page.tsx +917 -0
  8. package/src/engine/app/(docs)/layout.tsx +1 -13
  9. package/src/engine/app/api/proxy/route.ts +23 -0
  10. package/src/engine/app/copy-page.css +59 -1
  11. package/src/engine/app/global.css +3487 -6
  12. package/src/engine/app/layout.tsx +59 -8
  13. package/src/engine/app/llms-file/route.ts +87 -0
  14. package/src/engine/app/llms-full-file/route.ts +62 -0
  15. package/src/engine/app/md-file/[...slug]/route.ts +409 -0
  16. package/src/engine/app/page.tsx +45 -0
  17. package/src/engine/app/robots.txt/route.ts +61 -0
  18. package/src/engine/app/rss-file/[...slug]/route.ts +176 -0
  19. package/src/engine/app/search.css +20 -0
  20. package/src/engine/app/sitemap.xml/route.ts +80 -0
  21. package/src/engine/components/assistant.tsx +16 -5
  22. package/src/engine/components/changelog-filters.tsx +114 -0
  23. package/src/engine/components/code-group.tsx +383 -0
  24. package/src/engine/components/color.tsx +118 -0
  25. package/src/engine/components/expandable.tsx +77 -0
  26. package/src/engine/components/icon.tsx +136 -0
  27. package/src/engine/components/image-zoom-fallback.tsx +147 -0
  28. package/src/engine/components/image.tsx +111 -0
  29. package/src/engine/components/lang-switcher.tsx +95 -0
  30. package/src/engine/components/manual-api-playground.tsx +154 -0
  31. package/src/engine/components/mermaid.tsx +142 -0
  32. package/src/engine/components/openapi-toc-sync.tsx +59 -0
  33. package/src/engine/components/openapi.tsx +1679 -0
  34. package/src/engine/components/page-feedback.tsx +153 -0
  35. package/src/engine/components/product-switcher.tsx +102 -0
  36. package/src/engine/components/prompt.tsx +90 -0
  37. package/src/engine/components/providers.tsx +21 -0
  38. package/src/engine/components/search.tsx +70 -3
  39. package/src/engine/components/sidebar-links.tsx +49 -0
  40. package/src/engine/components/synced-tabs.tsx +57 -0
  41. package/src/engine/components/theme-toggle.tsx +39 -0
  42. package/src/engine/components/toc-examples.tsx +110 -0
  43. package/src/engine/components/version-switcher.tsx +89 -0
  44. package/src/engine/components/view.tsx +344 -0
  45. package/src/engine/generated/redirects.ts +3 -0
  46. package/src/engine/lib/changelog.ts +246 -0
  47. package/src/engine/lib/layout.shared.ts +57 -7
  48. package/src/engine/lib/llms.ts +444 -0
  49. package/src/engine/lib/navigation-normalize.mjs +525 -0
  50. package/src/engine/lib/navigation-normalize.ts +695 -0
  51. package/src/engine/lib/redirects.ts +194 -0
  52. package/src/engine/lib/source.ts +121 -4
  53. package/src/engine/lib/velu.ts +635 -5
  54. package/src/engine/mdx-components.tsx +648 -0
  55. package/src/engine/middleware.ts +66 -0
  56. package/src/engine/next.config.mjs +2 -2
  57. package/src/engine/public/icons/cursor-dark.svg +12 -0
  58. package/src/engine/public/icons/cursor-light.svg +12 -0
  59. package/src/engine/source.config.ts +98 -1
  60. package/src/engine/src/components/PageTitle.astro +16 -5
  61. package/src/engine/src/lib/velu.ts +97 -16
  62. package/src/navigation-normalize.ts +686 -0
  63. package/src/themes.ts +6 -6
  64. package/src/validate.ts +235 -24
  65. package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -69
@@ -0,0 +1,246 @@
1
+ export interface ChangelogTocItem {
2
+ title: string;
3
+ url: string;
4
+ depth: number;
5
+ }
6
+
7
+ export interface ChangelogUpdateEntry {
8
+ label: string;
9
+ anchor: string;
10
+ date?: string;
11
+ description?: string;
12
+ tags: string[];
13
+ contentMarkdown: string;
14
+ rssTitle?: string;
15
+ rssDescription?: string;
16
+ }
17
+
18
+ export interface ChangelogHeadingEntry {
19
+ title: string;
20
+ anchor: string;
21
+ contentMarkdown: string;
22
+ }
23
+
24
+ export interface ParsedChangelogData {
25
+ toc: ChangelogTocItem[];
26
+ tags: string[];
27
+ updates: ChangelogUpdateEntry[];
28
+ }
29
+
30
+ interface ParsedRssProp {
31
+ title?: string;
32
+ description?: string;
33
+ }
34
+
35
+ const STRING_LITERAL = /"(.*?)"|'(.*?)'/;
36
+
37
+ export function slugifyUpdateLabel(value: string): string {
38
+ const base = value
39
+ .toLowerCase()
40
+ .trim()
41
+ .replace(/[^a-z0-9]+/g, '-')
42
+ .replace(/^-+|-+$/g, '');
43
+ return `update-${base || 'item'}`;
44
+ }
45
+
46
+ export function slugifyHeading(value: string): string {
47
+ return value
48
+ .toLowerCase()
49
+ .trim()
50
+ .replace(/[^a-z0-9]+/g, '-')
51
+ .replace(/^-+|-+$/g, '') || 'section';
52
+ }
53
+
54
+ function parseStringProp(attributes: string, name: string): string | undefined {
55
+ const regex = new RegExp(`\\b${name}\\s*=\\s*(\"([^\"]+)\"|'([^']+)')`, 'i');
56
+ const match = attributes.match(regex);
57
+ if (!match) return undefined;
58
+ return (match[2] ?? match[3] ?? '').trim() || undefined;
59
+ }
60
+
61
+ function parseTags(attributes: string): string[] {
62
+ const direct = parseStringProp(attributes, 'tags');
63
+ if (direct) return [direct];
64
+
65
+ const match = attributes.match(/\btags\s*=\s*\{\s*\[([\s\S]*?)\]\s*\}/i);
66
+ if (!match) return [];
67
+
68
+ const tokens = match[1].split(',').map((entry) => entry.trim()).filter(Boolean);
69
+ const tags: string[] = [];
70
+ for (const token of tokens) {
71
+ const literal = token.match(STRING_LITERAL);
72
+ const value = (literal?.[1] ?? literal?.[2] ?? '').trim();
73
+ if (value) tags.push(value);
74
+ }
75
+ return tags;
76
+ }
77
+
78
+ function parseRss(attributes: string): ParsedRssProp {
79
+ const inline = parseStringProp(attributes, 'rss');
80
+ if (inline) {
81
+ return { description: inline };
82
+ }
83
+
84
+ const objectMatch = attributes.match(/\brss\s*=\s*\{\s*\{([\s\S]*?)\}\s*\}/i);
85
+ if (!objectMatch) return {};
86
+ const body = objectMatch[1];
87
+
88
+ return {
89
+ title: parseStringProp(body, 'title'),
90
+ description: parseStringProp(body, 'description'),
91
+ };
92
+ }
93
+
94
+ export function markdownToPlainText(markdown: string): string {
95
+ return markdown
96
+ .replace(/```[\s\S]*?```/g, '')
97
+ .replace(/<[^>]+>/g, '')
98
+ .replace(/\{[^}]+\}/g, '')
99
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
100
+ .replace(/`([^`]+)`/g, '$1')
101
+ .replace(/\n{3,}/g, '\n\n')
102
+ .trim();
103
+ }
104
+
105
+ function parseHeadings(markdown: string): ChangelogHeadingEntry[] {
106
+ if (!markdown.trim()) return [];
107
+
108
+ const matches = Array.from(markdown.matchAll(/^#{1,6}\s+(.+)$/gm));
109
+ if (matches.length === 0) return [];
110
+
111
+ const seen = new Map<string, number>();
112
+ const entries: ChangelogHeadingEntry[] = [];
113
+
114
+ for (let index = 0; index < matches.length; index += 1) {
115
+ const current = matches[index];
116
+ const next = matches[index + 1];
117
+ const rawTitle = (current[1] ?? '').trim();
118
+ const title = markdownToPlainText(rawTitle) || 'Update';
119
+
120
+ const baseSlug = slugifyHeading(title);
121
+ const duplicateCount = seen.get(baseSlug) ?? 0;
122
+ seen.set(baseSlug, duplicateCount + 1);
123
+ const anchor = duplicateCount === 0 ? baseSlug : `${baseSlug}-${duplicateCount}`;
124
+
125
+ const bodyStart = (current.index ?? 0) + current[0].length;
126
+ const bodyEnd = next?.index ?? markdown.length;
127
+ const body = markdown.slice(bodyStart, bodyEnd).trim();
128
+
129
+ entries.push({
130
+ title,
131
+ anchor,
132
+ contentMarkdown: body,
133
+ });
134
+ }
135
+
136
+ return entries;
137
+ }
138
+
139
+ export function parseChangelogFromMarkdown(markdown: string | undefined): ParsedChangelogData {
140
+ if (!markdown) return { toc: [], tags: [], updates: [] };
141
+
142
+ const searchable = markdown
143
+ .replace(/```[\s\S]*?```/g, '')
144
+ .replace(/~~~[\s\S]*?~~~/g, '');
145
+
146
+ const updates: ChangelogUpdateEntry[] = [];
147
+ const tagSet = new Set<string>();
148
+ const regex = /<Update\b([^>]*)>([\s\S]*?)<\/Update>/gi;
149
+
150
+ for (const match of searchable.matchAll(regex)) {
151
+ const attrs = match[1] ?? '';
152
+ const body = (match[2] ?? '').trim();
153
+ const date = parseStringProp(attrs, 'date');
154
+ const label = parseStringProp(attrs, 'label') ?? date ?? 'Update';
155
+ const description = parseStringProp(attrs, 'description');
156
+ const tags = parseTags(attrs);
157
+ const rss = parseRss(attrs);
158
+ const anchor = slugifyUpdateLabel(label);
159
+
160
+ for (const tag of tags) tagSet.add(tag);
161
+
162
+ updates.push({
163
+ label,
164
+ anchor,
165
+ date,
166
+ description,
167
+ tags,
168
+ contentMarkdown: body,
169
+ rssTitle: rss.title,
170
+ rssDescription: rss.description,
171
+ });
172
+ }
173
+
174
+ return {
175
+ toc: updates.map((update) => ({
176
+ title: update.label,
177
+ url: `#${update.anchor}`,
178
+ depth: 2,
179
+ })),
180
+ tags: Array.from(tagSet),
181
+ updates,
182
+ };
183
+ }
184
+
185
+ export function parseFrontmatterValue(markdown: string | undefined, key: string): string | undefined {
186
+ const frontmatterMatch = markdown?.match(/^---\r?\n([\s\S]*?)\r?\n---/);
187
+ if (!frontmatterMatch) return undefined;
188
+ const frontmatter = frontmatterMatch[1];
189
+ const line = frontmatter
190
+ .split(/\r?\n/)
191
+ .find((entry) => entry.trim().toLowerCase().startsWith(`${key.toLowerCase()}:`));
192
+ if (!line) return undefined;
193
+
194
+ const raw = line.slice(line.indexOf(':') + 1).trim();
195
+ if (!raw) return undefined;
196
+ const literal = raw.match(STRING_LITERAL);
197
+ return (literal?.[1] ?? literal?.[2] ?? raw).trim();
198
+ }
199
+
200
+ export function parseFrontmatterBoolean(markdown: string | undefined, key: string): boolean {
201
+ const value = parseFrontmatterValue(markdown, key);
202
+ if (!value) return false;
203
+ const normalized = value.trim().toLowerCase();
204
+ return normalized === 'true' || normalized === 'yes' || normalized === '1';
205
+ }
206
+
207
+ export function getUpdateRssEntries(update: ChangelogUpdateEntry): Array<{
208
+ title: string;
209
+ anchor: string;
210
+ description: string;
211
+ }> {
212
+ if (update.rssTitle || update.rssDescription) {
213
+ return [
214
+ {
215
+ title: update.rssTitle?.trim() || update.label,
216
+ anchor: update.anchor,
217
+ description: toRssDescription(update),
218
+ },
219
+ ];
220
+ }
221
+
222
+ const headings = parseHeadings(update.contentMarkdown);
223
+ if (headings.length > 0) {
224
+ return headings.map((heading) => ({
225
+ title: heading.title,
226
+ anchor: heading.anchor,
227
+ description: markdownToPlainText(heading.contentMarkdown) || toRssDescription(update),
228
+ }));
229
+ }
230
+
231
+ return [
232
+ {
233
+ title: update.label,
234
+ anchor: update.anchor,
235
+ description: toRssDescription(update),
236
+ },
237
+ ];
238
+ }
239
+
240
+ export function toRssDescription(update: ChangelogUpdateEntry): string {
241
+ if (update.rssDescription && update.rssDescription.trim()) return update.rssDescription.trim();
242
+ if (update.description && update.description.trim()) return update.description.trim();
243
+
244
+ const plain = markdownToPlainText(update.contentMarkdown);
245
+ return plain || update.label;
246
+ }
@@ -1,17 +1,67 @@
1
1
  import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
2
- import { getExternalTabs } from '@/lib/velu';
2
+ import { createElement } from 'react';
3
+ import { VersionSwitcher } from '@/components/version-switcher';
4
+ import { getExternalTabs, getNavbarAnchors, getSiteLogoAsset, getSiteName, getVersionOptions } from '@/lib/velu';
3
5
 
4
6
  export function baseOptions(): BaseLayoutProps {
5
7
  const externalTabs = getExternalTabs();
6
- const links = externalTabs.map((tab: { label: string; href: string }) => ({
7
- text: tab.label,
8
- url: tab.href,
9
- secondary: false,
10
- }));
8
+ const navAnchors = getNavbarAnchors();
9
+ const versions = getVersionOptions();
10
+ const siteName = getSiteName();
11
+ const logo = getSiteLogoAsset();
12
+ const lightLogo = logo.light ?? logo.dark;
13
+ const darkLogo = logo.dark ?? logo.light;
14
+ const logoHref = typeof logo.href === 'string' && logo.href.trim().length > 0 ? logo.href.trim() : '/';
15
+
16
+ const navTitle =
17
+ lightLogo || darkLogo
18
+ ? createElement(
19
+ 'span',
20
+ { className: 'velu-nav-brand' },
21
+ lightLogo
22
+ ? createElement('img', {
23
+ src: lightLogo,
24
+ alt: siteName,
25
+ className: 'velu-nav-logo velu-nav-logo-light',
26
+ })
27
+ : null,
28
+ darkLogo
29
+ ? createElement('img', {
30
+ src: darkLogo,
31
+ alt: siteName,
32
+ className: 'velu-nav-logo velu-nav-logo-dark',
33
+ })
34
+ : null,
35
+ )
36
+ : siteName;
37
+
38
+ const links = [
39
+ ...externalTabs.map((tab: { label: string; href: string }) => ({
40
+ text: tab.label,
41
+ url: tab.href,
42
+ secondary: false,
43
+ })),
44
+ ...navAnchors
45
+ .filter((a): a is { anchor: string; href: string } => typeof a.href === 'string' && a.href.length > 0)
46
+ .map((a) => ({
47
+ text: a.anchor,
48
+ url: a.href,
49
+ secondary: true,
50
+ })),
51
+ ];
11
52
 
12
53
  return {
13
54
  nav: {
14
- title: 'Velu Docs',
55
+ title: navTitle,
56
+ url: logoHref,
57
+ children:
58
+ versions.length > 1
59
+ ? createElement(
60
+ 'div',
61
+ { className: 'velu-header-version-switcher' },
62
+ createElement(VersionSwitcher, { versions })
63
+ )
64
+ : undefined,
15
65
  },
16
66
  links,
17
67
  };