@f5-sales-demo/docs-theme 3.9.46 → 3.9.47
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
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { buildSubcategorySidebar, filePathToSlug } from './subcategory-sidebar';
|
|
6
|
+
|
|
7
|
+
const workspaces: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(workspaces.splice(0).map((workspace) => rm(workspace, { force: true, recursive: true })));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function writeDoc(root: string, relativePath: string, title: string, order?: number): Promise<void> {
|
|
14
|
+
const filePath = path.join(root, relativePath);
|
|
15
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
const sidebar = order === undefined ? '' : `sidebar:\n order: ${order}\n`;
|
|
17
|
+
await writeFile(filePath, `---\ntitle: ${title}\n${sidebar}---\n\nContent.\n`);
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
describe('filePathToSlug', () => {
|
|
5
21
|
it('lowercases capitalised path segments to match Starlight entry slugs', () => {
|
|
@@ -23,3 +39,83 @@ describe('filePathToSlug', () => {
|
|
|
23
39
|
expect(filePathToSlug('Enhancements\\foo.mdx')).toBe('/enhancements/foo/');
|
|
24
40
|
});
|
|
25
41
|
});
|
|
42
|
+
|
|
43
|
+
describe('buildSubcategorySidebar without subcategories', () => {
|
|
44
|
+
it('preserves three directory levels, index landings, and sidebar order', async () => {
|
|
45
|
+
const workspace = await mkdtemp(path.join(tmpdir(), 'docs-theme-sidebar-'));
|
|
46
|
+
workspaces.push(workspace);
|
|
47
|
+
const contentDir = path.join(workspace, 'content');
|
|
48
|
+
const docsDir = path.join(contentDir, 'en');
|
|
49
|
+
|
|
50
|
+
await writeDoc(docsDir, 'index.mdx', 'Home', 0);
|
|
51
|
+
await writeDoc(docsDir, 'reference/index.mdx', 'Reference', 1);
|
|
52
|
+
await writeDoc(docsDir, 'demo/index.mdx', 'Demo', 2);
|
|
53
|
+
await writeDoc(docsDir, 'demo/verify.mdx', 'Verify', 20);
|
|
54
|
+
await writeDoc(docsDir, 'demo/deploy.mdx', 'Deploy', 10);
|
|
55
|
+
await writeDoc(docsDir, 'demo/troubleshooting/index.mdx', 'Troubleshooting', 5);
|
|
56
|
+
await writeDoc(docsDir, 'demo/troubleshooting/bgp/index.mdx', 'BGP', 1);
|
|
57
|
+
await writeDoc(docsDir, 'demo/troubleshooting/bgp/routes.mdx', 'Routes', 1);
|
|
58
|
+
|
|
59
|
+
const sidebar = buildSubcategorySidebar(contentDir);
|
|
60
|
+
|
|
61
|
+
expect(sidebar?.[0]).toMatchObject({ label: 'Overview', link: '/' });
|
|
62
|
+
expect(sidebar?.slice(1)).toEqual([
|
|
63
|
+
{
|
|
64
|
+
label: 'Reference',
|
|
65
|
+
collapsed: true,
|
|
66
|
+
translations: expect.any(Object),
|
|
67
|
+
items: [{ label: 'Overview', slug: 'reference', translations: expect.any(Object) }],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'Demo',
|
|
71
|
+
collapsed: true,
|
|
72
|
+
items: [
|
|
73
|
+
{ label: 'Overview', slug: 'demo', translations: expect.any(Object) },
|
|
74
|
+
{
|
|
75
|
+
label: 'Troubleshooting',
|
|
76
|
+
collapsed: true,
|
|
77
|
+
translations: expect.any(Object),
|
|
78
|
+
items: [
|
|
79
|
+
{ label: 'Overview', slug: 'demo/troubleshooting', translations: expect.any(Object) },
|
|
80
|
+
{
|
|
81
|
+
label: 'BGP',
|
|
82
|
+
collapsed: true,
|
|
83
|
+
items: [
|
|
84
|
+
{ label: 'Overview', slug: 'demo/troubleshooting/bgp', translations: expect.any(Object) },
|
|
85
|
+
{ slug: 'demo/troubleshooting/bgp/routes' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{ slug: 'demo/deploy' },
|
|
91
|
+
{ slug: 'demo/verify' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('falls back to title ordering when sidebar order is absent', async () => {
|
|
98
|
+
const workspace = await mkdtemp(path.join(tmpdir(), 'docs-theme-sidebar-'));
|
|
99
|
+
workspaces.push(workspace);
|
|
100
|
+
const contentDir = path.join(workspace, 'content');
|
|
101
|
+
|
|
102
|
+
await writeDoc(contentDir, 'guides/index.mdx', 'Guides');
|
|
103
|
+
await writeDoc(contentDir, 'guides/z-last.mdx', 'Zebra');
|
|
104
|
+
await writeDoc(contentDir, 'guides/a-first.mdx', 'Alpha');
|
|
105
|
+
|
|
106
|
+
const sidebar = buildSubcategorySidebar(contentDir);
|
|
107
|
+
|
|
108
|
+
expect(sidebar).toEqual([
|
|
109
|
+
{
|
|
110
|
+
label: 'Guides',
|
|
111
|
+
collapsed: true,
|
|
112
|
+
translations: expect.any(Object),
|
|
113
|
+
items: [
|
|
114
|
+
{ label: 'Overview', slug: 'guides', translations: expect.any(Object) },
|
|
115
|
+
{ slug: 'guides/a-first' },
|
|
116
|
+
{ slug: 'guides/z-last' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -6,10 +6,17 @@ import { sidebarTranslations } from '../i18n/translations.ts';
|
|
|
6
6
|
/**
|
|
7
7
|
* Starlight sidebar config types (simplified).
|
|
8
8
|
*/
|
|
9
|
-
type SidebarLink = { label
|
|
9
|
+
type SidebarLink = { label?: string; link?: string; slug?: string; translations?: Record<string, string> };
|
|
10
10
|
type SidebarGroup = { label: string; items: SidebarItem[]; collapsed?: boolean; translations?: Record<string, string> };
|
|
11
11
|
type SidebarItem = SidebarLink | SidebarGroup;
|
|
12
12
|
|
|
13
|
+
interface OrderedSidebarItem {
|
|
14
|
+
item: SidebarItem;
|
|
15
|
+
order: number | undefined;
|
|
16
|
+
sortLabel: string;
|
|
17
|
+
sortPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
type DocType = 'resource' | 'data-source' | 'guide' | 'function';
|
|
14
21
|
|
|
15
22
|
interface DocEntry {
|
|
@@ -77,6 +84,105 @@ function kebabToTitleCase(kebab: string): string {
|
|
|
77
84
|
.join(' ');
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
function readNavigationMetadata(filePath: string, fallbackTitle: string): { title: string; order: number | undefined } {
|
|
88
|
+
try {
|
|
89
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
90
|
+
const frontmatter = matter(raw).data as Record<string, unknown>;
|
|
91
|
+
const title =
|
|
92
|
+
typeof frontmatter.title === 'string' && frontmatter.title.trim()
|
|
93
|
+
? frontmatter.title.trim()
|
|
94
|
+
: typeof frontmatter.page_title === 'string' && frontmatter.page_title.trim()
|
|
95
|
+
? frontmatter.page_title.trim()
|
|
96
|
+
: fallbackTitle;
|
|
97
|
+
const sidebar = frontmatter.sidebar;
|
|
98
|
+
const order =
|
|
99
|
+
typeof sidebar === 'object' &&
|
|
100
|
+
sidebar !== null &&
|
|
101
|
+
'order' in sidebar &&
|
|
102
|
+
typeof sidebar.order === 'number' &&
|
|
103
|
+
Number.isFinite(sidebar.order)
|
|
104
|
+
? sidebar.order
|
|
105
|
+
: undefined;
|
|
106
|
+
return { title, order };
|
|
107
|
+
} catch {
|
|
108
|
+
return { title: fallbackTitle, order: undefined };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function compareOrderedSidebarItems(a: OrderedSidebarItem, b: OrderedSidebarItem): number {
|
|
113
|
+
const orderDifference = (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY);
|
|
114
|
+
if (orderDifference !== 0) return orderDifference;
|
|
115
|
+
const labelDifference = a.sortLabel.localeCompare(b.sortLabel);
|
|
116
|
+
return labelDifference !== 0 ? labelDifference : a.sortPath.localeCompare(b.sortPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildDirectorySidebar(dirPath: string, scanDir: string): OrderedSidebarItem | undefined {
|
|
120
|
+
let entries: fs.Dirent[];
|
|
121
|
+
try {
|
|
122
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
123
|
+
} catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const relativeDir = path.relative(scanDir, dirPath).replace(/\\/g, '/');
|
|
128
|
+
const directoryName = path.basename(dirPath);
|
|
129
|
+
const fallbackLabel = kebabToTitleCase(directoryName);
|
|
130
|
+
const indexEntry = entries.find((entry) => entry.isFile() && /^index\.mdx?$/.test(entry.name));
|
|
131
|
+
const indexPath = indexEntry ? path.join(dirPath, indexEntry.name) : undefined;
|
|
132
|
+
const metadata = indexPath
|
|
133
|
+
? readNavigationMetadata(indexPath, fallbackLabel)
|
|
134
|
+
: { title: fallbackLabel, order: undefined };
|
|
135
|
+
const children: OrderedSidebarItem[] = [];
|
|
136
|
+
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
|
139
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
140
|
+
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
const group = buildDirectorySidebar(fullPath, scanDir);
|
|
143
|
+
if (group) children.push(group);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!entry.isFile() || !/\.mdx?$/.test(entry.name) || /^index\.mdx?$/.test(entry.name)) continue;
|
|
147
|
+
|
|
148
|
+
const relativePath = path.relative(scanDir, fullPath).replace(/\\/g, '/');
|
|
149
|
+
const slug = filePathToSlug(relativePath).replace(/^\/|\/$/g, '');
|
|
150
|
+
const fallbackTitle = path.basename(entry.name, path.extname(entry.name)).replace(/[-_]/g, ' ');
|
|
151
|
+
const page = readNavigationMetadata(fullPath, fallbackTitle);
|
|
152
|
+
children.push({
|
|
153
|
+
item: { slug },
|
|
154
|
+
order: page.order,
|
|
155
|
+
sortLabel: page.title,
|
|
156
|
+
sortPath: relativePath,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
children.sort(compareOrderedSidebarItems);
|
|
161
|
+
const items: SidebarItem[] = [];
|
|
162
|
+
if (indexPath) {
|
|
163
|
+
items.push({
|
|
164
|
+
label: 'Overview',
|
|
165
|
+
slug: filePathToSlug(path.relative(scanDir, indexPath)).replace(/^\/|\/$/g, ''),
|
|
166
|
+
translations: sidebarTranslations.Overview,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
items.push(...children.map((child) => child.item));
|
|
170
|
+
if (items.length === 0) return undefined;
|
|
171
|
+
|
|
172
|
+
const translations = sidebarTranslations[metadata.title as keyof typeof sidebarTranslations];
|
|
173
|
+
return {
|
|
174
|
+
item: {
|
|
175
|
+
label: metadata.title,
|
|
176
|
+
collapsed: true,
|
|
177
|
+
...(translations ? { translations } : {}),
|
|
178
|
+
items,
|
|
179
|
+
},
|
|
180
|
+
order: metadata.order,
|
|
181
|
+
sortLabel: metadata.title,
|
|
182
|
+
sortPath: relativeDir,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
80
186
|
/**
|
|
81
187
|
* Convert a file path relative to the content dir into a Starlight link slug.
|
|
82
188
|
* e.g. "resources/api_crawler.md" → "/resources/api_crawler/"
|
|
@@ -170,51 +276,46 @@ export function buildSubcategorySidebar(contentDir: string): SidebarItem[] | und
|
|
|
170
276
|
|
|
171
277
|
// If no files have subcategory, generate autogenerate entries with translations
|
|
172
278
|
if (!hasAnySubcategory) {
|
|
173
|
-
|
|
279
|
+
let rootEntries: fs.Dirent[];
|
|
174
280
|
try {
|
|
175
|
-
|
|
176
|
-
for (const entry of dirEntries) {
|
|
177
|
-
if (entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.startsWith('_')) {
|
|
178
|
-
topLevelDirs.push(entry.name);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
281
|
+
rootEntries = fs.readdirSync(scanDir, { withFileTypes: true });
|
|
181
282
|
} catch {
|
|
182
283
|
return undefined;
|
|
183
284
|
}
|
|
184
|
-
|
|
285
|
+
const orderedItems: OrderedSidebarItem[] = [];
|
|
185
286
|
|
|
186
|
-
const
|
|
287
|
+
for (const entry of rootEntries) {
|
|
288
|
+
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
|
289
|
+
const fullPath = path.join(scanDir, entry.name);
|
|
187
290
|
|
|
188
|
-
|
|
189
|
-
|
|
291
|
+
if (entry.isDirectory()) {
|
|
292
|
+
const group = buildDirectorySidebar(fullPath, scanDir);
|
|
293
|
+
if (group) orderedItems.push(group);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (!entry.isFile() || !/\.mdx?$/.test(entry.name) || /^index\.mdx?$/.test(entry.name)) continue;
|
|
297
|
+
|
|
298
|
+
const slug = filePathToSlug(entry.name).replace(/^\/|\/$/g, '');
|
|
299
|
+
const fallbackTitle = path.basename(entry.name, path.extname(entry.name)).replace(/[-_]/g, ' ');
|
|
300
|
+
const page = readNavigationMetadata(fullPath, fallbackTitle);
|
|
301
|
+
orderedItems.push({ item: { slug }, order: page.order, sortLabel: page.title, sortPath: entry.name });
|
|
190
302
|
}
|
|
191
303
|
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return { slug: slug.replace(/^\/|\/$/g, '') } as SidebarLink;
|
|
204
|
-
})
|
|
205
|
-
.filter((item): item is SidebarLink => item !== null);
|
|
206
|
-
|
|
207
|
-
if (links.length > 0) {
|
|
208
|
-
sidebar.push({
|
|
209
|
-
label,
|
|
210
|
-
collapsed: false,
|
|
211
|
-
...(translations ? { translations } : {}),
|
|
212
|
-
items: links,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
304
|
+
if (hasOverview) {
|
|
305
|
+
const overviewEntry = rootEntries.find((entry) => entry.isFile() && /^index\.mdx?$/.test(entry.name));
|
|
306
|
+
const overview = overviewEntry
|
|
307
|
+
? readNavigationMetadata(path.join(scanDir, overviewEntry.name), 'Overview')
|
|
308
|
+
: { title: 'Overview', order: undefined };
|
|
309
|
+
orderedItems.push({
|
|
310
|
+
item: { label: 'Overview', link: '/', translations: sidebarTranslations.Overview },
|
|
311
|
+
order: overview.order ?? Number.NEGATIVE_INFINITY,
|
|
312
|
+
sortLabel: overview.title,
|
|
313
|
+
sortPath: '',
|
|
314
|
+
});
|
|
215
315
|
}
|
|
216
316
|
|
|
217
|
-
|
|
317
|
+
orderedItems.sort(compareOrderedSidebarItems);
|
|
318
|
+
return orderedItems.length > 0 ? orderedItems.map((entry) => entry.item) : undefined;
|
|
218
319
|
}
|
|
219
320
|
|
|
220
321
|
// Partition docs by type
|