@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.
Files changed (38) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/navigation/decoratedNavigationConfig.d.ts +2 -0
  4. package/dist/navigation/decoratedNavigationConfig.js +204 -0
  5. package/dist/navigation/filterDivisions.d.ts +65 -0
  6. package/dist/navigation/filterDivisions.js +697 -0
  7. package/dist/navigation/filterDivisions.test.d.ts +1 -0
  8. package/dist/navigation/filterDivisions.test.js +670 -0
  9. package/dist/navigation/getScopedNavForPath.test.d.ts +1 -0
  10. package/dist/navigation/getScopedNavForPath.test.js +135 -0
  11. package/dist/navigation/getVersionOrLanguageFromPath.d.ts +12 -0
  12. package/dist/navigation/getVersionOrLanguageFromPath.js +71 -0
  13. package/dist/navigation/getVersionOrLanguageFromPath.test.d.ts +1 -0
  14. package/dist/navigation/getVersionOrLanguageFromPath.test.js +133 -0
  15. package/dist/navigation/index.d.ts +5 -0
  16. package/dist/navigation/index.js +5 -0
  17. package/dist/navigation/isCommonPage.d.ts +8 -0
  18. package/dist/navigation/isCommonPage.js +36 -0
  19. package/dist/navigation/isCommonPage.test.d.ts +1 -0
  20. package/dist/navigation/isCommonPage.test.js +56 -0
  21. package/dist/navigation/pathsDifferByOneSegment.test.d.ts +1 -0
  22. package/dist/navigation/pathsDifferByOneSegment.test.js +91 -0
  23. package/dist/navigation/scopeNavToPath.d.ts +18 -0
  24. package/dist/navigation/scopeNavToPath.js +455 -0
  25. package/dist/navigation/scopeNavToPath.test.d.ts +1 -0
  26. package/dist/navigation/scopeNavToPath.test.js +117 -0
  27. package/dist/navigation/scopeOffPathDivisions.test.d.ts +1 -0
  28. package/dist/navigation/scopeOffPathDivisions.test.js +852 -0
  29. package/dist/navigation/scopeProductNav.test.d.ts +1 -0
  30. package/dist/navigation/scopeProductNav.test.js +288 -0
  31. package/dist/navigation/updateFirstHrefInDivision.d.ts +29 -0
  32. package/dist/navigation/updateFirstHrefInDivision.js +383 -0
  33. package/dist/paths/isEqualIgnoringLeadingSlash.d.ts +1 -0
  34. package/dist/paths/isEqualIgnoringLeadingSlash.js +21 -0
  35. package/dist/paths/isEqualIgnoringLeadingSlash.test.d.ts +1 -0
  36. package/dist/paths/isEqualIgnoringLeadingSlash.test.js +43 -0
  37. package/dist/tsconfig.build.tsbuildinfo +1 -1
  38. 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 {};