@mintlify/common 1.0.1023 → 1.0.1024
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/navigation/decoratedNavigationConfig.d.ts +2 -0
- package/dist/navigation/decoratedNavigationConfig.js +204 -0
- package/dist/navigation/filterDivisions.d.ts +65 -0
- package/dist/navigation/filterDivisions.js +697 -0
- package/dist/navigation/filterDivisions.test.d.ts +1 -0
- package/dist/navigation/filterDivisions.test.js +670 -0
- package/dist/navigation/getScopedNavForPath.test.d.ts +1 -0
- package/dist/navigation/getScopedNavForPath.test.js +135 -0
- package/dist/navigation/getVersionOrLanguageFromPath.d.ts +12 -0
- package/dist/navigation/getVersionOrLanguageFromPath.js +71 -0
- package/dist/navigation/getVersionOrLanguageFromPath.test.d.ts +1 -0
- package/dist/navigation/getVersionOrLanguageFromPath.test.js +133 -0
- package/dist/navigation/index.d.ts +5 -0
- package/dist/navigation/index.js +5 -0
- package/dist/navigation/isCommonPage.d.ts +8 -0
- package/dist/navigation/isCommonPage.js +36 -0
- package/dist/navigation/isCommonPage.test.d.ts +1 -0
- package/dist/navigation/isCommonPage.test.js +56 -0
- package/dist/navigation/pathsDifferByOneSegment.test.d.ts +1 -0
- package/dist/navigation/pathsDifferByOneSegment.test.js +91 -0
- package/dist/navigation/scopeNavToPath.d.ts +18 -0
- package/dist/navigation/scopeNavToPath.js +455 -0
- package/dist/navigation/scopeNavToPath.test.d.ts +1 -0
- package/dist/navigation/scopeNavToPath.test.js +117 -0
- package/dist/navigation/scopeOffPathDivisions.test.d.ts +1 -0
- package/dist/navigation/scopeOffPathDivisions.test.js +852 -0
- package/dist/navigation/scopeProductNav.test.d.ts +1 -0
- package/dist/navigation/scopeProductNav.test.js +288 -0
- package/dist/navigation/updateFirstHrefInDivision.d.ts +29 -0
- package/dist/navigation/updateFirstHrefInDivision.js +383 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.d.ts +1 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.js +21 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.test.d.ts +1 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.test.js +43 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { locales } from '@mintlify/models';
|
|
2
|
+
import { generatePathToLanguageDict, generatePathToVersionDict } from '../divisions/index.js';
|
|
3
|
+
import { optionallyRemoveLeadingSlash } from '../fs/optionallySlash.js';
|
|
4
|
+
import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
|
|
5
|
+
import { filterDivisions } from './filterDivisions.js';
|
|
6
|
+
import { getFirstPageFromNavigation } from './getFirstPageFromNavigation.js';
|
|
7
|
+
import { getVersionOrLanguageFromPath } from './getVersionOrLanguageFromPath.js';
|
|
8
|
+
import { pathsDifferByOneSegment } from './updateFirstHrefInDivision.js';
|
|
9
|
+
const SCOPED_DIVISION_KEYS = ['versions', 'languages'];
|
|
10
|
+
const DIVISION_TYPE_BY_KEY = {
|
|
11
|
+
versions: 'version',
|
|
12
|
+
languages: 'language',
|
|
13
|
+
};
|
|
14
|
+
const CONTAINER_KEYS = [
|
|
15
|
+
'versions',
|
|
16
|
+
'languages',
|
|
17
|
+
'tabs',
|
|
18
|
+
'anchors',
|
|
19
|
+
'dropdowns',
|
|
20
|
+
'products',
|
|
21
|
+
'menu',
|
|
22
|
+
];
|
|
23
|
+
const STUB_STRIPPED_KEYS = new Set([
|
|
24
|
+
'anchors',
|
|
25
|
+
'asyncapi',
|
|
26
|
+
'dropdowns',
|
|
27
|
+
'global',
|
|
28
|
+
'groups',
|
|
29
|
+
'languages',
|
|
30
|
+
'menu',
|
|
31
|
+
'openapi',
|
|
32
|
+
'pages',
|
|
33
|
+
'products',
|
|
34
|
+
'tabs',
|
|
35
|
+
'versions',
|
|
36
|
+
]);
|
|
37
|
+
function isNavNode(value) {
|
|
38
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function isScopedDivisionKey(key) {
|
|
41
|
+
return key === 'versions' || key === 'languages';
|
|
42
|
+
}
|
|
43
|
+
function isDecoratedPageNode(node) {
|
|
44
|
+
return typeof node.href === 'string' && typeof node.title === 'string';
|
|
45
|
+
}
|
|
46
|
+
function pageAccessGroups(node) {
|
|
47
|
+
if (!Array.isArray(node.groups))
|
|
48
|
+
return undefined;
|
|
49
|
+
const groups = node.groups.filter((group) => typeof group === 'string');
|
|
50
|
+
return groups.length === node.groups.length ? groups : undefined;
|
|
51
|
+
}
|
|
52
|
+
// inHiddenSubtree propagates hidden from ancestor groups/divisions: a visible
|
|
53
|
+
// page under a hidden group is unreachable in getFirstPageFromNavigation
|
|
54
|
+
// order, and a stub baked from it would resurrect an invisible division
|
|
55
|
+
function collectPageCandidates(node, candidates, inHiddenSubtree = false) {
|
|
56
|
+
if (Array.isArray(node)) {
|
|
57
|
+
for (const entry of node) {
|
|
58
|
+
collectPageCandidates(entry, candidates, inHiddenSubtree);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!isNavNode(node))
|
|
63
|
+
return;
|
|
64
|
+
const hidden = inHiddenSubtree || node.hidden === true;
|
|
65
|
+
if (isDecoratedPageNode(node)) {
|
|
66
|
+
candidates.push({
|
|
67
|
+
href: node.href,
|
|
68
|
+
isRaw: false,
|
|
69
|
+
hidden,
|
|
70
|
+
groups: pageAccessGroups(node),
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const [key, value] of Object.entries(node)) {
|
|
75
|
+
if (key === 'pages' && Array.isArray(value)) {
|
|
76
|
+
for (const entry of value) {
|
|
77
|
+
if (typeof entry === 'string') {
|
|
78
|
+
candidates.push({ href: entry, isRaw: true, hidden });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
collectPageCandidates(entry, candidates, hidden);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (Array.isArray(value) || isNavNode(value)) {
|
|
86
|
+
collectPageCandidates(value, candidates, hidden);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function divisionContainsPath(division, currentPath) {
|
|
91
|
+
const candidates = [];
|
|
92
|
+
collectPageCandidates(division, candidates);
|
|
93
|
+
return candidates.some((candidate) => isEqualIgnoringLeadingSlash(candidate.href, currentPath));
|
|
94
|
+
}
|
|
95
|
+
function collectContainingProductNames(node, currentPath, names) {
|
|
96
|
+
if (Array.isArray(node)) {
|
|
97
|
+
for (const entry of node) {
|
|
98
|
+
if (isNavNode(entry) || Array.isArray(entry)) {
|
|
99
|
+
collectContainingProductNames(entry, currentPath, names);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const products = node.products;
|
|
105
|
+
if (Array.isArray(products)) {
|
|
106
|
+
for (const product of products) {
|
|
107
|
+
if (isNavNode(product) && divisionContainsPath(product, currentPath)) {
|
|
108
|
+
names.add(typeof product.product === 'string' ? product.product : undefined);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const value of Object.values(node)) {
|
|
113
|
+
if (isNavNode(value) || Array.isArray(value)) {
|
|
114
|
+
collectContainingProductNames(value, currentPath, names);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function pickStubTarget(division, ctx, type) {
|
|
119
|
+
const candidates = [];
|
|
120
|
+
collectPageCandidates(division, candidates);
|
|
121
|
+
// groups: [] means no group grants access (checkNavAccess denies it), so
|
|
122
|
+
// only a candidate with no groups key at all is anonymously visible
|
|
123
|
+
const visible = candidates.filter((candidate) => !candidate.hidden && candidate.groups === undefined);
|
|
124
|
+
const nonHidden = candidates.filter((candidate) => !candidate.hidden);
|
|
125
|
+
const pool = ctx.isPreview
|
|
126
|
+
? candidates
|
|
127
|
+
: visible.length
|
|
128
|
+
? visible
|
|
129
|
+
: nonHidden.length
|
|
130
|
+
? nonHidden
|
|
131
|
+
: candidates;
|
|
132
|
+
const exact = pool.find((candidate) => isEqualIgnoringLeadingSlash(candidate.href, ctx.currentPath));
|
|
133
|
+
if (exact)
|
|
134
|
+
return exact;
|
|
135
|
+
const pathMatch = pool.find((candidate) => pathsDifferByOneSegment(candidate.href, ctx.currentPath, type));
|
|
136
|
+
return pathMatch !== null && pathMatch !== void 0 ? pathMatch : pool[0];
|
|
137
|
+
}
|
|
138
|
+
// carrying the target's hidden/groups keeps a division with no anonymously
|
|
139
|
+
// visible page just as invisible when the client re-runs its access checks
|
|
140
|
+
// over the stub
|
|
141
|
+
function makeStubPage(title, target) {
|
|
142
|
+
const stub = { title, href: target.href };
|
|
143
|
+
if (target.hidden)
|
|
144
|
+
stub.hidden = true;
|
|
145
|
+
if (target.groups)
|
|
146
|
+
stub.groups = target.groups;
|
|
147
|
+
return stub;
|
|
148
|
+
}
|
|
149
|
+
function divisionLabel(division, key) {
|
|
150
|
+
const label = key === 'versions' ? division.version : division.language;
|
|
151
|
+
return typeof label === 'string' ? label : '';
|
|
152
|
+
}
|
|
153
|
+
function divisionValue(division, key) {
|
|
154
|
+
const value = key === 'versions' ? division.version : division.language;
|
|
155
|
+
return typeof value === 'string' ? value : undefined;
|
|
156
|
+
}
|
|
157
|
+
function resolveStubTarget(division, key, ctx) {
|
|
158
|
+
const targets = key === 'versions' ? ctx.versionTargets : ctx.languageTargets;
|
|
159
|
+
const value = divisionValue(division, key);
|
|
160
|
+
const resolved = targets === null || targets === void 0 ? void 0 : targets.get(value);
|
|
161
|
+
const fallback = pickStubTarget(division, ctx, DIVISION_TYPE_BY_KEY[key]);
|
|
162
|
+
// the jump-map walk records targets without access checks, so a precomputed
|
|
163
|
+
// entry only supplies the switch destination; hidden/groups still come from
|
|
164
|
+
// the division's real pages or an access-invisible division would resurface
|
|
165
|
+
if (resolved) {
|
|
166
|
+
return {
|
|
167
|
+
href: resolved.href,
|
|
168
|
+
isRaw: false,
|
|
169
|
+
hidden: (fallback === null || fallback === void 0 ? void 0 : fallback.hidden) === true,
|
|
170
|
+
groups: fallback === null || fallback === void 0 ? void 0 : fallback.groups,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return fallback;
|
|
174
|
+
}
|
|
175
|
+
function stubDivision(division, key, ctx) {
|
|
176
|
+
if (typeof division.href === 'string')
|
|
177
|
+
return division;
|
|
178
|
+
const target = resolveStubTarget(division, key, ctx);
|
|
179
|
+
// a division with no page candidates carries no page payload worth stripping,
|
|
180
|
+
// so keep it unchanged rather than inventing a switch target
|
|
181
|
+
if (!target)
|
|
182
|
+
return division;
|
|
183
|
+
const stub = {};
|
|
184
|
+
for (const [entryKey, value] of Object.entries(division)) {
|
|
185
|
+
if (!STUB_STRIPPED_KEYS.has(entryKey)) {
|
|
186
|
+
stub[entryKey] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// stub pages exist only so firstHrefInVersion/Language resolves a switch
|
|
190
|
+
// target; they are never rendered, so the division identifier stands in for
|
|
191
|
+
// a display title
|
|
192
|
+
stub.pages = target.isRaw ? [target.href] : [makeStubPage(divisionLabel(division, key), target)];
|
|
193
|
+
return stub;
|
|
194
|
+
}
|
|
195
|
+
const SIMPLE_DIVISION_KEYS = ['tabs', 'anchors'];
|
|
196
|
+
function isSimpleDivisionKey(key) {
|
|
197
|
+
return key === 'tabs' || key === 'anchors';
|
|
198
|
+
}
|
|
199
|
+
function simpleDivisionLabel(division, key) {
|
|
200
|
+
const label = key === 'tabs' ? division.tab : division.anchor;
|
|
201
|
+
return typeof label === 'string' ? label : '';
|
|
202
|
+
}
|
|
203
|
+
// versions/languages feed switch-target derivation, dropdowns feed the
|
|
204
|
+
// client's stateful dropdownCache hrefs, and the active product is client
|
|
205
|
+
// session state — divisions containing any of them stay whole
|
|
206
|
+
const STATEFUL_DIVISION_KEYS = new Set(['versions', 'languages', 'dropdowns', 'products']);
|
|
207
|
+
function containsStatefulDivision(node) {
|
|
208
|
+
if (Array.isArray(node))
|
|
209
|
+
return node.some(containsStatefulDivision);
|
|
210
|
+
if (!isNavNode(node))
|
|
211
|
+
return false;
|
|
212
|
+
for (const [key, value] of Object.entries(node)) {
|
|
213
|
+
if (STATEFUL_DIVISION_KEYS.has(key) && Array.isArray(value))
|
|
214
|
+
return true;
|
|
215
|
+
if ((Array.isArray(value) || isNavNode(value)) && containsStatefulDivision(value))
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
// mirrors buildDivisionItem's anonymous href resolution (first page in
|
|
221
|
+
// getFirstPageFromNavigation order that is neither hidden nor group-gated);
|
|
222
|
+
// falls back to the first raw candidate so a division with no anonymously
|
|
223
|
+
// visible page keeps its inaccessibility via the carried hidden/groups
|
|
224
|
+
function resolveVisibleTarget(division, ctx) {
|
|
225
|
+
var _a;
|
|
226
|
+
const page = getFirstPageFromNavigation(
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
228
|
+
division, (_a = ctx.userGroups) !== null && _a !== void 0 ? _a : new Set(), true, ctx.isPreview === true);
|
|
229
|
+
if (page) {
|
|
230
|
+
return { href: page.href, isRaw: false, hidden: page.hidden === true, groups: page.groups };
|
|
231
|
+
}
|
|
232
|
+
// preview renders every division even when no page is reachable (href '/');
|
|
233
|
+
// only the real subtree reproduces that, so signal keep-whole
|
|
234
|
+
if (ctx.isPreview)
|
|
235
|
+
return undefined;
|
|
236
|
+
const candidates = [];
|
|
237
|
+
collectPageCandidates(division, candidates);
|
|
238
|
+
return candidates[0];
|
|
239
|
+
}
|
|
240
|
+
function hasViewerVisiblePage(node, ctx) {
|
|
241
|
+
var _a;
|
|
242
|
+
return (getFirstPageFromNavigation(
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
244
|
+
node, (_a = ctx.userGroups) !== null && _a !== void 0 ? _a : new Set(), true, ctx.isPreview === true) !== undefined);
|
|
245
|
+
}
|
|
246
|
+
const MENU_STUBBABLE_KEYS = new Set(['pages', 'groups']);
|
|
247
|
+
// inactive tabs/products still render their full hover menu, so menu items
|
|
248
|
+
// survive stubbing with their metadata and a single resolved page each
|
|
249
|
+
function stubMenuItem(item, ctx) {
|
|
250
|
+
if (!isNavNode(item))
|
|
251
|
+
return item;
|
|
252
|
+
if (typeof item.href === 'string')
|
|
253
|
+
return item;
|
|
254
|
+
const subtreeKeys = Object.keys(item).filter((key) => STUB_STRIPPED_KEYS.has(key));
|
|
255
|
+
if (!subtreeKeys.length || !subtreeKeys.every((key) => MENU_STUBBABLE_KEYS.has(key))) {
|
|
256
|
+
return scopeNode(item, ctx);
|
|
257
|
+
}
|
|
258
|
+
const target = resolveVisibleTarget(item, ctx);
|
|
259
|
+
if (!target)
|
|
260
|
+
return item;
|
|
261
|
+
const stub = {};
|
|
262
|
+
for (const [entryKey, value] of Object.entries(item)) {
|
|
263
|
+
if (!STUB_STRIPPED_KEYS.has(entryKey)) {
|
|
264
|
+
stub[entryKey] = value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
stub.pages = target.isRaw
|
|
268
|
+
? [target.href]
|
|
269
|
+
: [makeStubPage(typeof item.item === 'string' ? item.item : '', target)];
|
|
270
|
+
return stub;
|
|
271
|
+
}
|
|
272
|
+
function stubSimpleDivision(division, key, ctx) {
|
|
273
|
+
if (typeof division.href === 'string')
|
|
274
|
+
return division;
|
|
275
|
+
if (containsStatefulDivision(division))
|
|
276
|
+
return scopeNode(division, ctx);
|
|
277
|
+
const target = resolveVisibleTarget(division, ctx);
|
|
278
|
+
if (!target)
|
|
279
|
+
return division;
|
|
280
|
+
const stub = {};
|
|
281
|
+
for (const [entryKey, value] of Object.entries(division)) {
|
|
282
|
+
if (entryKey === 'menu' && Array.isArray(value)) {
|
|
283
|
+
stub.menu = value.map((item) => stubMenuItem(item, ctx));
|
|
284
|
+
}
|
|
285
|
+
else if (!STUB_STRIPPED_KEYS.has(entryKey)) {
|
|
286
|
+
stub[entryKey] = value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
stub.pages = target.isRaw
|
|
290
|
+
? [target.href]
|
|
291
|
+
: [makeStubPage(simpleDivisionLabel(division, key), target)];
|
|
292
|
+
return stub;
|
|
293
|
+
}
|
|
294
|
+
function scopeSimpleDivisionArray(entries, key, ctx) {
|
|
295
|
+
const activeIndex = entries.findIndex((entry) => isNavNode(entry) && divisionContainsPath(entry, ctx.currentPath));
|
|
296
|
+
// when the path-containing division is invisible to the viewer (hidden-only
|
|
297
|
+
// or group-gated), the client's active-division fallback lands on a sibling
|
|
298
|
+
// instead — that sibling must keep its real subtree, so nothing is stubbed
|
|
299
|
+
const activeEntry = activeIndex === -1 ? undefined : entries[activeIndex];
|
|
300
|
+
const activeVisible = isNavNode(activeEntry) && hasViewerVisiblePage(activeEntry, ctx);
|
|
301
|
+
return entries.map((entry, index) => {
|
|
302
|
+
if (!isNavNode(entry))
|
|
303
|
+
return entry;
|
|
304
|
+
// without a path match the active division is unknowable, so nothing is stubbed
|
|
305
|
+
if (!activeVisible || index === activeIndex)
|
|
306
|
+
return scopeNode(entry, ctx);
|
|
307
|
+
return stubSimpleDivision(entry, key, ctx);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// the same collapse applied to stubbed tabs' menus, but inside the active
|
|
311
|
+
// tab/anchor: the item containing the path keeps its subtree (it feeds the
|
|
312
|
+
// sidebar), siblings survive as hover-menu metadata plus one resolved page
|
|
313
|
+
function scopeMenuArray(entries, ctx) {
|
|
314
|
+
const activeIndex = entries.findIndex((entry) => isNavNode(entry) && divisionContainsPath(entry, ctx.currentPath));
|
|
315
|
+
const activeEntry = activeIndex === -1 ? undefined : entries[activeIndex];
|
|
316
|
+
const activeVisible = isNavNode(activeEntry) && hasViewerVisiblePage(activeEntry, ctx);
|
|
317
|
+
return entries.map((entry, index) => {
|
|
318
|
+
if (!isNavNode(entry))
|
|
319
|
+
return entry;
|
|
320
|
+
if (!activeVisible || index === activeIndex)
|
|
321
|
+
return scopeNode(entry, ctx);
|
|
322
|
+
return stubMenuItem(entry, ctx);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function scopeDivisionArray(entries, key, ctx) {
|
|
326
|
+
if (ctx.skipVersionLanguageStubs) {
|
|
327
|
+
return entries.map((entry) => (isNavNode(entry) ? scopeNode(entry, ctx) : entry));
|
|
328
|
+
}
|
|
329
|
+
const currentValue = key === 'versions' ? ctx.currentVersion : ctx.currentLanguage;
|
|
330
|
+
let activeIndex = -1;
|
|
331
|
+
if (currentValue) {
|
|
332
|
+
activeIndex = entries.findIndex((entry) => isNavNode(entry) &&
|
|
333
|
+
divisionValue(entry, key) === currentValue &&
|
|
334
|
+
divisionContainsPath(entry, ctx.currentPath));
|
|
335
|
+
if (activeIndex === -1) {
|
|
336
|
+
activeIndex = entries.findIndex((entry) => isNavNode(entry) && divisionValue(entry, key) === currentValue);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (activeIndex === -1) {
|
|
340
|
+
activeIndex = entries.findIndex((entry) => isNavNode(entry) && divisionContainsPath(entry, ctx.currentPath));
|
|
341
|
+
}
|
|
342
|
+
if (activeIndex === -1) {
|
|
343
|
+
activeIndex = entries.findIndex((entry) => isNavNode(entry) && entry.default === true);
|
|
344
|
+
}
|
|
345
|
+
if (activeIndex === -1)
|
|
346
|
+
activeIndex = 0;
|
|
347
|
+
return entries.map((entry, index) => {
|
|
348
|
+
if (!isNavNode(entry))
|
|
349
|
+
return entry;
|
|
350
|
+
if (index === activeIndex)
|
|
351
|
+
return scopeNode(entry, ctx);
|
|
352
|
+
return stubDivision(entry, key, ctx);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function scopeNode(node, ctx) {
|
|
356
|
+
const scoped = Object.assign({}, node);
|
|
357
|
+
for (const key of CONTAINER_KEYS) {
|
|
358
|
+
const value = scoped[key];
|
|
359
|
+
if (!Array.isArray(value))
|
|
360
|
+
continue;
|
|
361
|
+
if (isScopedDivisionKey(key) && value.length > 1) {
|
|
362
|
+
scoped[key] = scopeDivisionArray(value, key, ctx);
|
|
363
|
+
}
|
|
364
|
+
else if (isSimpleDivisionKey(key) && value.length > 1) {
|
|
365
|
+
scoped[key] = scopeSimpleDivisionArray(value, key, ctx);
|
|
366
|
+
}
|
|
367
|
+
else if (key === 'menu' &&
|
|
368
|
+
value.length > 1 &&
|
|
369
|
+
(typeof scoped.tab === 'string' || typeof scoped.anchor === 'string')) {
|
|
370
|
+
// menus on products stay whole: the active product is client session
|
|
371
|
+
// state, so the server cannot know which product's menu will render
|
|
372
|
+
scoped[key] = scopeMenuArray(value, ctx);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
scoped[key] = value.map((entry) => (isNavNode(entry) ? scopeNode(entry, ctx) : entry));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return scoped;
|
|
379
|
+
}
|
|
380
|
+
export function scopeDecoratedNavToPath(nav, currentPath) {
|
|
381
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
382
|
+
return scopeNode(nav, { currentPath });
|
|
383
|
+
}
|
|
384
|
+
export function scopeDocsConfigNavToPath(docsConfig, currentPath) {
|
|
385
|
+
return Object.assign(Object.assign({}, docsConfig), {
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
387
|
+
navigation: scopeNode(docsConfig.navigation, { currentPath }) });
|
|
388
|
+
}
|
|
389
|
+
export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = new Set(), isPreview = false, }) {
|
|
390
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
391
|
+
const containingProductNames = new Set();
|
|
392
|
+
collectContainingProductNames(
|
|
393
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
394
|
+
decoratedNav, currentPath, containingProductNames);
|
|
395
|
+
const soleProductName = containingProductNames.size === 1 ? [...containingProductNames][0] : undefined;
|
|
396
|
+
const currentProduct = typeof soleProductName === 'string' ? soleProductName : undefined;
|
|
397
|
+
const productAmbiguous = containingProductNames.size > 1;
|
|
398
|
+
const initial = filterDivisions({
|
|
399
|
+
currentPath,
|
|
400
|
+
decoratedNav,
|
|
401
|
+
userGroups,
|
|
402
|
+
currentVersion: undefined,
|
|
403
|
+
currentLanguage: undefined,
|
|
404
|
+
currentProduct,
|
|
405
|
+
shouldUseDivisionMatch: false,
|
|
406
|
+
cache: { dropdownCache: new Map() },
|
|
407
|
+
isPreview,
|
|
408
|
+
});
|
|
409
|
+
let currentVersion;
|
|
410
|
+
let currentLanguage;
|
|
411
|
+
if (initial.versions.length || initial.languages.length) {
|
|
412
|
+
const defaultVersion = (_a = initial.versions.find((version) => version.default)) === null || _a === void 0 ? void 0 : _a.version;
|
|
413
|
+
const defaultLanguage = (_b = initial.languages.find((language) => language.default)) === null || _b === void 0 ? void 0 : _b.language;
|
|
414
|
+
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 : '');
|
|
415
|
+
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);
|
|
416
|
+
const strippedPath = optionallyRemoveLeadingSlash(currentPath);
|
|
417
|
+
const dictLanguage = generatePathToLanguageDict(decoratedNav).get(strippedPath);
|
|
418
|
+
currentVersion =
|
|
419
|
+
generatePathToVersionDict(decoratedNav).get(strippedPath) || initialVersion || undefined;
|
|
420
|
+
currentLanguage =
|
|
421
|
+
(_h = (_g = locales.find((locale) => locale === dictLanguage)) !== null && _g !== void 0 ? _g : initialLanguage) !== null && _h !== void 0 ? _h : undefined;
|
|
422
|
+
}
|
|
423
|
+
const filtered = filterDivisions({
|
|
424
|
+
currentPath,
|
|
425
|
+
decoratedNav,
|
|
426
|
+
userGroups,
|
|
427
|
+
currentVersion,
|
|
428
|
+
currentLanguage,
|
|
429
|
+
currentProduct,
|
|
430
|
+
shouldUseDivisionMatch: true,
|
|
431
|
+
cache: { dropdownCache: new Map() },
|
|
432
|
+
isPreview,
|
|
433
|
+
});
|
|
434
|
+
const scopedNav = scopeNode(
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
436
|
+
decoratedNav, {
|
|
437
|
+
currentPath,
|
|
438
|
+
currentVersion,
|
|
439
|
+
currentLanguage,
|
|
440
|
+
versionTargets: filtered.firstHrefInVersion,
|
|
441
|
+
languageTargets: filtered.firstHrefInLanguage,
|
|
442
|
+
userGroups,
|
|
443
|
+
isPreview,
|
|
444
|
+
skipVersionLanguageStubs: productAmbiguous,
|
|
445
|
+
}
|
|
446
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
|
|
447
|
+
);
|
|
448
|
+
return {
|
|
449
|
+
scopedNav,
|
|
450
|
+
firstHrefInVersion: productAmbiguous ? new Map() : filtered.firstHrefInVersion,
|
|
451
|
+
firstHrefInLanguage: productAmbiguous ? new Map() : filtered.firstHrefInLanguage,
|
|
452
|
+
currentVersion,
|
|
453
|
+
currentLanguage,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { scopeDecoratedNavToPath, scopeDocsConfigNavToPath } from './scopeNavToPath.js';
|
|
3
|
+
const page = (href, title = href) => ({ title, href });
|
|
4
|
+
const loose = (value) => value;
|
|
5
|
+
const decoratedNav = {
|
|
6
|
+
languages: [
|
|
7
|
+
{
|
|
8
|
+
language: 'en',
|
|
9
|
+
default: true,
|
|
10
|
+
groups: [
|
|
11
|
+
{
|
|
12
|
+
group: 'Getting started',
|
|
13
|
+
pages: [page('/get-started/intro'), page('/get-started/install')],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
language: 'es',
|
|
19
|
+
navbar: { links: [{ label: 'Soporte', href: 'https://example.com' }] },
|
|
20
|
+
groups: [
|
|
21
|
+
{
|
|
22
|
+
group: 'Comenzar',
|
|
23
|
+
pages: [page('/es/get-started/intro'), page('/es/get-started/install')],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
language: 'fr',
|
|
29
|
+
groups: [{ group: 'Démarrer', pages: [page('/fr/other/page')] }],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
describe('scopeDecoratedNavToPath', () => {
|
|
34
|
+
it('keeps the active division subtree and stubs siblings with an equivalent-page href', () => {
|
|
35
|
+
const scoped = scopeDecoratedNavToPath(decoratedNav, '/get-started/intro');
|
|
36
|
+
const [en, es, fr] = loose(scoped).languages;
|
|
37
|
+
if (!en || !es || !fr)
|
|
38
|
+
throw new Error('missing languages');
|
|
39
|
+
expect(en).toEqual(loose(decoratedNav).languages[0]);
|
|
40
|
+
expect(es.groups).toBeUndefined();
|
|
41
|
+
expect(es.language).toBe('es');
|
|
42
|
+
expect(es.navbar).toBeDefined();
|
|
43
|
+
expect(es.pages).toEqual([{ title: 'es', href: '/es/get-started/intro' }]);
|
|
44
|
+
expect(fr.pages).toEqual([{ title: 'fr', href: '/fr/other/page' }]);
|
|
45
|
+
});
|
|
46
|
+
it('falls back to the default division when the path is not found', () => {
|
|
47
|
+
const scoped = scopeDecoratedNavToPath(decoratedNav, '/does/not/exist');
|
|
48
|
+
const [en, es] = loose(scoped).languages;
|
|
49
|
+
if (!en || !es)
|
|
50
|
+
throw new Error('missing languages');
|
|
51
|
+
expect(en.groups).toBeDefined();
|
|
52
|
+
expect(es.pages).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
it('scopes nested versions inside the active language', () => {
|
|
55
|
+
var _a;
|
|
56
|
+
const nav = {
|
|
57
|
+
languages: [
|
|
58
|
+
{
|
|
59
|
+
language: 'en',
|
|
60
|
+
versions: [
|
|
61
|
+
{ version: 'v2', groups: [{ group: 'Docs', pages: [page('/v2/a'), page('/v2/b')] }] },
|
|
62
|
+
{ version: 'v1', groups: [{ group: 'Docs', pages: [page('/v1/a')] }] },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{ language: 'es', pages: [page('/es/a')] },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
const scoped = scopeDecoratedNavToPath(nav, '/v1/a');
|
|
69
|
+
const [en, es] = loose(scoped).languages;
|
|
70
|
+
if (!en || !es)
|
|
71
|
+
throw new Error('missing languages');
|
|
72
|
+
const [v2, v1] = (_a = en.versions) !== null && _a !== void 0 ? _a : [];
|
|
73
|
+
if (!v2 || !v1)
|
|
74
|
+
throw new Error('missing versions');
|
|
75
|
+
expect(v1.groups).toBeDefined();
|
|
76
|
+
expect(v2.groups).toBeUndefined();
|
|
77
|
+
expect(v2.pages).toEqual([{ title: 'v2', href: '/v2/a' }]);
|
|
78
|
+
expect(es.pages).toEqual([{ title: 'es', href: '/es/a' }]);
|
|
79
|
+
});
|
|
80
|
+
it('leaves href-only divisions untouched', () => {
|
|
81
|
+
const nav = {
|
|
82
|
+
versions: [
|
|
83
|
+
{ version: 'v3', pages: [page('/v3/a')] },
|
|
84
|
+
{ version: 'v2', href: 'https://v2.example.com' },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const scoped = scopeDecoratedNavToPath(nav, '/v3/a');
|
|
88
|
+
const [, v2] = loose(scoped).versions;
|
|
89
|
+
expect(v2).toEqual({ version: 'v2', href: 'https://v2.example.com' });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('scopeDocsConfigNavToPath', () => {
|
|
93
|
+
it('stubs sibling raw-language trees with a single page path while keeping metadata', () => {
|
|
94
|
+
const docsConfig = {
|
|
95
|
+
name: 'Test',
|
|
96
|
+
navigation: {
|
|
97
|
+
languages: [
|
|
98
|
+
{ language: 'en', default: true, pages: ['get-started/intro', 'get-started/install'] },
|
|
99
|
+
{
|
|
100
|
+
language: 'es',
|
|
101
|
+
navbar: { links: [] },
|
|
102
|
+
footer: { socials: {} },
|
|
103
|
+
pages: ['es/get-started/intro', 'es/get-started/install'],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const scoped = scopeDocsConfigNavToPath(docsConfig, '/get-started/install');
|
|
109
|
+
const [en, es] = loose(scoped.navigation).languages;
|
|
110
|
+
if (!en || !es)
|
|
111
|
+
throw new Error('missing languages');
|
|
112
|
+
expect(en.pages).toHaveLength(2);
|
|
113
|
+
expect(es.pages).toEqual(['es/get-started/install']);
|
|
114
|
+
expect(es.navbar).toBeDefined();
|
|
115
|
+
expect(es.footer).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|