Skip to content

Commit 00045fe

Browse files
committed
feat: navigation list sort weight added
1 parent 67c54d6 commit 00045fe

8 files changed

Lines changed: 283 additions & 40 deletions

File tree

src/generators/web/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { title, repository, editURL } from '#theme/config';
5454
| `version` | `string` | Current version label (e.g. `'v22.x'`) |
5555
| `versions` | `Array<{ url, label, major }>` | Pre-computed version entries with labels and URL templates (only `{path}` remains for per-page use) |
5656
| `editURL` | `string` | Partially populated "edit this page" URL template (only `{path}` remains) |
57-
| `pages` | `Array<[string, string]>` | Sorted `[name, path]` tuples for sidebar navigation |
57+
| `pages` | `Array<[number, { heading, path, category? }]>` | Sorted `[weight, page]` tuples for sidebar navigation (explicit weights first, then default ordering) |
5858
| `languageDisplayNameMap` | `Map<string, string>` | Shiki language alias → display name map for code blocks |
5959

6060
#### Usage in custom components
@@ -69,16 +69,19 @@ export default ({ metadata }) => (
6969
<nav>
7070
<p>Current: {version}</p>
7171
<ul>
72-
{pages.map(([name, path]) => (
73-
<li key={path}>
74-
<a href={`${path}.html`}>{name}</a>
72+
{pages.map(([weight, page]) => (
73+
<li key={page.path} data-weight={weight}>
74+
<a href={`${page.path}.html`}>{page.heading}</a>
7575
</li>
7676
))}
7777
</ul>
7878
</nav>
7979
);
8080
```
8181

82+
If a page defines `weight` in frontmatter, lower values are listed first.
83+
Pages without `weight` use `-1` and keep the default upstream ordering.
84+
8285
### Layout props
8386

8487
The `Layout` component receives the following props:

src/generators/web/ui/components/SideBar/utils/__tests__/index.test.mjs

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
'use strict';
2-
31
import assert from 'node:assert/strict';
42
import { describe, it } from 'node:test';
53

@@ -11,10 +9,10 @@ const {
119
} = await import('../index.mjs');
1210

1311
const pages = [
14-
['File System API', 'fs', 'File System'],
15-
['HTTP API', 'http', 'Networking'],
16-
['Path API', 'path', 'File System'],
17-
['Index', 'index'],
12+
[1, { heading: 'File System API', path: 'fs', category: 'File System' }],
13+
[2, { heading: 'HTTP API', path: 'http', category: 'Networking' }],
14+
[3, { heading: 'Path API', path: 'path', category: 'File System' }],
15+
[-1, { heading: 'Index', path: 'index' }],
1816
];
1917

2018
const versions = [
@@ -46,9 +44,9 @@ describe('buildSideBarGroups', () => {
4644

4745
it('puts entries without category into an Others group at the end by default', () => {
4846
const uncategorizedPages = [
49-
['Buffer', 'buffer', 'Binary'],
50-
['Unknown', 'unknown'],
51-
['Config', 'config', ''],
47+
[1, { heading: 'Buffer', path: 'buffer', category: 'Binary' }],
48+
[-1, { heading: 'Unknown', path: 'unknown' }],
49+
[-1, { heading: 'Config', path: 'config', category: '' }],
5250
];
5351
const metadata = { path: 'buffer', basename: 'buffer' };
5452

@@ -64,7 +62,7 @@ describe('buildSideBarGroups', () => {
6462
it('uses a custom default group name when provided', () => {
6563
const metadata = { path: 'unknown', basename: 'unknown' };
6664
const result = buildSideBarGroups(
67-
[['Unknown', 'unknown']],
65+
[[-1, { heading: 'Unknown', path: 'unknown' }]],
6866
metadata,
6967
'General'
7068
);
@@ -113,9 +111,59 @@ describe('getSidebarItems', () => {
113111
const metadata = { path: 'guide/fs', basename: 'fs' };
114112
const result = getSidebarItems(
115113
[
116-
['File System API', 'guide/fs', 'File System'],
117-
['HTTP API', 'guide/http', 'Networking'],
118-
['Child API', 'guide/sub/child'],
114+
[
115+
1,
116+
{
117+
heading: 'File System API',
118+
path: 'guide/fs',
119+
category: 'File System',
120+
},
121+
],
122+
[
123+
2,
124+
{ heading: 'HTTP API', path: 'guide/http', category: 'Networking' },
125+
],
126+
[-1, { heading: 'Child API', path: 'guide/sub/child' }],
127+
],
128+
metadata
129+
);
130+
131+
assert.deepStrictEqual(result, [
132+
{
133+
label: 'File System API',
134+
link: 'fs.html',
135+
category: 'File System',
136+
},
137+
{
138+
label: 'HTTP API',
139+
link: 'http.html',
140+
category: 'Networking',
141+
},
142+
{
143+
label: 'Child API',
144+
link: 'sub/child.html',
145+
category: undefined,
146+
},
147+
]);
148+
});
149+
150+
it('maps the new [weight, page] tuple shape', () => {
151+
const metadata = { path: 'guide/fs', basename: 'fs' };
152+
const result = getSidebarItems(
153+
[
154+
[
155+
10,
156+
{
157+
heading: 'File System API',
158+
path: 'guide/fs',
159+
category: 'File System',
160+
},
161+
],
162+
[
163+
20,
164+
{ heading: 'HTTP API', path: 'guide/http', category: 'Networking' },
165+
],
166+
[-1, { heading: 'Child API', path: 'guide/sub/child' }],
119167
],
120168
metadata
121169
);

src/generators/web/ui/components/SideBar/utils/index.mjs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { relative } from '../../../../../../utils/url.mjs';
44
* Builds grouped sidebar navigation from categorized page entries.
55
* Pages without a category are placed under the provided default group.
66
*
7-
* @param {Array<[string, string, string?]>} pages - Array of page entries as [heading, path, category?]
7+
* @param {Array<[number, { heading: string, path: string, category?: string }> >} pages
88
* @param {{ path: string, basename: string }} metadata - Metadata for the current page, used to resolve links
99
* @param {string} [defaultGroupName='Others'] - Name for the default group containing uncategorized pages
1010
* @returns {Array<{ groupName: string, items: Array<{ label: string, link: string }> }>}
@@ -53,19 +53,21 @@ export const buildSideBarGroups = (
5353

5454
/**
5555
* Converts page entries to sidebar items with resolved links based on current page metadata.
56-
* @param {Array<[string, string, string?]>} pages
56+
* @param {Array<[number, { heading: string, path: string, category?: string }> >} pages
5757
* @param {{ path: string, basename: string }} metadata
5858
* @returns {Array<{ label: string, link: string, category?: string }>}
5959
*/
6060
export const getSidebarItems = (pages, metadata) =>
61-
pages.map(([heading, path, category]) => ({
62-
label: heading,
63-
link:
64-
metadata.path === path
65-
? `${metadata.basename}.html`
66-
: `${relative(path, metadata.path)}.html`,
67-
category,
68-
}));
61+
pages.map(([, { heading, path, category }]) => {
62+
return {
63+
label: heading,
64+
link:
65+
metadata.path === path
66+
? `${metadata.basename}.html`
67+
: `${relative(path, metadata.path)}.html`,
68+
category,
69+
};
70+
});
6971

7072
/**
7173
* Extracts the major version number from a version string.

src/generators/web/ui/types.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ declare module '#theme/config' {
1515
major: number;
1616
}>;
1717
export const editURL: string;
18-
export const pages: Array<[string, string, string?]>;
18+
export const pages: Array<
19+
[number, { heading: string; path: string; category?: string }]
20+
>;
1921
export const languageDisplayNameMap: Map<string, string>;
2022
}
2123

src/generators/web/utils/__tests__/config.test.mjs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe('buildVersionEntries', () => {
102102
});
103103

104104
describe('buildPageList', () => {
105-
it('returns sorted [name, path, category] tuples from input entries', () => {
105+
it('returns sorted [weight, page] tuples from input entries', () => {
106106
const input = [
107107
makeEntry('http', 'HTTP', '/http'),
108108
makeEntry('fs', 'File System', '/fs'),
@@ -112,8 +112,11 @@ describe('buildPageList', () => {
112112

113113
assert.equal(result.length, 2);
114114
// Sorted alphabetically by name
115-
assert.deepStrictEqual(result[0], ['File System', '/fs', 'File System']);
116-
assert.deepStrictEqual(result[1], ['HTTP', '/http', undefined]);
115+
assert.deepStrictEqual(result[0], [
116+
-1,
117+
{ heading: 'File System', path: '/fs', category: 'File System' },
118+
]);
119+
assert.deepStrictEqual(result[1], [-1, { heading: 'HTTP', path: '/http' }]);
117120
});
118121

119122
it('filters out entries whose heading depth is not 1', () => {
@@ -131,7 +134,41 @@ describe('buildPageList', () => {
131134
const result = buildPageList(input);
132135

133136
assert.equal(result.length, 1);
134-
assert.deepStrictEqual(result[0], ['File System', '/fs', 'File System']);
137+
assert.deepStrictEqual(result[0], [
138+
-1,
139+
{ heading: 'File System', path: '/fs', category: 'File System' },
140+
]);
141+
});
142+
143+
it('prioritizes pages with explicit weight before default sorting', () => {
144+
const input = [
145+
{
146+
data: {
147+
api: 'http',
148+
path: '/http',
149+
heading: { depth: 1, data: { name: 'HTTP' } },
150+
weight: 20,
151+
},
152+
},
153+
{
154+
data: {
155+
api: 'fs',
156+
path: '/fs',
157+
category: 'File System',
158+
heading: { depth: 1, data: { name: 'File System' } },
159+
weight: '10',
160+
},
161+
},
162+
makeEntry('buffer', 'Buffer', '/buffer'),
163+
];
164+
165+
const result = buildPageList(input);
166+
167+
assert.deepStrictEqual(result, [
168+
[10, { heading: 'File System', path: '/fs', category: 'File System' }],
169+
[20, { heading: 'HTTP', path: '/http' }],
170+
[-1, { heading: 'Buffer', path: '/buffer' }],
171+
]);
135172
});
136173
});
137174

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import {
5+
DEFAULT_PAGE_WEIGHT,
6+
normalizePageWeight,
7+
compareSidebarPageWeight,
8+
buildSidebarPages,
9+
} from '../pages.mjs';
10+
11+
describe('normalizePageWeight', () => {
12+
it('returns finite numeric weights as-is', () => {
13+
assert.equal(normalizePageWeight(10), 10);
14+
assert.equal(normalizePageWeight(0), 0);
15+
assert.equal(normalizePageWeight(-3), -3);
16+
});
17+
18+
it('parses string numeric weights', () => {
19+
assert.equal(normalizePageWeight('10'), 10);
20+
assert.equal(normalizePageWeight(' 5.5 '), 5.5);
21+
});
22+
23+
it('falls back to default for invalid values', () => {
24+
assert.equal(normalizePageWeight(undefined), DEFAULT_PAGE_WEIGHT);
25+
assert.equal(normalizePageWeight(''), DEFAULT_PAGE_WEIGHT);
26+
assert.equal(normalizePageWeight('abc'), DEFAULT_PAGE_WEIGHT);
27+
assert.equal(normalizePageWeight(Number.NaN), DEFAULT_PAGE_WEIGHT);
28+
});
29+
});
30+
31+
describe('compareSidebarPageWeight', () => {
32+
it('sorts explicit weights before default weight', () => {
33+
assert.equal(compareSidebarPageWeight({ weight: 5 }, { weight: -1 }), -1);
34+
assert.equal(compareSidebarPageWeight({ weight: -1 }, { weight: 5 }), 1);
35+
});
36+
37+
it('sorts by ascending explicit weight', () => {
38+
assert.equal(compareSidebarPageWeight({ weight: 1 }, { weight: 2 }), -1);
39+
assert.equal(compareSidebarPageWeight({ weight: 3 }, { weight: 2 }), 1);
40+
});
41+
42+
it('keeps relative weight when both sides are default', () => {
43+
assert.equal(compareSidebarPageWeight({ weight: -1 }, { weight: -1 }), 0);
44+
});
45+
});
46+
47+
describe('buildSidebarPages', () => {
48+
it('builds [weight, page] tuples and keeps optional category', () => {
49+
const pages = buildSidebarPages([
50+
{
51+
path: '/buffer',
52+
heading: { data: { name: 'Buffer' } },
53+
weight: '20',
54+
},
55+
{
56+
path: '/fs',
57+
category: 'File System',
58+
heading: { data: { name: 'File System' } },
59+
},
60+
{
61+
path: '/http',
62+
heading: { data: { name: 'HTTP' } },
63+
weight: 10,
64+
},
65+
]);
66+
67+
assert.deepStrictEqual(pages, [
68+
[10, { heading: 'HTTP', path: '/http' }],
69+
[20, { heading: 'Buffer', path: '/buffer' }],
70+
[-1, { heading: 'File System', path: '/fs', category: 'File System' }],
71+
]);
72+
});
73+
});

src/generators/web/utils/config.mjs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
'use strict';
2-
31
import { LANGS } from '@node-core/rehype-shiki';
42

3+
import { buildSidebarPages } from './pages.mjs';
54
import getConfig from '../../../utils/configuration/index.mjs';
65
import { populate } from '../../../utils/configuration/templates.mjs';
76
import { getVersionFromSemVer } from '../../../utils/generators.mjs';
@@ -33,16 +32,12 @@ export function buildVersionEntries(config, pageURLBase) {
3332
* Pre-compute sorted page list for sidebar navigation.
3433
*
3534
* @param {Array<import('../../jsx-ast/utils/buildContent.mjs').JSXContent>} input
36-
* @returns {Array<[string, string, string?]>}
35+
* @returns {Array<[number, { heading: string, path: string, category?: string }]>}
3736
*/
3837
export function buildPageList(input) {
3938
const headNodes = getSortedHeadNodes(input.map(({ data }) => data));
4039

41-
return headNodes.map(({ path, category, heading }) => [
42-
heading.data.name,
43-
path,
44-
category,
45-
]);
40+
return buildSidebarPages(headNodes);
4641
}
4742

4843
/**

0 commit comments

Comments
 (0)