@alliance-droid/svelte-docs-system 0.0.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 (134) hide show
  1. package/COMPONENTS.md +365 -0
  2. package/COVERAGE_REPORT.md +663 -0
  3. package/README.md +42 -0
  4. package/SEARCH_VERIFICATION.md +229 -0
  5. package/TEST_SUMMARY.md +344 -0
  6. package/bin/init.js +821 -0
  7. package/docs/E2E_TESTS.md +354 -0
  8. package/docs/TESTING.md +754 -0
  9. package/docs/de/index.md +41 -0
  10. package/docs/en/COMPONENTS.md +443 -0
  11. package/docs/en/api/examples.md +100 -0
  12. package/docs/en/api/overview.md +69 -0
  13. package/docs/en/components/index.md +622 -0
  14. package/docs/en/config/navigation.md +505 -0
  15. package/docs/en/config/theme-and-colors.md +395 -0
  16. package/docs/en/getting-started/integration.md +406 -0
  17. package/docs/en/guides/common-setups.md +651 -0
  18. package/docs/en/index.md +243 -0
  19. package/docs/en/markdown.md +102 -0
  20. package/docs/en/routing.md +64 -0
  21. package/docs/en/setup.md +52 -0
  22. package/docs/en/troubleshooting.md +704 -0
  23. package/docs/es/index.md +41 -0
  24. package/docs/fr/index.md +41 -0
  25. package/docs/ja/index.md +41 -0
  26. package/package.json +40 -0
  27. package/pagefind.toml +8 -0
  28. package/postcss.config.js +5 -0
  29. package/src/app.css +119 -0
  30. package/src/app.d.ts +13 -0
  31. package/src/app.html +11 -0
  32. package/src/lib/assets/favicon.svg +1 -0
  33. package/src/lib/components/APITable.svelte +120 -0
  34. package/src/lib/components/APITable.test.ts +153 -0
  35. package/src/lib/components/Breadcrumbs.svelte +85 -0
  36. package/src/lib/components/Breadcrumbs.test.ts +148 -0
  37. package/src/lib/components/Callout.svelte +60 -0
  38. package/src/lib/components/Callout.test.ts +100 -0
  39. package/src/lib/components/CodeBlock.svelte +68 -0
  40. package/src/lib/components/CodeBlock.test.ts +133 -0
  41. package/src/lib/components/DocLayout.svelte +84 -0
  42. package/src/lib/components/Footer.svelte +78 -0
  43. package/src/lib/components/Image.svelte +100 -0
  44. package/src/lib/components/Image.test.ts +163 -0
  45. package/src/lib/components/Navbar.svelte +141 -0
  46. package/src/lib/components/Search.svelte +248 -0
  47. package/src/lib/components/Sidebar.svelte +110 -0
  48. package/src/lib/components/Tabs.svelte +48 -0
  49. package/src/lib/components/Tabs.test.ts +102 -0
  50. package/src/lib/config.test.ts +140 -0
  51. package/src/lib/config.ts +179 -0
  52. package/src/lib/configIntegration.test.ts +272 -0
  53. package/src/lib/configLoader.ts +231 -0
  54. package/src/lib/configParser.test.ts +217 -0
  55. package/src/lib/configParser.ts +234 -0
  56. package/src/lib/index.ts +34 -0
  57. package/src/lib/integration.test.ts +426 -0
  58. package/src/lib/navigationBuilder.test.ts +338 -0
  59. package/src/lib/navigationBuilder.ts +268 -0
  60. package/src/lib/performance.test.ts +369 -0
  61. package/src/lib/routing.test.ts +202 -0
  62. package/src/lib/routing.ts +127 -0
  63. package/src/lib/search-functionality.test.ts +493 -0
  64. package/src/lib/stores/i18n.test.ts +180 -0
  65. package/src/lib/stores/i18n.ts +143 -0
  66. package/src/lib/stores/nav.ts +36 -0
  67. package/src/lib/stores/search.test.ts +140 -0
  68. package/src/lib/stores/search.ts +162 -0
  69. package/src/lib/stores/theme.ts +59 -0
  70. package/src/lib/stores/version.test.ts +139 -0
  71. package/src/lib/stores/version.ts +111 -0
  72. package/src/lib/themeCustomization.test.ts +223 -0
  73. package/src/lib/themeCustomization.ts +212 -0
  74. package/src/lib/utils/highlight.test.ts +136 -0
  75. package/src/lib/utils/highlight.ts +100 -0
  76. package/src/lib/utils/index.ts +7 -0
  77. package/src/lib/utils/markdown.test.ts +357 -0
  78. package/src/lib/utils/markdown.ts +77 -0
  79. package/src/routes/+layout.server.ts +1 -0
  80. package/src/routes/+layout.svelte +28 -0
  81. package/src/routes/+page.svelte +165 -0
  82. package/static/robots.txt +3 -0
  83. package/svelte.config.js +18 -0
  84. package/tailwind.config.ts +55 -0
  85. package/template-starter/.github/workflows/build.yml +40 -0
  86. package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
  87. package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
  88. package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
  89. package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
  90. package/template-starter/README.md +320 -0
  91. package/template-starter/docs/_config.json +39 -0
  92. package/template-starter/docs/api/components.md +257 -0
  93. package/template-starter/docs/api/overview.md +169 -0
  94. package/template-starter/docs/guides/configuration.md +145 -0
  95. package/template-starter/docs/guides/github-pages-deployment.md +254 -0
  96. package/template-starter/docs/guides/netlify-deployment.md +159 -0
  97. package/template-starter/docs/guides/vercel-deployment.md +131 -0
  98. package/template-starter/docs/index.md +49 -0
  99. package/template-starter/docs/setup.md +149 -0
  100. package/template-starter/package.json +31 -0
  101. package/template-starter/pagefind.toml +3 -0
  102. package/template-starter/postcss.config.js +5 -0
  103. package/template-starter/src/app.css +34 -0
  104. package/template-starter/src/app.d.ts +13 -0
  105. package/template-starter/src/app.html +11 -0
  106. package/template-starter/src/lib/components/APITable.svelte +120 -0
  107. package/template-starter/src/lib/components/APITable.test.ts +19 -0
  108. package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
  109. package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
  110. package/template-starter/src/lib/components/Callout.svelte +60 -0
  111. package/template-starter/src/lib/components/Callout.test.ts +16 -0
  112. package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
  113. package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
  114. package/template-starter/src/lib/components/DocLayout.svelte +84 -0
  115. package/template-starter/src/lib/components/Footer.svelte +78 -0
  116. package/template-starter/src/lib/components/Image.svelte +100 -0
  117. package/template-starter/src/lib/components/Image.test.ts +15 -0
  118. package/template-starter/src/lib/components/Navbar.svelte +141 -0
  119. package/template-starter/src/lib/components/Search.svelte +248 -0
  120. package/template-starter/src/lib/components/Sidebar.svelte +110 -0
  121. package/template-starter/src/lib/components/Tabs.svelte +48 -0
  122. package/template-starter/src/lib/components/Tabs.test.ts +17 -0
  123. package/template-starter/src/lib/index.ts +15 -0
  124. package/template-starter/src/routes/+layout.svelte +28 -0
  125. package/template-starter/src/routes/+page.svelte +92 -0
  126. package/template-starter/svelte.config.js +17 -0
  127. package/template-starter/tailwind.config.ts +17 -0
  128. package/template-starter/tsconfig.json +13 -0
  129. package/template-starter/vite.config.ts +6 -0
  130. package/tests/e2e/example.spec.ts +345 -0
  131. package/tsconfig.json +20 -0
  132. package/vite.config.ts +6 -0
  133. package/vitest.config.ts +34 -0
  134. package/vitest.setup.ts +21 -0
