@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.
- package/COMPONENTS.md +365 -0
- package/COVERAGE_REPORT.md +663 -0
- package/README.md +42 -0
- package/SEARCH_VERIFICATION.md +229 -0
- package/TEST_SUMMARY.md +344 -0
- package/bin/init.js +821 -0
- package/docs/E2E_TESTS.md +354 -0
- package/docs/TESTING.md +754 -0
- package/docs/de/index.md +41 -0
- package/docs/en/COMPONENTS.md +443 -0
- package/docs/en/api/examples.md +100 -0
- package/docs/en/api/overview.md +69 -0
- package/docs/en/components/index.md +622 -0
- package/docs/en/config/navigation.md +505 -0
- package/docs/en/config/theme-and-colors.md +395 -0
- package/docs/en/getting-started/integration.md +406 -0
- package/docs/en/guides/common-setups.md +651 -0
- package/docs/en/index.md +243 -0
- package/docs/en/markdown.md +102 -0
- package/docs/en/routing.md +64 -0
- package/docs/en/setup.md +52 -0
- package/docs/en/troubleshooting.md +704 -0
- package/docs/es/index.md +41 -0
- package/docs/fr/index.md +41 -0
- package/docs/ja/index.md +41 -0
- package/package.json +40 -0
- package/pagefind.toml +8 -0
- package/postcss.config.js +5 -0
- package/src/app.css +119 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/components/APITable.svelte +120 -0
- package/src/lib/components/APITable.test.ts +153 -0
- package/src/lib/components/Breadcrumbs.svelte +85 -0
- package/src/lib/components/Breadcrumbs.test.ts +148 -0
- package/src/lib/components/Callout.svelte +60 -0
- package/src/lib/components/Callout.test.ts +100 -0
- package/src/lib/components/CodeBlock.svelte +68 -0
- package/src/lib/components/CodeBlock.test.ts +133 -0
- package/src/lib/components/DocLayout.svelte +84 -0
- package/src/lib/components/Footer.svelte +78 -0
- package/src/lib/components/Image.svelte +100 -0
- package/src/lib/components/Image.test.ts +163 -0
- package/src/lib/components/Navbar.svelte +141 -0
- package/src/lib/components/Search.svelte +248 -0
- package/src/lib/components/Sidebar.svelte +110 -0
- package/src/lib/components/Tabs.svelte +48 -0
- package/src/lib/components/Tabs.test.ts +102 -0
- package/src/lib/config.test.ts +140 -0
- package/src/lib/config.ts +179 -0
- package/src/lib/configIntegration.test.ts +272 -0
- package/src/lib/configLoader.ts +231 -0
- package/src/lib/configParser.test.ts +217 -0
- package/src/lib/configParser.ts +234 -0
- package/src/lib/index.ts +34 -0
- package/src/lib/integration.test.ts +426 -0
- package/src/lib/navigationBuilder.test.ts +338 -0
- package/src/lib/navigationBuilder.ts +268 -0
- package/src/lib/performance.test.ts +369 -0
- package/src/lib/routing.test.ts +202 -0
- package/src/lib/routing.ts +127 -0
- package/src/lib/search-functionality.test.ts +493 -0
- package/src/lib/stores/i18n.test.ts +180 -0
- package/src/lib/stores/i18n.ts +143 -0
- package/src/lib/stores/nav.ts +36 -0
- package/src/lib/stores/search.test.ts +140 -0
- package/src/lib/stores/search.ts +162 -0
- package/src/lib/stores/theme.ts +59 -0
- package/src/lib/stores/version.test.ts +139 -0
- package/src/lib/stores/version.ts +111 -0
- package/src/lib/themeCustomization.test.ts +223 -0
- package/src/lib/themeCustomization.ts +212 -0
- package/src/lib/utils/highlight.test.ts +136 -0
- package/src/lib/utils/highlight.ts +100 -0
- package/src/lib/utils/index.ts +7 -0
- package/src/lib/utils/markdown.test.ts +357 -0
- package/src/lib/utils/markdown.ts +77 -0
- package/src/routes/+layout.server.ts +1 -0
- package/src/routes/+layout.svelte +28 -0
- package/src/routes/+page.svelte +165 -0
- package/static/robots.txt +3 -0
- package/svelte.config.js +18 -0
- package/tailwind.config.ts +55 -0
- package/template-starter/.github/workflows/build.yml +40 -0
- package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
- package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
- package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
- package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
- package/template-starter/README.md +320 -0
- package/template-starter/docs/_config.json +39 -0
- package/template-starter/docs/api/components.md +257 -0
- package/template-starter/docs/api/overview.md +169 -0
- package/template-starter/docs/guides/configuration.md +145 -0
- package/template-starter/docs/guides/github-pages-deployment.md +254 -0
- package/template-starter/docs/guides/netlify-deployment.md +159 -0
- package/template-starter/docs/guides/vercel-deployment.md +131 -0
- package/template-starter/docs/index.md +49 -0
- package/template-starter/docs/setup.md +149 -0
- package/template-starter/package.json +31 -0
- package/template-starter/pagefind.toml +3 -0
- package/template-starter/postcss.config.js +5 -0
- package/template-starter/src/app.css +34 -0
- package/template-starter/src/app.d.ts +13 -0
- package/template-starter/src/app.html +11 -0
- package/template-starter/src/lib/components/APITable.svelte +120 -0
- package/template-starter/src/lib/components/APITable.test.ts +19 -0
- package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
- package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
- package/template-starter/src/lib/components/Callout.svelte +60 -0
- package/template-starter/src/lib/components/Callout.test.ts +16 -0
- package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
- package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
- package/template-starter/src/lib/components/DocLayout.svelte +84 -0
- package/template-starter/src/lib/components/Footer.svelte +78 -0
- package/template-starter/src/lib/components/Image.svelte +100 -0
- package/template-starter/src/lib/components/Image.test.ts +15 -0
- package/template-starter/src/lib/components/Navbar.svelte +141 -0
- package/template-starter/src/lib/components/Search.svelte +248 -0
- package/template-starter/src/lib/components/Sidebar.svelte +110 -0
- package/template-starter/src/lib/components/Tabs.svelte +48 -0
- package/template-starter/src/lib/components/Tabs.test.ts +17 -0
- package/template-starter/src/lib/index.ts +15 -0
- package/template-starter/src/routes/+layout.svelte +28 -0
- package/template-starter/src/routes/+page.svelte +92 -0
- package/template-starter/svelte.config.js +17 -0
- package/template-starter/tailwind.config.ts +17 -0
- package/template-starter/tsconfig.json +13 -0
- package/template-starter/vite.config.ts +6 -0
- package/tests/e2e/example.spec.ts +345 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +34 -0
- 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
|
+
}
|