@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.
@@ -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
- const filteredPages = entry.pages
85
- .map((page) => filterInaccessiblePagesRecursive(page, userGroups, isPreview))
86
- .filter((page) => page !== undefined);
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: parentGroups.length
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 = compareDivisions(item, subDivision);
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 (JSON.stringify(structuredClone(mostSpecificGroups)) ===
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 = compareDivisions(item, subDivision) && isPageAccessible === true;
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
- const compareDivisions = (item, subDivision) => {
664
- return JSON.stringify(structuredClone(item)) === JSON.stringify(structuredClone(subDivision));
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, _h;
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 dictLanguage = generatePathToLanguageDict(decoratedNav).get(strippedPath);
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
- generatePathToVersionDict(decoratedNav).get(strippedPath) || initialVersion || undefined;
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
- (_h = (_g = locales.find((locale) => locale === dictLanguage)) !== null && _g !== void 0 ? _g : initialLanguage) !== null && _h !== void 0 ? _h : undefined;
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,