@mintlify/common 1.0.1069 → 1.0.1071
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/dist/navigation/accessiblePagesTraversal.test.d.ts +1 -0
- package/dist/navigation/accessiblePagesTraversal.test.js +126 -0
- package/dist/navigation/activeDivisionMatching.test.d.ts +1 -0
- package/dist/navigation/activeDivisionMatching.test.js +245 -0
- package/dist/navigation/filterDivisions.js +25 -13
- package/dist/navigation/scopeNavDictCache.test.d.ts +1 -0
- package/dist/navigation/scopeNavDictCache.test.js +68 -0
- package/dist/navigation/scopeNavToPath.js +26 -6
- package/dist/navigation/versionLanguageResolution.test.d.ts +1 -0
- package/dist/navigation/versionLanguageResolution.test.js +122 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { filterDivisions } from './filterDivisions.js';
|
|
2
|
+
const baseArgs = {
|
|
3
|
+
userGroups: new Set(),
|
|
4
|
+
currentVersion: undefined,
|
|
5
|
+
currentLanguage: undefined,
|
|
6
|
+
shouldUseDivisionMatch: true,
|
|
7
|
+
cache: { dropdownCache: new Map() },
|
|
8
|
+
};
|
|
9
|
+
const run = (decoratedNav, currentPath, overrides = {}) => filterDivisions(Object.assign(Object.assign(Object.assign({}, baseArgs), { decoratedNav, currentPath }), overrides));
|
|
10
|
+
describe('accessible page traversal', () => {
|
|
11
|
+
it('returns the accessible siblings for a match late in a long page list', () => {
|
|
12
|
+
const nav = {
|
|
13
|
+
pages: [
|
|
14
|
+
{ title: 'Public one', href: '/public-one' },
|
|
15
|
+
{ title: 'Gated', href: '/gated', groups: ['admin'] },
|
|
16
|
+
{ title: 'Public two', href: '/public-two' },
|
|
17
|
+
{ title: 'Target', href: '/target' },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
const result = run(nav, '/target');
|
|
21
|
+
expect(result.groupsOrPages).toEqual([
|
|
22
|
+
{ title: 'Public one', href: '/public-one' },
|
|
23
|
+
{ title: 'Public two', href: '/public-two' },
|
|
24
|
+
{ title: 'Target', href: '/target' },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
it('returns the same accessible siblings whichever page in the list matches', () => {
|
|
28
|
+
const nav = {
|
|
29
|
+
pages: [
|
|
30
|
+
{ title: 'First', href: '/first' },
|
|
31
|
+
{ title: 'Gated', href: '/gated', groups: [] },
|
|
32
|
+
{ title: 'Last', href: '/last' },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
const first = run(nav, '/first');
|
|
36
|
+
const last = run(nav, '/last');
|
|
37
|
+
expect(first.groupsOrPages).toEqual(last.groupsOrPages);
|
|
38
|
+
expect(first.groupsOrPages).toEqual([
|
|
39
|
+
{ title: 'First', href: '/first' },
|
|
40
|
+
{ title: 'Last', href: '/last' },
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
it('includes pages the user has access to through their groups', () => {
|
|
44
|
+
const nav = {
|
|
45
|
+
pages: [
|
|
46
|
+
{ title: 'Admin only', href: '/admin-only', groups: ['admin'] },
|
|
47
|
+
{ title: 'Sales only', href: '/sales-only', groups: ['sales'] },
|
|
48
|
+
{ title: 'Target', href: '/target' },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
const result = run(nav, '/target', { userGroups: new Set(['admin']) });
|
|
52
|
+
expect(result.groupsOrPages).toEqual([
|
|
53
|
+
{ title: 'Admin only', href: '/admin-only', groups: ['admin'] },
|
|
54
|
+
{ title: 'Target', href: '/target' },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
it('keeps gated siblings in preview', () => {
|
|
58
|
+
const nav = {
|
|
59
|
+
pages: [
|
|
60
|
+
{ title: 'Gated', href: '/gated', groups: ['admin'] },
|
|
61
|
+
{ title: 'Target', href: '/target' },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
const result = run(nav, '/target', { isPreview: true });
|
|
65
|
+
expect(result.groupsOrPages).toEqual([
|
|
66
|
+
{ title: 'Gated', href: '/gated', groups: ['admin'] },
|
|
67
|
+
{ title: 'Target', href: '/target' },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
it('prefers parent groups over the sibling list when nested', () => {
|
|
71
|
+
const nav = {
|
|
72
|
+
groups: [
|
|
73
|
+
{
|
|
74
|
+
group: 'Outer',
|
|
75
|
+
pages: [
|
|
76
|
+
{ title: 'Inner one', href: '/inner-one' },
|
|
77
|
+
{ title: 'Inner two', href: '/inner-two' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
const result = run(nav, '/inner-two');
|
|
83
|
+
expect(result.groupsOrPages).toEqual([
|
|
84
|
+
{
|
|
85
|
+
group: 'Outer',
|
|
86
|
+
pages: [
|
|
87
|
+
{ title: 'Inner one', href: '/inner-one' },
|
|
88
|
+
{ title: 'Inner two', href: '/inner-two' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
it('resolves a root page against its filtered siblings', () => {
|
|
94
|
+
const nav = {
|
|
95
|
+
groups: [
|
|
96
|
+
{
|
|
97
|
+
group: 'Outer',
|
|
98
|
+
root: { title: 'Root', href: '/root' },
|
|
99
|
+
pages: [
|
|
100
|
+
{ title: 'Gated', href: '/gated', groups: [] },
|
|
101
|
+
{ title: 'Visible', href: '/visible' },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
const result = run(nav, '/root');
|
|
107
|
+
expect(result.groupsOrPages).toEqual([
|
|
108
|
+
{
|
|
109
|
+
group: 'Outer',
|
|
110
|
+
root: { title: 'Root', href: '/root' },
|
|
111
|
+
pages: [{ title: 'Visible', href: '/visible' }],
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
it('does not mutate the navigation it is given', () => {
|
|
116
|
+
const nav = {
|
|
117
|
+
pages: [
|
|
118
|
+
{ title: 'Gated', href: '/gated', groups: [] },
|
|
119
|
+
{ title: 'Target', href: '/target' },
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
const before = JSON.stringify(nav);
|
|
123
|
+
run(nav, '/target');
|
|
124
|
+
expect(JSON.stringify(nav)).toBe(before);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { filterDivisions, isActiveDivision } from './filterDivisions.js';
|
|
2
|
+
const baseArgs = {
|
|
3
|
+
userGroups: new Set(),
|
|
4
|
+
currentVersion: undefined,
|
|
5
|
+
currentLanguage: undefined,
|
|
6
|
+
shouldUseDivisionMatch: false,
|
|
7
|
+
cache: { dropdownCache: new Map() },
|
|
8
|
+
};
|
|
9
|
+
const run = (decoratedNav, currentPath, overrides = {}) => filterDivisions(Object.assign(Object.assign(Object.assign({}, baseArgs), { decoratedNav, currentPath }), overrides));
|
|
10
|
+
const pages = (prefix) => [
|
|
11
|
+
{ title: 'First', href: `${prefix}/first` },
|
|
12
|
+
{ title: 'Second', href: `${prefix}/second` },
|
|
13
|
+
];
|
|
14
|
+
describe('active division matching', () => {
|
|
15
|
+
it('marks only the tab containing the path active when sibling names are unique', () => {
|
|
16
|
+
const nav = {
|
|
17
|
+
tabs: [
|
|
18
|
+
{ tab: 'Guides', pages: pages('/guides') },
|
|
19
|
+
{ tab: 'Reference', pages: pages('/reference') },
|
|
20
|
+
{ tab: 'Support', pages: pages('/support') },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
const result = run(nav, '/reference/second');
|
|
24
|
+
expect(result.tabs.map((tab) => tab.tab)).toEqual(['Guides', 'Reference', 'Support']);
|
|
25
|
+
expect(result.tabs.filter(isActiveDivision).map((tab) => tab.tab)).toEqual(['Reference']);
|
|
26
|
+
});
|
|
27
|
+
it('marks a single version active and preserves sibling order', () => {
|
|
28
|
+
const nav = {
|
|
29
|
+
versions: [
|
|
30
|
+
{ version: 'v3', default: true, pages: pages('/v3') },
|
|
31
|
+
{ version: 'v2', pages: pages('/v2') },
|
|
32
|
+
{ version: 'v1', pages: pages('/v1') },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
const result = run(nav, '/v2/first');
|
|
36
|
+
expect(result.versions.map((version) => version.version)).toEqual(['v3', 'v2', 'v1']);
|
|
37
|
+
expect(result.versions.filter(isActiveDivision).map((version) => version.version)).toEqual([
|
|
38
|
+
'v2',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
it('keeps every byte-identical sibling division active', () => {
|
|
42
|
+
var _a;
|
|
43
|
+
const duplicated = { tab: 'Duplicated', pages: pages('/duplicated') };
|
|
44
|
+
const nav = {
|
|
45
|
+
tabs: [
|
|
46
|
+
structuredClone(duplicated),
|
|
47
|
+
structuredClone(duplicated),
|
|
48
|
+
{ tab: 'Other', pages: pages('/other') },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
const result = run(nav, '/duplicated/first');
|
|
52
|
+
expect(result.tabs.filter(isActiveDivision).map((tab) => tab.tab)).toEqual([
|
|
53
|
+
'Duplicated',
|
|
54
|
+
'Duplicated',
|
|
55
|
+
]);
|
|
56
|
+
expect((_a = result.tabs.find((tab) => tab.tab === 'Other')) === null || _a === void 0 ? void 0 : _a.isActive).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('keeps every byte-identical sibling version active', () => {
|
|
59
|
+
const duplicated = { version: 'v1', pages: pages('/v1') };
|
|
60
|
+
const nav = {
|
|
61
|
+
versions: [structuredClone(duplicated), structuredClone(duplicated)],
|
|
62
|
+
};
|
|
63
|
+
const result = run(nav, '/v1/first');
|
|
64
|
+
expect(result.versions.filter(isActiveDivision)).toHaveLength(2);
|
|
65
|
+
});
|
|
66
|
+
it('marks one division active when siblings share a name but differ in content', () => {
|
|
67
|
+
var _a;
|
|
68
|
+
const nav = {
|
|
69
|
+
tabs: [
|
|
70
|
+
{ tab: 'Shared', pages: pages('/left') },
|
|
71
|
+
{ tab: 'Shared', pages: pages('/right') },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
const result = run(nav, '/right/first');
|
|
75
|
+
const active = result.tabs.filter(isActiveDivision);
|
|
76
|
+
expect(active).toHaveLength(1);
|
|
77
|
+
expect((_a = active[0]) === null || _a === void 0 ? void 0 : _a.href).toBe('/right/first');
|
|
78
|
+
});
|
|
79
|
+
it('marks one division active when siblings share content but differ in name', () => {
|
|
80
|
+
const shared = pages('/shared');
|
|
81
|
+
const nav = {
|
|
82
|
+
tabs: [
|
|
83
|
+
{ tab: 'One', pages: structuredClone(shared) },
|
|
84
|
+
{ tab: 'Two', pages: structuredClone(shared) },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const result = run(nav, '/shared/first');
|
|
88
|
+
expect(result.tabs.filter(isActiveDivision).map((tab) => tab.tab)).toEqual(['One']);
|
|
89
|
+
});
|
|
90
|
+
it('drops hidden siblings while keeping the active division', () => {
|
|
91
|
+
const nav = {
|
|
92
|
+
tabs: [
|
|
93
|
+
{ tab: 'Visible', pages: pages('/visible') },
|
|
94
|
+
{ tab: 'Hidden', hidden: true, pages: pages('/hidden') },
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
const result = run(nav, '/visible/first');
|
|
98
|
+
expect(result.tabs.map((tab) => tab.tab)).toEqual(['Visible']);
|
|
99
|
+
});
|
|
100
|
+
it('keeps a hidden division when it is the active one', () => {
|
|
101
|
+
const nav = {
|
|
102
|
+
tabs: [
|
|
103
|
+
{ tab: 'Visible', pages: pages('/visible') },
|
|
104
|
+
{ tab: 'Hidden', hidden: true, pages: pages('/hidden') },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
const result = run(nav, '/hidden/first');
|
|
108
|
+
expect(result.tabs.filter(isActiveDivision).map((tab) => tab.tab)).toEqual(['Hidden']);
|
|
109
|
+
});
|
|
110
|
+
it('resolves the active product when the iteration order is reordered for currentProduct', () => {
|
|
111
|
+
const nav = {
|
|
112
|
+
products: [
|
|
113
|
+
{ product: 'alpha', pages: pages('/alpha') },
|
|
114
|
+
{ product: 'beta', pages: pages('/beta') },
|
|
115
|
+
{ product: 'gamma', pages: pages('/gamma') },
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
const result = run(nav, '/gamma/first', { currentProduct: 'gamma' });
|
|
119
|
+
expect(result.products.map((product) => product.product)).toEqual(['alpha', 'beta', 'gamma']);
|
|
120
|
+
expect(result.products.filter(isActiveDivision).map((product) => product.product)).toEqual([
|
|
121
|
+
'gamma',
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
it('resolves the active version when shouldUseDivisionMatch narrows to currentVersion', () => {
|
|
125
|
+
const nav = {
|
|
126
|
+
versions: [
|
|
127
|
+
{ version: 'v2', default: true, pages: [{ title: 'Shared', href: '/shared' }] },
|
|
128
|
+
{ version: 'v1', pages: [{ title: 'Shared', href: '/shared' }] },
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
const result = run(nav, '/shared', {
|
|
132
|
+
currentVersion: 'v1',
|
|
133
|
+
shouldUseDivisionMatch: true,
|
|
134
|
+
});
|
|
135
|
+
expect(result.versions.filter(isActiveDivision).map((version) => version.version)).toEqual([
|
|
136
|
+
'v1',
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
it('falls back to the default chrome for a path that is not in the navigation', () => {
|
|
140
|
+
const nav = {
|
|
141
|
+
tabs: [
|
|
142
|
+
{ tab: 'Guides', pages: pages('/guides') },
|
|
143
|
+
{ tab: 'Reference', pages: pages('/reference') },
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
const result = run(nav, '/nothing/here');
|
|
147
|
+
expect(result.tabs.map((tab) => tab.tab)).toEqual(['Guides', 'Reference']);
|
|
148
|
+
expect(result.tabs.filter(isActiveDivision).map((tab) => tab.tab)).toEqual(['Guides']);
|
|
149
|
+
});
|
|
150
|
+
it('resolves the nearest divisions for an out-of-nav path sharing a prefix', () => {
|
|
151
|
+
const nav = {
|
|
152
|
+
versions: [
|
|
153
|
+
{ version: 'v2', default: true, pages: pages('/v2/guides') },
|
|
154
|
+
{ version: 'v1', pages: pages('/v1/guides') },
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
const result = run(nav, '/v1/guides/missing');
|
|
158
|
+
expect(result.versions.map((version) => version.version)).toEqual(['v2', 'v1']);
|
|
159
|
+
});
|
|
160
|
+
it('does not mutate the navigation it is given', () => {
|
|
161
|
+
const nav = {
|
|
162
|
+
tabs: [
|
|
163
|
+
{ tab: 'Guides', pages: pages('/guides') },
|
|
164
|
+
{ tab: 'Reference', pages: pages('/reference') },
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
const before = JSON.stringify(nav);
|
|
168
|
+
run(nav, '/reference/first');
|
|
169
|
+
expect(JSON.stringify(nav)).toBe(before);
|
|
170
|
+
});
|
|
171
|
+
it('marks the active anchor', () => {
|
|
172
|
+
const nav = {
|
|
173
|
+
anchors: [
|
|
174
|
+
{ anchor: 'Home', pages: pages('/home') },
|
|
175
|
+
{ anchor: 'API', pages: pages('/api') },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
const result = run(nav, '/api/second');
|
|
179
|
+
expect(result.anchors.filter(isActiveDivision).map((anchor) => anchor.anchor)).toEqual(['API']);
|
|
180
|
+
});
|
|
181
|
+
it('marks the active dropdown', () => {
|
|
182
|
+
const nav = {
|
|
183
|
+
dropdowns: [
|
|
184
|
+
{ dropdown: 'Cloud', pages: pages('/cloud') },
|
|
185
|
+
{ dropdown: 'Self host', pages: pages('/self-host') },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
const result = run(nav, '/self-host/first');
|
|
189
|
+
expect(result.dropdowns.filter(isActiveDivision).map((dropdown) => dropdown.dropdown)).toEqual([
|
|
190
|
+
'Self host',
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
it('marks the active menu item within a tab', () => {
|
|
194
|
+
var _a, _b;
|
|
195
|
+
const nav = {
|
|
196
|
+
tabs: [
|
|
197
|
+
{
|
|
198
|
+
tab: 'Products',
|
|
199
|
+
menu: [
|
|
200
|
+
{ item: 'Alpha', pages: pages('/alpha') },
|
|
201
|
+
{ item: 'Beta', pages: pages('/beta') },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
const result = run(nav, '/beta/first');
|
|
207
|
+
expect((_b = (_a = result.tabs[0]) === null || _a === void 0 ? void 0 : _a.menu) === null || _b === void 0 ? void 0 : _b.filter(isActiveDivision).map((entry) => entry.item)).toEqual([
|
|
208
|
+
'Beta',
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
it('marks the active language', () => {
|
|
212
|
+
const nav = {
|
|
213
|
+
languages: [
|
|
214
|
+
{ language: 'en', default: true, pages: pages('/en') },
|
|
215
|
+
{ language: 'es', pages: pages('/es') },
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
const result = run(nav, '/es/first');
|
|
219
|
+
expect(result.languages.filter(isActiveDivision).map((language) => language.language)).toEqual([
|
|
220
|
+
'es',
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
it('keeps every byte-identical sibling anchor active', () => {
|
|
224
|
+
const duplicated = { anchor: 'Duplicated', pages: pages('/duplicated') };
|
|
225
|
+
const nav = {
|
|
226
|
+
anchors: [structuredClone(duplicated), structuredClone(duplicated)],
|
|
227
|
+
};
|
|
228
|
+
const result = run(nav, '/duplicated/second');
|
|
229
|
+
expect(result.anchors.filter(isActiveDivision)).toHaveLength(2);
|
|
230
|
+
});
|
|
231
|
+
it('resolves divisions whose siblings expose no name', () => {
|
|
232
|
+
var _a;
|
|
233
|
+
const nav = {
|
|
234
|
+
tabs: [
|
|
235
|
+
{ tab: 'Named', pages: pages('/named') },
|
|
236
|
+
{ tab: '', pages: pages('/unnamed-first') },
|
|
237
|
+
{ tab: '', pages: pages('/unnamed-second') },
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
const result = run(nav, '/unnamed-second/first');
|
|
241
|
+
const active = result.tabs.filter(isActiveDivision);
|
|
242
|
+
expect(active).toHaveLength(1);
|
|
243
|
+
expect((_a = active[0]) === null || _a === void 0 ? void 0 : _a.href).toBe('/unnamed-second/first');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -79,17 +79,20 @@ export const filterDivisions = ({ currentPath, currentVersion, currentLanguage,
|
|
|
79
79
|
return { page: foundPage, groupsOrPages };
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
// the accessible-page list depends only on the entry, so it is built once for
|
|
83
|
+
// the whole loop, and lazily so an entry holding no page objects never pays for it
|
|
84
|
+
let accessiblePages;
|
|
82
85
|
for (const page of entry.pages) {
|
|
83
86
|
if (typeof page === 'object') {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
+
accessiblePages !== null && accessiblePages !== void 0 ? accessiblePages : (accessiblePages = parentGroups.length
|
|
88
|
+
? parentGroups
|
|
89
|
+
: entry.pages
|
|
90
|
+
.map((entryPage) => filterInaccessiblePagesRecursive(entryPage, userGroups, isPreview))
|
|
91
|
+
.filter((entryPage) => entryPage !== undefined));
|
|
87
92
|
const { page: foundPage, groupsOrPages } = findPageInNavigation({
|
|
88
93
|
currentPath,
|
|
89
94
|
entry: page,
|
|
90
|
-
parentGroups:
|
|
91
|
-
? parentGroups
|
|
92
|
-
: filteredPages,
|
|
95
|
+
parentGroups: accessiblePages,
|
|
93
96
|
isPreview,
|
|
94
97
|
ancestorDivisionNames,
|
|
95
98
|
});
|
|
@@ -141,10 +144,11 @@ export const filterDivisions = ({ currentPath, currentVersion, currentLanguage,
|
|
|
141
144
|
ancestorDivisionNames: newAncestorNames,
|
|
142
145
|
});
|
|
143
146
|
if (page) {
|
|
147
|
+
const isSameDivision = createDivisionMatcher(subDivisions);
|
|
144
148
|
const filteredSubDivisions = subDivisions
|
|
145
149
|
.filter((division) => isDivisionAccessible(division, userGroups, isPreview))
|
|
146
150
|
.map((item) => {
|
|
147
|
-
const isActive =
|
|
151
|
+
const isActive = isSameDivision(item, subDivision);
|
|
148
152
|
if ('hidden' in item && item.hidden && !isActive)
|
|
149
153
|
return undefined;
|
|
150
154
|
return buildDivisionItem({
|
|
@@ -568,8 +572,7 @@ function findMostRelevantDivisions(currentPath, decoratedNav, currentVersion, cu
|
|
|
568
572
|
mostSpecificGroups = isPageAccessible ? mostSpecificGroups : [];
|
|
569
573
|
if (mostSpecificGroups) {
|
|
570
574
|
const isStrictlyEqual = (entry) => {
|
|
571
|
-
return
|
|
572
|
-
JSON.stringify(structuredClone(entry)));
|
|
575
|
+
return JSON.stringify(mostSpecificGroups) === JSON.stringify(entry);
|
|
573
576
|
};
|
|
574
577
|
// look for divisions based on the most specific groups
|
|
575
578
|
function findMostRelevantDivisionsHelper(entry, parentGroups, ancestorDivisionNames = []) {
|
|
@@ -615,10 +618,11 @@ function findMostRelevantDivisions(currentPath, decoratedNav, currentVersion, cu
|
|
|
615
618
|
* - the specific group has been found in the division
|
|
616
619
|
*/
|
|
617
620
|
if (founded || !isPageAccessible) {
|
|
621
|
+
const isSameDivision = createDivisionMatcher(subDivisions);
|
|
618
622
|
const filteredSubDivisions = subDivisions
|
|
619
623
|
.filter((division) => isDivisionAccessible(division, userGroups, isPreview))
|
|
620
624
|
.map((item) => {
|
|
621
|
-
const isActive =
|
|
625
|
+
const isActive = isSameDivision(item, subDivision) && isPageAccessible === true;
|
|
622
626
|
if (item.hidden && !isActive)
|
|
623
627
|
return undefined;
|
|
624
628
|
return buildDivisionItem({
|
|
@@ -660,9 +664,17 @@ function findMaxMatchingPrefix(path1, path2) {
|
|
|
660
664
|
}
|
|
661
665
|
return path1.substring(0, matchingLength);
|
|
662
666
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
667
|
+
// both arguments always come from the same subDivisions array, so identity is the
|
|
668
|
+
// question this is really asking. sharing a name is the only way two distinct
|
|
669
|
+
// entries can serialize equally, so only those fall back to the deep comparison,
|
|
670
|
+
// which keeps the existing behaviour of marking every identical sibling active
|
|
671
|
+
function createDivisionMatcher(subDivisions) {
|
|
672
|
+
const names = subDivisions.map(findDivisionName);
|
|
673
|
+
if (new Set(names).size === names.length) {
|
|
674
|
+
return (item, subDivision) => item === subDivision;
|
|
675
|
+
}
|
|
676
|
+
return (item, subDivision) => item === subDivision || JSON.stringify(item) === JSON.stringify(subDivision);
|
|
677
|
+
}
|
|
666
678
|
function shouldSkipDivisionMatch({ subDivision, key, currentVersion, currentLanguage, subDivisions, }) {
|
|
667
679
|
const isVersionsConfig = key === 'versions' && !!currentVersion;
|
|
668
680
|
const isLanguagesConfig = key === 'languages' && !!currentLanguage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { generatePathToLanguageDict, generatePathToVersionDict } from '../divisions/index.js';
|
|
2
|
+
import { getScopedNavForPath } from './scopeNavToPath.js';
|
|
3
|
+
const navWithVersions = (versions) => ({
|
|
4
|
+
versions: versions.map((version, index) => ({
|
|
5
|
+
version,
|
|
6
|
+
default: index === 0,
|
|
7
|
+
groups: [
|
|
8
|
+
{
|
|
9
|
+
group: 'Guides',
|
|
10
|
+
pages: [
|
|
11
|
+
{ title: 'Overview', href: `/${version}/overview` },
|
|
12
|
+
{ title: 'Shared', href: '/shared' },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
describe('scoped nav path dictionaries', () => {
|
|
19
|
+
it('returns identical output across repeated calls with the same nav object', () => {
|
|
20
|
+
const nav = navWithVersions(['v2', 'v1']);
|
|
21
|
+
const first = getScopedNavForPath({ decoratedNav: nav, currentPath: '/v1/overview' });
|
|
22
|
+
const second = getScopedNavForPath({ decoratedNav: nav, currentPath: '/v1/overview' });
|
|
23
|
+
expect(JSON.stringify(second.scopedNav)).toBe(JSON.stringify(first.scopedNav));
|
|
24
|
+
expect(second.currentVersion).toBe(first.currentVersion);
|
|
25
|
+
expect([...second.firstHrefInVersion]).toEqual([...first.firstHrefInVersion]);
|
|
26
|
+
});
|
|
27
|
+
it('resolves different paths correctly after the dictionaries are warm', () => {
|
|
28
|
+
const nav = navWithVersions(['v2', 'v1']);
|
|
29
|
+
getScopedNavForPath({ decoratedNav: nav, currentPath: '/v2/overview' });
|
|
30
|
+
const scoped = getScopedNavForPath({ decoratedNav: nav, currentPath: '/v1/overview' });
|
|
31
|
+
expect(scoped.currentVersion).toBe('v1');
|
|
32
|
+
});
|
|
33
|
+
it('does not share dictionaries between distinct nav objects', () => {
|
|
34
|
+
const first = navWithVersions(['v2', 'v1']);
|
|
35
|
+
const second = navWithVersions(['v9', 'v8']);
|
|
36
|
+
expect(getScopedNavForPath({ decoratedNav: first, currentPath: '/v1/overview' }).currentVersion).toBe('v1');
|
|
37
|
+
expect(getScopedNavForPath({ decoratedNav: second, currentPath: '/v8/overview' }).currentVersion).toBe('v8');
|
|
38
|
+
expect(getScopedNavForPath({ decoratedNav: first, currentPath: '/v2/overview' }).currentVersion).toBe('v2');
|
|
39
|
+
});
|
|
40
|
+
it('does not share dictionaries between structurally identical nav objects', () => {
|
|
41
|
+
const original = navWithVersions(['v2', 'v1']);
|
|
42
|
+
const clone = structuredClone(original);
|
|
43
|
+
const fromOriginal = getScopedNavForPath({
|
|
44
|
+
decoratedNav: original,
|
|
45
|
+
currentPath: '/v1/overview',
|
|
46
|
+
});
|
|
47
|
+
const fromClone = getScopedNavForPath({ decoratedNav: clone, currentPath: '/v1/overview' });
|
|
48
|
+
expect(fromClone.currentVersion).toBe(fromOriginal.currentVersion);
|
|
49
|
+
expect(JSON.stringify(fromClone.scopedNav)).toBe(JSON.stringify(fromOriginal.scopedNav));
|
|
50
|
+
});
|
|
51
|
+
it('keeps the exported dictionary builders returning fresh maps', () => {
|
|
52
|
+
const nav = navWithVersions(['v2', 'v1']);
|
|
53
|
+
const versionDict = generatePathToVersionDict(nav);
|
|
54
|
+
const languageDict = generatePathToLanguageDict(nav);
|
|
55
|
+
versionDict.set('poisoned', 'nope');
|
|
56
|
+
languageDict.set('poisoned', 'nope');
|
|
57
|
+
expect(generatePathToVersionDict(nav).has('poisoned')).toBe(false);
|
|
58
|
+
expect(generatePathToLanguageDict(nav).has('poisoned')).toBe(false);
|
|
59
|
+
expect(getScopedNavForPath({ decoratedNav: nav, currentPath: '/v1/overview' }).currentVersion).toBe('v1');
|
|
60
|
+
});
|
|
61
|
+
it('does not mutate the navigation it is given', () => {
|
|
62
|
+
const nav = navWithVersions(['v2', 'v1']);
|
|
63
|
+
const before = JSON.stringify(nav);
|
|
64
|
+
getScopedNavForPath({ decoratedNav: nav, currentPath: '/v1/overview' });
|
|
65
|
+
getScopedNavForPath({ decoratedNav: nav, currentPath: '/shared' });
|
|
66
|
+
expect(JSON.stringify(nav)).toBe(before);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -475,8 +475,26 @@ export function scopeDocsConfigNavToPath(docsConfig, currentPath) {
|
|
|
475
475
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
476
476
|
navigation: scopeNode(docsConfig.navigation, { currentPath }) });
|
|
477
477
|
}
|
|
478
|
+
const pathToVersionDictCache = new WeakMap();
|
|
479
|
+
const pathToLanguageDictCache = new WeakMap();
|
|
480
|
+
function cachedPathToVersionDict(decoratedNav) {
|
|
481
|
+
const cached = pathToVersionDictCache.get(decoratedNav);
|
|
482
|
+
if (cached)
|
|
483
|
+
return cached;
|
|
484
|
+
const dict = generatePathToVersionDict(decoratedNav);
|
|
485
|
+
pathToVersionDictCache.set(decoratedNav, dict);
|
|
486
|
+
return dict;
|
|
487
|
+
}
|
|
488
|
+
function cachedPathToLanguageDict(decoratedNav) {
|
|
489
|
+
const cached = pathToLanguageDictCache.get(decoratedNav);
|
|
490
|
+
if (cached)
|
|
491
|
+
return cached;
|
|
492
|
+
const dict = generatePathToLanguageDict(decoratedNav);
|
|
493
|
+
pathToLanguageDictCache.set(decoratedNav, dict);
|
|
494
|
+
return dict;
|
|
495
|
+
}
|
|
478
496
|
export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = new Set(), isPreview = false, includeProductVersionMetadata = false, }) {
|
|
479
|
-
var _a, _b, _c, _d, _e, _f, _g
|
|
497
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
480
498
|
const containingProductNames = new Set();
|
|
481
499
|
collectContainingProductNames(
|
|
482
500
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
@@ -500,14 +518,16 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
|
|
|
500
518
|
if (initial.versions.length || initial.languages.length) {
|
|
501
519
|
const defaultVersion = (_a = initial.versions.find((version) => version.default)) === null || _a === void 0 ? void 0 : _a.version;
|
|
502
520
|
const defaultLanguage = (_b = initial.languages.find((language) => language.default)) === null || _b === void 0 ? void 0 : _b.language;
|
|
503
|
-
const initialVersion = getVersionOrLanguageFromPath('version', decoratedNav, currentPath, defaultVersion, (_d = (_c = initial.versions[0]) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '');
|
|
504
|
-
const initialLanguage = getVersionOrLanguageFromPath('language', decoratedNav, currentPath, defaultLanguage, (_f = (_e = initial.languages[0]) === null || _e === void 0 ? void 0 : _e.language) !== null && _f !== void 0 ? _f : undefined);
|
|
505
521
|
const strippedPath = optionallyRemoveLeadingSlash(currentPath);
|
|
506
|
-
const
|
|
522
|
+
const dictVersion = cachedPathToVersionDict(decoratedNav).get(strippedPath);
|
|
523
|
+
const dictLanguage = cachedPathToLanguageDict(decoratedNav).get(strippedPath);
|
|
524
|
+
const localeFromDict = locales.find((locale) => locale === dictLanguage);
|
|
507
525
|
currentVersion =
|
|
508
|
-
|
|
526
|
+
dictVersion ||
|
|
527
|
+
getVersionOrLanguageFromPath('version', decoratedNav, currentPath, defaultVersion, (_d = (_c = initial.versions[0]) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '') ||
|
|
528
|
+
undefined;
|
|
509
529
|
currentLanguage =
|
|
510
|
-
(
|
|
530
|
+
(_g = localeFromDict !== null && localeFromDict !== void 0 ? localeFromDict : getVersionOrLanguageFromPath('language', decoratedNav, currentPath, defaultLanguage, (_f = (_e = initial.languages[0]) === null || _e === void 0 ? void 0 : _e.language) !== null && _f !== void 0 ? _f : undefined)) !== null && _g !== void 0 ? _g : undefined;
|
|
511
531
|
}
|
|
512
532
|
const filtered = filterDivisions({
|
|
513
533
|
currentPath,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|