@@ -0,0 +1,338 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ formatFileName,
4
+ parseFileInfo,
5
+ generateNavigationFromFiles,
6
+ validateNavigation,
7
+ filterFiles,
8
+ findNavItem,
9
+ getPrevNext,
10
+ } from './navigationBuilder';
11
+
12
+ describe('Navigation Builder', () => {
13
+ describe('formatFileName', () => {
14
+ it('should format file names to titles', () => {
15
+ expect(formatFileName('setup')).toBe('Setup');
16
+ expect(formatFileName('getting-started')).toBe('Getting Started');
17
+ expect(formatFileName('api_reference')).toBe('Api Reference');
18
+ expect(formatFileName('README')).toMatch(/readme/i);
19
+ });
20
+
21
+ it('should handle multiple word separators', () => {
22
+ expect(formatFileName('getting-started-guide')).toBe('Getting Started Guide');
23
+ expect(formatFileName('api_v2_reference')).toBe('Api V2 Reference');
24
+ });
25
+ });
26
+
27
+ describe('parseFileInfo', () => {
28
+ it('should parse file info from path', () => {
29
+ const info = parseFileInfo('docs/setup.md');
30
+ expect(info.path).toBe('docs/setup.md');
31
+ expect(info.name).toBe('setup');
32
+ expect(info.title).toBe('Setup');
33
+ expect(info.isIndex).toBe(false);
34
+ });
35
+
36
+ it('should detect index files', () => {
37
+ const info = parseFileInfo('docs/index.md');
38
+ expect(info.name).toBe('index');
39
+ expect(info.isIndex).toBe(true);
40
+ });
41
+
42
+ it('should handle nested paths', () => {
43
+ const info = parseFileInfo('docs/api/overview.md');
44
+ expect(info.name).toBe('overview');
45
+ expect(info.path).toBe('docs/api/overview.md');
46
+ });
47
+
48
+ it('should use frontmatter title if provided', () => {
49
+ const info = parseFileInfo('docs/setup.md', {
50
+ title: 'Custom Title',
51
+ });
52
+ expect(info.title).toBe('Custom Title');
53
+ });
54
+
55
+ it('should use frontmatter order if provided', () => {
56
+ const info = parseFileInfo('docs/setup.md', {
57
+ order: 5,
58
+ });
59
+ expect(info.order).toBe(5);
60
+ });
61
+ });
62
+
63
+ describe('generateNavigationFromFiles', () => {
64
+ it('should generate navigation from files', () => {
65
+ const files = [
66
+ parseFileInfo('docs/index.md'),
67
+ parseFileInfo('docs/setup.md'),
68
+ parseFileInfo('docs/api/overview.md'),
69
+ ];
70
+
71
+ const nav = generateNavigationFromFiles(files, '/docs');
72
+ expect(nav.length).toBeGreaterThan(0);
73
+ expect(nav[0].title).toBeDefined();
74
+ });
75
+
76
+ it('should create sections for directories', () => {
77
+ const files = [
78
+ parseFileInfo('docs/setup.md'),
79
+ parseFileInfo('docs/api/overview.md'),
80
+ parseFileInfo('docs/api/methods.md'),
81
+ ];
82
+
83
+ const nav = generateNavigationFromFiles(files, '/docs');
84
+ // Should have at least a root section and an api section
85
+ expect(nav.length).toBeGreaterThanOrEqual(2);
86
+ });
87
+
88
+ it('should handle custom base route', () => {
89
+ const files = [parseFileInfo('docs/setup.md')];
90
+ const nav = generateNavigationFromFiles(files, '/help');
91
+
92
+ const items = nav.flatMap((s) => s.items);
93
+ const setupItem = items.find((i) => i.label === 'Setup');
94
+ expect(setupItem?.href).toContain('/help');
95
+ });
96
+
97
+ it('should sort files with index first', () => {
98
+ const files = [
99
+ parseFileInfo('docs/setup.md'),
100
+ parseFileInfo('docs/index.md'),
101
+ parseFileInfo('docs/guide.md'),
102
+ ];
103
+
104
+ const nav = generateNavigationFromFiles(files, '/docs');
105
+ const section = nav.find((s) => s.title === 'Documentation');
106
+ if (section) {
107
+ // Index should not appear in the items list as it's handled specially
108
+ expect(section.items.length).toBeGreaterThan(0);
109
+ }
110
+ });
111
+
112
+ it('should respect order property', () => {
113
+ const files = [
114
+ parseFileInfo('docs/a.md', { order: 3 }),
115
+ parseFileInfo('docs/b.md', { order: 1 }),
116
+ parseFileInfo('docs/c.md', { order: 2 }),
117
+ ];
118
+
119
+ const nav = generateNavigationFromFiles(files, '/docs');
120
+ const section = nav.find((s) => s.title === 'Documentation');
121
+ if (section) {
122
+ expect(section.items[0].label).toContain('B'); // order 1
123
+ expect(section.items[1].label).toContain('C'); // order 2
124
+ expect(section.items[2].label).toContain('A'); // order 3
125
+ }
126
+ });
127
+ });
128
+
129
+ describe('validateNavigation', () => {
130
+ it('should validate valid navigation', () => {
131
+ const nav = {
132
+ title: 'My Docs',
133
+ sections: [
134
+ {
135
+ title: 'Getting Started',
136
+ items: [{ label: 'Setup', href: '/docs/setup' }],
137
+ },
138
+ ],
139
+ };
140
+
141
+ const result = validateNavigation(nav);
142
+ expect(result.valid).toBe(true);
143
+ expect(result.errors).toHaveLength(0);
144
+ });
145
+
146
+ it('should reject non-object navigation', () => {
147
+ const result = validateNavigation('not-object' as any);
148
+ expect(result.valid).toBe(false);
149
+ });
150
+
151
+ it('should validate section structure', () => {
152
+ const nav = {
153
+ sections: [
154
+ {
155
+ title: 'Section',
156
+ items: [{ label: 'Item' }], // Missing href, but that's ok
157
+ },
158
+ ],
159
+ };
160
+
161
+ const result = validateNavigation(nav);
162
+ expect(result.valid).toBe(true);
163
+ });
164
+
165
+ it('should reject invalid section structure', () => {
166
+ const nav = {
167
+ sections: [
168
+ {
169
+ // Missing title
170
+ items: [],
171
+ title: undefined as any,
172
+ },
173
+ ],
174
+ } as any;
175
+
176
+ const result = validateNavigation(nav);
177
+ expect(result.valid).toBe(false);
178
+ expect(result.errors.length).toBeGreaterThan(0);
179
+ });
180
+
181
+ it('should validate exclude paths is array', () => {
182
+ const nav = {
183
+ excludePaths: 'not-array' as any,
184
+ };
185
+
186
+ const result = validateNavigation(nav);
187
+ expect(result.valid).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe('filterFiles', () => {
192
+ it('should filter files by exclude patterns', () => {
193
+ const files = [
194
+ parseFileInfo('docs/setup.md'),
195
+ parseFileInfo('docs/draft.md'),
196
+ parseFileInfo('docs/api/overview.md'),
197
+ ];
198
+
199
+ const filtered = filterFiles(files, ['draft']);
200
+ expect(filtered.length).toBe(2);
201
+ expect(filtered.some((f) => f.name === 'draft')).toBe(false);
202
+ });
203
+
204
+ it('should exclude multiple patterns', () => {
205
+ const files = [
206
+ parseFileInfo('docs/setup.md'),
207
+ parseFileInfo('docs/draft.md'),
208
+ parseFileInfo('docs/private.md'),
209
+ ];
210
+
211
+ const filtered = filterFiles(files, ['draft', 'private']);
212
+ expect(filtered.length).toBe(1);
213
+ expect(filtered[0].name).toBe('setup');
214
+ });
215
+
216
+ it('should handle empty exclude list', () => {
217
+ const files = [parseFileInfo('docs/setup.md')];
218
+ const filtered = filterFiles(files, []);
219
+ expect(filtered).toEqual(files);
220
+ });
221
+ });
222
+
223
+ describe('findNavItem', () => {
224
+ it('should find nav item by href', () => {
225
+ const sections = [
226
+ {
227
+ title: 'Getting Started',
228
+ items: [{ label: 'Setup', href: '/docs/setup' }],
229
+ },
230
+ ];
231
+
232
+ const item = findNavItem(sections, '/docs/setup');
233
+ expect(item?.label).toBe('Setup');
234
+ });
235
+
236
+ it('should return null if not found', () => {
237
+ const sections = [
238
+ {
239
+ title: 'Getting Started',
240
+ items: [{ label: 'Setup', href: '/docs/setup' }],
241
+ },
242
+ ];
243
+
244
+ const item = findNavItem(sections, '/docs/missing');
245
+ expect(item).toBeNull();
246
+ });
247
+
248
+ it('should find items in nested children', () => {
249
+ const sections = [
250
+ {
251
+ title: 'Docs',
252
+ items: [
253
+ {
254
+ label: 'API',
255
+ children: [{ label: 'Reference', href: '/docs/api/reference' }],
256
+ },
257
+ ],
258
+ },
259
+ ];
260
+
261
+ const item = findNavItem(sections, '/docs/api/reference');
262
+ expect(item?.label).toBe('Reference');
263
+ });
264
+ });
265
+
266
+ describe('getPrevNext', () => {
267
+ it('should get previous and next items', () => {
268
+ const sections = [
269
+ {
270
+ title: 'Docs',
271
+ items: [
272
+ { label: 'First', href: '/docs/1' },
273
+ { label: 'Second', href: '/docs/2' },
274
+ { label: 'Third', href: '/docs/3' },
275
+ ],
276
+ },
277
+ ];
278
+
279
+ const result = getPrevNext(sections, '/docs/2');
280
+ expect(result.prev?.label).toBe('First');
281
+ expect(result.next?.label).toBe('Third');
282
+ });
283
+
284
+ it('should handle first item', () => {
285
+ const sections = [
286
+ {
287
+ title: 'Docs',
288
+ items: [
289
+ { label: 'First', href: '/docs/1' },
290
+ { label: 'Second', href: '/docs/2' },
291
+ ],
292
+ },
293
+ ];
294
+
295
+ const result = getPrevNext(sections, '/docs/1');
296
+ expect(result.prev).toBeNull();
297
+ expect(result.next?.label).toBe('Second');
298
+ });
299
+
300
+ it('should handle last item', () => {
301
+ const sections = [
302
+ {
303
+ title: 'Docs',
304
+ items: [
305
+ { label: 'First', href: '/docs/1' },
306
+ { label: 'Last', href: '/docs/2' },
307
+ ],
308
+ },
309
+ ];
310
+
311
+ const result = getPrevNext(sections, '/docs/2');
312
+ expect(result.prev?.label).toBe('First');
313
+ expect(result.next).toBeNull();
314
+ });
315
+
316
+ it('should work with nested items', () => {
317
+ const sections = [
318
+ {
319
+ title: 'Docs',
320
+ items: [
321
+ { label: 'First', href: '/docs/1' },
322
+ {
323
+ label: 'Group',
324
+ children: [
325
+ { label: 'Nested', href: '/docs/nested' },
326
+ { label: 'Nested2', href: '/docs/nested2' },
327
+ ],
328
+ },
329
+ { label: 'Last', href: '/docs/last' },
330
+ ],
331
+ },
332
+ ];
333
+
334
+ const result = getPrevNext(sections, '/docs/nested2');
335
+ expect(result.next?.label).toBe('Last');
336
+ });
337
+ });
338
+ });
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Navigation builder for generating navigation from folder structure or config
3
+ */
4
+
5
+ import type { NavItem, NavSection, NavigationConfig } from './config';
6
+
7
+ export interface FileInfo {
8
+ /** Full file path (e.g., docs/api/overview.md) */
9
+ path: string;
10
+ /** File name without extension (e.g., overview) */
11
+ name: string;
12
+ /** File title (from frontmatter or derived from name) */
13
+ title?: string;
14
+ /** File order (from frontmatter) */
15
+ order?: number;
16
+ /** Is this the index file? */
17
+ isIndex: boolean;
18
+ }
19
+
20
+ /**
21
+ * Convert a file name to a display title
22
+ * Example: "api-reference" -> "API Reference"
23
+ */
24
+ export function formatFileName(name: string): string {
25
+ return name
26
+ .replace(/[-_]/g, ' ') // Replace hyphens and underscores with spaces
27
+ .replace(/\b\w/g, (char) => char.toUpperCase()); // Capitalize first letter of each word
28
+ }
29
+
30
+ /**
31
+ * Parse file information from path
32
+ * Example: "docs/api/reference.md" -> { path: "docs/api/reference.md", name: "reference", isIndex: false }
33
+ */
34
+ export function parseFileInfo(filePath: string, fileMetadata?: { title?: string; order?: number }): FileInfo {
35
+ const cleanPath = filePath.replace(/^\.\//, ''); // Remove leading ./
36
+ const lastSlash = cleanPath.lastIndexOf('/');
37
+ const fileName = cleanPath.substring(lastSlash + 1);
38
+ const name = fileName.replace(/\.md$/, '');
39
+
40
+ return {
41
+ path: cleanPath,
42
+ name,
43
+ title: fileMetadata?.title || formatFileName(name),
44
+ order: fileMetadata?.order,
45
+ isIndex: name === 'index',
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Group files by directory
51
+ */
52
+ function groupFilesByDirectory(files: FileInfo[]): Map<string, FileInfo[]> {
53
+ const groups = new Map<string, FileInfo[]>();
54
+
55
+ files.forEach((file) => {
56
+ const match = file.path.match(/^docs\/(.+)\/[^/]+\.md$/);
57
+ const dir = match ? match[1] : 'root';
58
+
59
+ if (!groups.has(dir)) {
60
+ groups.set(dir, []);
61
+ }
62
+ groups.get(dir)!.push(file);
63
+ });
64
+
65
+ return groups;
66
+ }
67
+
68
+ /**
69
+ * Sort files by order, then alphabetically
70
+ */
71
+ function sortFiles(files: FileInfo[]): FileInfo[] {
72
+ return [...files].sort((a, b) => {
73
+ // Priority to index files at the top
74
+ if (a.isIndex && !b.isIndex) return -1;
75
+ if (!a.isIndex && b.isIndex) return 1;
76
+
77
+ // Then sort by order if specified
78
+ if (a.order !== undefined && b.order !== undefined) {
79
+ return a.order - b.order;
80
+ }
81
+ if (a.order !== undefined) return -1;
82
+ if (b.order !== undefined) return 1;
83
+
84
+ // Finally alphabetically
85
+ return a.name.localeCompare(b.name);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Generate navigation from a list of files
91
+ * Useful for auto-generating sidebar from folder structure
92
+ */
93
+ export function generateNavigationFromFiles(files: FileInfo[], baseRoute: string = '/docs'): NavSection[] {
94
+ const groups = groupFilesByDirectory(files);
95
+ const sections: NavSection[] = [];
96
+
97
+ // Process files in natural order
98
+ const sortedDirs = Array.from(groups.keys()).sort((a, b) => {
99
+ if (a === 'root') return -1;
100
+ if (b === 'root') return 1;
101
+ return a.localeCompare(b);
102
+ });
103
+
104
+ sortedDirs.forEach((dir) => {
105
+ const dirFiles = sortFiles(groups.get(dir)!);
106
+
107
+ if (dir === 'root') {
108
+ // Root files go to main section
109
+ dirFiles.forEach((file) => {
110
+ if (!file.isIndex) {
111
+ if (!sections[0]) {
112
+ sections.push({ title: 'Documentation', items: [] });
113
+ }
114
+ sections[0].items.push({
115
+ label: file.title || file.name,
116
+ href: `${baseRoute}/${file.name}`,
117
+ });
118
+ }
119
+ });
120
+ } else {
121
+ // Directory files go to their own section
122
+ const sectionTitle = formatFileName(dir);
123
+ const items: NavItem[] = dirFiles.map((file) => ({
124
+ label: file.title || file.name,
125
+ href: `${baseRoute}/${dir}/${file.name}`,
126
+ }));
127
+
128
+ sections.push({ title: sectionTitle, items });
129
+ }
130
+ });
131
+
132
+ return sections;
133
+ }
134
+
135
+ /**
136
+ * Flatten navigation sections to a single array of NavItems
137
+ */
138
+ export function flattenNavigation(sections: NavSection[]): NavItem[] {
139
+ const items: NavItem[] = [];
140
+
141
+ sections.forEach((section) => {
142
+ items.push({
143
+ label: section.title,
144
+ children: section.items,
145
+ });
146
+ });
147
+
148
+ return items;
149
+ }
150
+
151
+ /**
152
+ * Validate navigation configuration
153
+ */
154
+ export function validateNavigation(nav: NavigationConfig): { valid: boolean; errors: string[] } {
155
+ const errors: string[] = [];
156
+
157
+ if (!nav || typeof nav !== 'object') {
158
+ return { valid: false, errors: ['Navigation must be an object'] };
159
+ }
160
+
161
+ if (nav.sections !== undefined) {
162
+ if (!Array.isArray(nav.sections)) {
163
+ errors.push('sections must be an array');
164
+ } else {
165
+ nav.sections.forEach((section, idx) => {
166
+ if (!section.title) {
167
+ errors.push(`Section ${idx}: missing title`);
168
+ }
169
+ if (!Array.isArray(section.items)) {
170
+ errors.push(`Section ${idx}: items must be an array`);
171
+ } else {
172
+ section.items.forEach((item, itemIdx) => {
173
+ if (!item.label) {
174
+ errors.push(`Section ${idx}, item ${itemIdx}: missing label`);
175
+ }
176
+ });
177
+ }
178
+ });
179
+ }
180
+ }
181
+
182
+ if (nav.excludePaths !== undefined && !Array.isArray(nav.excludePaths)) {
183
+ errors.push('excludePaths must be an array');
184
+ }
185
+
186
+ return { valid: errors.length === 0, errors };
187
+ }
188
+
189
+ /**
190
+ * Filter files based on exclude patterns
191
+ */
192
+ export function filterFiles(files: FileInfo[], excludePatterns: string[] = []): FileInfo[] {
193
+ return files.filter((file) => {
194
+ return !excludePatterns.some((pattern) => {
195
+ // Simple pattern matching (could be extended to regex)
196
+ return file.path.includes(pattern);
197
+ });
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Build navigation from a config object
203
+ */
204
+ export function buildNavigationFromConfig(
205
+ config: NavigationConfig,
206
+ files?: FileInfo[],
207
+ baseRoute: string = '/docs'
208
+ ): NavSection[] {
209
+ // If explicit sections are provided, use them
210
+ if (config.sections && config.sections.length > 0) {
211
+ return config.sections;
212
+ }
213
+
214
+ // Otherwise auto-generate from files
215
+ if (files && files.length > 0) {
216
+ const filtered = filterFiles(files, config.excludePaths);
217
+ return generateNavigationFromFiles(filtered, baseRoute);
218
+ }
219
+
220
+ // Return empty navigation
221
+ return [];
222
+ }
223
+
224
+ /**
225
+ * Find a navigation item by href
226
+ */
227
+ export function findNavItem(sections: NavSection[], href: string): NavItem | null {
228
+ for (const section of sections) {
229
+ for (const item of section.items) {
230
+ if (item.href === href) {
231
+ return item;
232
+ }
233
+ if (item.children) {
234
+ const found = item.children.find((child) => child.href === href);
235
+ if (found) return found;
236
+ }
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+
242
+ /**
243
+ * Get previous and next navigation items in order
244
+ */
245
+ export function getPrevNext(
246
+ sections: NavSection[],
247
+ currentHref: string
248
+ ): { prev: NavItem | null; next: NavItem | null } {
249
+ const allItems: NavItem[] = [];
250
+
251
+ sections.forEach((section) => {
252
+ section.items.forEach((item) => {
253
+ if (item.href) allItems.push(item);
254
+ if (item.children) {
255
+ item.children.forEach((child) => {
256
+ if (child.href) allItems.push(child);
257
+ });
258
+ }
259
+ });
260
+ });
261
+
262
+ const currentIndex = allItems.findIndex((item) => item.href === currentHref);
263
+
264
+ return {
265
+ prev: currentIndex > 0 ? allItems[currentIndex - 1] : null,
266
+ next: currentIndex < allItems.length - 1 ? allItems[currentIndex + 1] : null,
267
+ };
268
+ }