@mintlify/common 1.0.1023 → 1.0.1025

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 (42) 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/scopeNavStateFlip.test.d.ts +1 -0
  24. package/dist/navigation/scopeNavStateFlip.test.js +567 -0
  25. package/dist/navigation/scopeNavToPath.d.ts +18 -0
  26. package/dist/navigation/scopeNavToPath.js +502 -0
  27. package/dist/navigation/scopeNavToPath.test.d.ts +1 -0
  28. package/dist/navigation/scopeNavToPath.test.js +117 -0
  29. package/dist/navigation/scopeOffPathDivisions.test.d.ts +1 -0
  30. package/dist/navigation/scopeOffPathDivisions.test.js +852 -0
  31. package/dist/navigation/scopeProductNav.test.d.ts +1 -0
  32. package/dist/navigation/scopeProductNav.test.js +288 -0
  33. package/dist/navigation/updateFirstHrefInDivision.d.ts +30 -0
  34. package/dist/navigation/updateFirstHrefInDivision.js +388 -0
  35. package/dist/navigation/updateFirstHrefInDivision.test.d.ts +1 -0
  36. package/dist/navigation/updateFirstHrefInDivision.test.js +52 -0
  37. package/dist/paths/isEqualIgnoringLeadingSlash.d.ts +1 -0
  38. package/dist/paths/isEqualIgnoringLeadingSlash.js +21 -0
  39. package/dist/paths/isEqualIgnoringLeadingSlash.test.d.ts +1 -0
  40. package/dist/paths/isEqualIgnoringLeadingSlash.test.js +43 -0
  41. package/dist/tsconfig.build.tsbuildinfo +1 -1
  42. package/package.json +3 -3
@@ -0,0 +1,388 @@
1
+ import { locales } from '@mintlify/models';
2
+ import { divisions, } from '@mintlify/validation';
3
+ import { readObjectArrayProperty } from '../divisions/iterateObjectArrayProperty.js';
4
+ import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
5
+ import { isPage } from './isPage.js';
6
+ export var DivisionMatchLevel;
7
+ (function (DivisionMatchLevel) {
8
+ DivisionMatchLevel[DivisionMatchLevel["EXACT"] = 0] = "EXACT";
9
+ DivisionMatchLevel[DivisionMatchLevel["PATH"] = 1] = "PATH";
10
+ DivisionMatchLevel[DivisionMatchLevel["GROUP"] = 2] = "GROUP";
11
+ DivisionMatchLevel[DivisionMatchLevel["DIVISION"] = 3] = "DIVISION";
12
+ DivisionMatchLevel[DivisionMatchLevel["NONE"] = 4] = "NONE";
13
+ DivisionMatchLevel[DivisionMatchLevel["UNVERSIONED"] = 5] = "UNVERSIONED";
14
+ })(DivisionMatchLevel || (DivisionMatchLevel = {}));
15
+ const ACCEPTED_BASE_PATHS = ['docs', 'doc', 'documentation', 'help'];
16
+ export function updateFirstHrefInDivision({ type, currentPath, entry, parentGroups, nearestDivisionValue, isActiveGroup, inActiveDivision, currentDivisionValue, allDivisionValues, firstHrefInDivision, navigationDivisions, isHiddenPage, activeGroupNames, }) {
17
+ if (typeof entry !== 'object')
18
+ return;
19
+ if (isPage(entry)) {
20
+ maybeUpdateFirstHrefInDivision(type, entry.href, nearestDivisionValue, isActiveGroup, inActiveDivision, currentPath, currentDivisionValue, allDivisionValues, firstHrefInDivision, isHiddenPage);
21
+ }
22
+ if ('pages' in entry && Array.isArray(entry.pages)) {
23
+ const root = 'root' in entry && entry.root != null && typeof entry.root === 'object'
24
+ ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- root is a DecoratedNavigationPage
25
+ entry.root
26
+ : undefined;
27
+ const isRootActive = root !== undefined &&
28
+ isPage(root) &&
29
+ isEqualIgnoringLeadingSlash(root.href, currentPath) &&
30
+ currentDivisionValue === nearestDivisionValue;
31
+ const isActivePages = entry.pages.some((subEntry) => isPage(subEntry) && isEqualIgnoringLeadingSlash(subEntry.href, currentPath)) && currentDivisionValue === nearestDivisionValue;
32
+ if (root !== undefined) {
33
+ updateFirstHrefInDivision({
34
+ type,
35
+ currentPath,
36
+ entry: root,
37
+ parentGroups: parentGroups.length ? parentGroups : entry.pages,
38
+ nearestDivisionValue,
39
+ isActiveGroup: isActiveGroup || isActivePages || isRootActive,
40
+ inActiveDivision,
41
+ currentDivisionValue,
42
+ allDivisionValues,
43
+ firstHrefInDivision,
44
+ navigationDivisions,
45
+ isHiddenPage,
46
+ activeGroupNames,
47
+ });
48
+ }
49
+ for (const page of entry.pages) {
50
+ if (typeof page === 'object') {
51
+ updateFirstHrefInDivision({
52
+ type,
53
+ currentPath,
54
+ entry: page,
55
+ parentGroups: parentGroups.length ? parentGroups : entry.pages,
56
+ nearestDivisionValue,
57
+ isActiveGroup: isActiveGroup || isActivePages || isRootActive,
58
+ inActiveDivision,
59
+ currentDivisionValue,
60
+ allDivisionValues,
61
+ firstHrefInDivision,
62
+ navigationDivisions,
63
+ isHiddenPage,
64
+ activeGroupNames,
65
+ });
66
+ }
67
+ }
68
+ }
69
+ if ('groups' in entry && !isPage(entry)) {
70
+ for (const group of entry.groups) {
71
+ if (typeof group !== 'object')
72
+ continue;
73
+ const groupName = 'group' in group && typeof group.group === 'string' ? group.group : undefined;
74
+ const matchesCurrentDivisionGroup = checkActiveGroup(group, currentPath) && currentDivisionValue === nearestDivisionValue;
75
+ const matchesActiveSiblingGroup = type === 'version' &&
76
+ currentDivisionValue !== nearestDivisionValue &&
77
+ !!groupName &&
78
+ (activeGroupNames === null || activeGroupNames === void 0 ? void 0 : activeGroupNames.has(groupName)) === true;
79
+ const isActiveGroup = matchesCurrentDivisionGroup || matchesActiveSiblingGroup;
80
+ updateFirstHrefInDivision({
81
+ type,
82
+ currentPath,
83
+ entry: group,
84
+ parentGroups: entry.groups,
85
+ nearestDivisionValue,
86
+ isActiveGroup,
87
+ inActiveDivision,
88
+ currentDivisionValue,
89
+ allDivisionValues,
90
+ firstHrefInDivision,
91
+ navigationDivisions,
92
+ isHiddenPage,
93
+ activeGroupNames,
94
+ });
95
+ }
96
+ }
97
+ for (const key of [...divisions, 'groups']) {
98
+ const subDivisions = readObjectArrayProperty(entry, key);
99
+ if (!subDivisions)
100
+ continue;
101
+ let inActiveDivision = false;
102
+ if (allDivisionValues && allDivisionValues.length) {
103
+ if (type === 'version' && key === 'versions') {
104
+ const localDivisionValueCount = navigationDivisions.versions.filter((version) => !version.isGlobal).length;
105
+ inActiveDivision =
106
+ subDivisions.length === localDivisionValueCount &&
107
+ subDivisions.every((subDivision) =>
108
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
109
+ allDivisionValues.includes(subDivision.version));
110
+ }
111
+ else if (type === 'language' && key === 'languages') {
112
+ const localDivisionValueCount = navigationDivisions.languages.filter((language) => !language.isGlobal).length;
113
+ inActiveDivision =
114
+ subDivisions.length === localDivisionValueCount &&
115
+ subDivisions.every((subDivision) =>
116
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
117
+ allDivisionValues.includes(subDivision.language));
118
+ }
119
+ }
120
+ let activeGroupsInVersionContext = activeGroupNames;
121
+ if (type === 'version' && key === 'versions' && currentDivisionValue) {
122
+ const currentVersionDivision = subDivisions.find((subDivision) => 'version' in subDivision &&
123
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
124
+ subDivision.version === currentDivisionValue);
125
+ if (currentVersionDivision) {
126
+ activeGroupsInVersionContext = findActiveGroupNames(
127
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
128
+ currentVersionDivision, currentPath);
129
+ }
130
+ }
131
+ for (const subDivision of subDivisions) {
132
+ let divisionValue;
133
+ if (type === 'version' && key === 'versions') {
134
+ divisionValue =
135
+ 'version' in subDivision
136
+ ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
137
+ subDivision.version
138
+ : undefined;
139
+ }
140
+ else if (type === 'language' && key === 'languages') {
141
+ divisionValue =
142
+ 'language' in subDivision
143
+ ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
144
+ subDivision.language
145
+ : undefined;
146
+ }
147
+ updateFirstHrefInDivision({
148
+ type,
149
+ currentPath,
150
+ entry: subDivision,
151
+ parentGroups: key === 'groups' && 'groups' in entry
152
+ ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
153
+ entry.groups
154
+ : parentGroups,
155
+ nearestDivisionValue: divisionValue !== null && divisionValue !== void 0 ? divisionValue : nearestDivisionValue,
156
+ isActiveGroup,
157
+ inActiveDivision: inActiveDivision,
158
+ currentDivisionValue,
159
+ allDivisionValues,
160
+ firstHrefInDivision,
161
+ navigationDivisions,
162
+ isHiddenPage,
163
+ activeGroupNames: activeGroupsInVersionContext,
164
+ });
165
+ }
166
+ }
167
+ }
168
+ function findActiveGroupNames(entry, currentPath, activeGroupNames = new Set()) {
169
+ if (typeof entry !== 'object')
170
+ return activeGroupNames;
171
+ if ('groups' in entry && !isPage(entry)) {
172
+ for (const group of entry.groups) {
173
+ if (typeof group !== 'object')
174
+ continue;
175
+ if ('group' in group &&
176
+ typeof group.group === 'string' &&
177
+ checkActiveGroup(group, currentPath)) {
178
+ activeGroupNames.add(group.group);
179
+ }
180
+ findActiveGroupNames(group, currentPath, activeGroupNames);
181
+ }
182
+ }
183
+ if ('pages' in entry && Array.isArray(entry.pages)) {
184
+ if ('root' in entry && typeof entry.root === 'object') {
185
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
186
+ findActiveGroupNames(entry.root, currentPath, activeGroupNames);
187
+ }
188
+ for (const page of entry.pages) {
189
+ if (typeof page === 'object') {
190
+ findActiveGroupNames(page, currentPath, activeGroupNames);
191
+ }
192
+ }
193
+ }
194
+ for (const key of divisions) {
195
+ const subDivisions = readObjectArrayProperty(entry, key);
196
+ if (!subDivisions)
197
+ continue;
198
+ for (const subDivision of subDivisions) {
199
+ findActiveGroupNames(subDivision, currentPath, activeGroupNames);
200
+ }
201
+ }
202
+ return activeGroupNames;
203
+ }
204
+ function parseLanguagePath(segments) {
205
+ let index = 0;
206
+ let locale;
207
+ let hasDocsBase = false;
208
+ const firstSegment = segments[0];
209
+ const firstIsDocsBase = !!firstSegment && ACCEPTED_BASE_PATHS.includes(firstSegment);
210
+ const firstIsLocale = !!firstSegment &&
211
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
212
+ locales.includes(firstSegment);
213
+ if (firstIsDocsBase) {
214
+ hasDocsBase = true;
215
+ index = 1;
216
+ const maybeLocale = segments[index];
217
+ if (maybeLocale &&
218
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
219
+ locales.includes(maybeLocale)) {
220
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
221
+ locale = maybeLocale;
222
+ index++;
223
+ }
224
+ }
225
+ else if (firstIsLocale) {
226
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
227
+ locale = firstSegment;
228
+ index = 1;
229
+ const maybeDocsBase = segments[index];
230
+ if (maybeDocsBase && ACCEPTED_BASE_PATHS.includes(maybeDocsBase)) {
231
+ hasDocsBase = true;
232
+ index++;
233
+ }
234
+ }
235
+ let contentSegments = segments.slice(index);
236
+ if (contentSegments.at(-1) === 'index') {
237
+ contentSegments = contentSegments.slice(0, -1);
238
+ }
239
+ return { hasDocsBase, locale, contentSegments };
240
+ }
241
+ /**
242
+ * This function attempts to maintain some context when switching versions by following these rules:
243
+ * 1. stay on the active page, if it is in the new version (unversioned)
244
+ * 2. navigate to the first page that is in the new version AND in the active group, if one exists
245
+ * 3. navigate to the first page that is in the new version
246
+ *
247
+ * @param href The href of the page being processed
248
+ * @param finalVersion The version of the page being processed
249
+ * @param inActiveGroup Whether the page being processed is in the same group as the active page
250
+ * @param currentPath The path of the active page
251
+ * @param currentVersion The version of the active page
252
+ * @param versionStrings If defined, the array of version strings
253
+ * @param firstHrefInDivision A map from version strings to the href that should be navigated to when switching to that version
254
+ */
255
+ function maybeUpdateFirstHrefInDivision(type, href, finalVersion, inActiveGroup, inActiveDivision, currentPath, currentVersion, versionStrings, firstHrefInDivision, isHiddenPage) {
256
+ const versionsToUpdate = finalVersion !== undefined ? [finalVersion] : (versionStrings !== null && versionStrings !== void 0 ? versionStrings : []);
257
+ // now that we support index.mdx, the href might empty string
258
+ // router.push cannot take empty string as url
259
+ href = href || '/';
260
+ // always use the current page if the current page is unversioned
261
+ if (href === currentPath) {
262
+ for (const version of versionsToUpdate) {
263
+ firstHrefInDivision.set(version, { href, matchLevel: DivisionMatchLevel.EXACT });
264
+ }
265
+ return;
266
+ }
267
+ // if the page is hidden, assign the current path to the current version with EXACT match level
268
+ if (isHiddenPage && currentVersion !== undefined) {
269
+ firstHrefInDivision.set(currentVersion, {
270
+ href: currentPath,
271
+ matchLevel: DivisionMatchLevel.EXACT,
272
+ });
273
+ }
274
+ // if currentPath and href differ by exactly one path segment, and it's not the last,
275
+ // we make the BOLD assumption that href is a different version of the same page
276
+ if (currentVersion !== undefined && finalVersion !== undefined) {
277
+ const existingRef = firstHrefInDivision.get(finalVersion);
278
+ if ((existingRef === undefined || DivisionMatchLevel.PATH < existingRef.matchLevel) &&
279
+ pathsDifferByOneSegment(href, currentPath, type)) {
280
+ firstHrefInDivision.set(finalVersion, { href, matchLevel: DivisionMatchLevel.PATH });
281
+ }
282
+ }
283
+ if (inActiveGroup) {
284
+ for (const version of versionsToUpdate) {
285
+ const existingHref = firstHrefInDivision.get(version);
286
+ if (existingHref === undefined || DivisionMatchLevel.GROUP < existingHref.matchLevel) {
287
+ firstHrefInDivision.set(version, { href, matchLevel: DivisionMatchLevel.GROUP });
288
+ }
289
+ }
290
+ return;
291
+ }
292
+ if (inActiveDivision) {
293
+ for (const version of versionsToUpdate) {
294
+ const existingHref = firstHrefInDivision.get(version);
295
+ if (existingHref === undefined || DivisionMatchLevel.DIVISION < existingHref.matchLevel) {
296
+ firstHrefInDivision.set(version, { href, matchLevel: DivisionMatchLevel.DIVISION });
297
+ }
298
+ }
299
+ return;
300
+ }
301
+ // a page that belongs to the target division beats a division-less catch-all,
302
+ // so rule 3 ("first page in the new version") holds regardless of walk order
303
+ const fallbackLevel = finalVersion !== undefined ? DivisionMatchLevel.NONE : DivisionMatchLevel.UNVERSIONED;
304
+ for (const version of versionsToUpdate) {
305
+ const existingHref = firstHrefInDivision.get(version);
306
+ if (existingHref === undefined || fallbackLevel < existingHref.matchLevel) {
307
+ firstHrefInDivision.set(version, { href, matchLevel: fallbackLevel });
308
+ }
309
+ }
310
+ }
311
+ export function pathsDifferByOneSegment(a, b, type) {
312
+ const aSegments = a.split('/').filter(Boolean);
313
+ const bSegments = b.split('/').filter(Boolean);
314
+ // For language switching, we need to be more strict - paths should be identical except for the language segment
315
+ // once normalized, content segments should be identical and only locale prefix may differ.
316
+ if (type === 'language') {
317
+ const aPath = parseLanguagePath(aSegments);
318
+ const bPath = parseLanguagePath(bSegments);
319
+ if (aPath.hasDocsBase !== bPath.hasDocsBase)
320
+ return false;
321
+ // For language switching, the content parts (including version segments) must be identical.
322
+ if (aPath.contentSegments.length !== bPath.contentSegments.length)
323
+ return false;
324
+ for (let i = 0; i < aPath.contentSegments.length; i++) {
325
+ if (aPath.contentSegments[i] !== bPath.contentSegments[i]) {
326
+ return false;
327
+ }
328
+ }
329
+ // The paths are language variants if locale presence/value differs.
330
+ if (!aPath.locale && !bPath.locale)
331
+ return false;
332
+ return aPath.locale !== bPath.locale;
333
+ }
334
+ const aStartsWithDocs = aSegments[0] && ACCEPTED_BASE_PATHS.includes(aSegments[0]);
335
+ const bStartsWithDocs = bSegments[0] && ACCEPTED_BASE_PATHS.includes(bSegments[0]);
336
+ // Only compare like-for-like paths (both with docs base prefix or both without)
337
+ if (aStartsWithDocs !== bStartsWithDocs)
338
+ return false;
339
+ const aContentSegments = aStartsWithDocs ? aSegments.slice(1) : aSegments;
340
+ const bContentSegments = bStartsWithDocs ? bSegments.slice(1) : bSegments;
341
+ // Existing behavior: same-length paths are version variants if exactly one non-terminal segment differs.
342
+ if (aContentSegments.length === bContentSegments.length &&
343
+ aContentSegments.length >= 2 &&
344
+ aContentSegments.at(-1) === bContentSegments.at(-1)) {
345
+ let mismatchFound = false;
346
+ for (let i = 0; i < aContentSegments.length - 1; i++) {
347
+ if (aContentSegments[i] !== bContentSegments[i]) {
348
+ if (mismatchFound)
349
+ return false;
350
+ mismatchFound = true;
351
+ }
352
+ }
353
+ return mismatchFound;
354
+ }
355
+ // New behavior: handle root-vs-prefixed version paths by allowing one leading segment difference.
356
+ const longerContentSegments = aContentSegments.length > bContentSegments.length ? aContentSegments : bContentSegments;
357
+ const shorterContentSegments = aContentSegments.length > bContentSegments.length ? bContentSegments : aContentSegments;
358
+ if (longerContentSegments.length !== shorterContentSegments.length + 1)
359
+ return false;
360
+ if (shorterContentSegments.length === 0)
361
+ return false;
362
+ for (let i = 0; i < shorterContentSegments.length; i++) {
363
+ if (longerContentSegments[i + 1] !== shorterContentSegments[i]) {
364
+ return false;
365
+ }
366
+ }
367
+ return true;
368
+ }
369
+ function checkActiveGroup(group, currentPath) {
370
+ if (isPage(group)) {
371
+ return isEqualIgnoringLeadingSlash(group.href, currentPath);
372
+ }
373
+ if ('pages' in group) {
374
+ if ('root' in group && typeof group.root === 'object') {
375
+ const isActivePage = checkActiveGroup(group.root, currentPath);
376
+ if (isActivePage) {
377
+ return true;
378
+ }
379
+ }
380
+ for (const page of group.pages) {
381
+ const isActivePage = checkActiveGroup(page, currentPath);
382
+ if (isActivePage) {
383
+ return true;
384
+ }
385
+ }
386
+ }
387
+ return false;
388
+ }
@@ -0,0 +1,52 @@
1
+ import { DivisionMatchLevel, updateFirstHrefInDivision, } from './updateFirstHrefInDivision.js';
2
+ const emptyDivisions = {
3
+ tabs: [],
4
+ anchors: [],
5
+ versions: [],
6
+ languages: [],
7
+ dropdowns: [],
8
+ products: [],
9
+ menu: [],
10
+ };
11
+ function visit(firstHrefInDivision, href, nearestDivisionValue) {
12
+ const page = { title: href, href };
13
+ updateFirstHrefInDivision({
14
+ type: 'version',
15
+ currentPath: '/docs',
16
+ entry: page,
17
+ parentGroups: [],
18
+ nearestDivisionValue,
19
+ isActiveGroup: false,
20
+ inActiveDivision: false,
21
+ currentDivisionValue: undefined,
22
+ allDivisionValues: ['v2', 'v1'],
23
+ firstHrefInDivision,
24
+ navigationDivisions: emptyDivisions,
25
+ isHiddenPage: false,
26
+ });
27
+ }
28
+ describe(updateFirstHrefInDivision, () => {
29
+ it('prefers a division-owned page over an earlier unversioned catch-all', () => {
30
+ const map = new Map();
31
+ visit(map, '/docs/getting-started', undefined);
32
+ visit(map, '/docs/v1/overview', 'v1');
33
+ expect(map.get('v1')).toEqual({
34
+ href: '/docs/v1/overview',
35
+ matchLevel: DivisionMatchLevel.NONE,
36
+ });
37
+ expect(map.get('v2')).toEqual({
38
+ href: '/docs/getting-started',
39
+ matchLevel: DivisionMatchLevel.UNVERSIONED,
40
+ });
41
+ });
42
+ it('keeps the first division-owned page regardless of visit order', () => {
43
+ const map = new Map();
44
+ visit(map, '/docs/v1/overview', 'v1');
45
+ visit(map, '/docs/getting-started', undefined);
46
+ visit(map, '/docs/v1/later-page', 'v1');
47
+ expect(map.get('v1')).toEqual({
48
+ href: '/docs/v1/overview',
49
+ matchLevel: DivisionMatchLevel.NONE,
50
+ });
51
+ });
52
+ });
@@ -0,0 +1 @@
1
+ export declare function isEqualIgnoringLeadingSlash(path1: string | undefined, path2: string | undefined): boolean;
@@ -0,0 +1,21 @@
1
+ import { optionallyRemoveLeadingSlash, optionallyRemoveTrailingSlash, } from '../fs/optionallySlash.js';
2
+ import { replaceSlashIndex } from '../slug/replaceSlashIndex.js';
3
+ function normalizeIndexPath(path) {
4
+ if (path === '')
5
+ return 'index';
6
+ if (path === 'index')
7
+ return path;
8
+ if (path.endsWith('/index')) {
9
+ const normalized = replaceSlashIndex(path);
10
+ return normalized === '' ? 'index' : normalized;
11
+ }
12
+ return path;
13
+ }
14
+ export function isEqualIgnoringLeadingSlash(path1, path2) {
15
+ if (path1 == null || path2 == null || typeof path1 !== 'string' || typeof path2 !== 'string') {
16
+ return false;
17
+ }
18
+ const first = optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(path1));
19
+ const second = optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(path2));
20
+ return normalizeIndexPath(first) === normalizeIndexPath(second);
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { isEqualIgnoringLeadingSlash } from './isEqualIgnoringLeadingSlash.js';
2
+ describe('isEqualIgnoringLeadingSlash', () => {
3
+ test('false when non-string inputs are passed', () => {
4
+ expect(isEqualIgnoringLeadingSlash({}, {})).toEqual(false);
5
+ expect(isEqualIgnoringLeadingSlash(undefined, '')).toEqual(false);
6
+ expect(isEqualIgnoringLeadingSlash(0, null)).toEqual(false);
7
+ });
8
+ test('true when strings match ignoring leading slash', () => {
9
+ expect(isEqualIgnoringLeadingSlash('', '/')).toEqual(true);
10
+ expect(isEqualIgnoringLeadingSlash('/path/to', 'path/to')).toEqual(true);
11
+ });
12
+ test('handles index paths', () => {
13
+ expect(isEqualIgnoringLeadingSlash('', 'index')).toEqual(true);
14
+ expect(isEqualIgnoringLeadingSlash('/', 'index')).toEqual(true);
15
+ expect(isEqualIgnoringLeadingSlash('path/to/index', 'path/to')).toEqual(true);
16
+ expect(isEqualIgnoringLeadingSlash('/path/to/index', 'path/to')).toEqual(true);
17
+ expect(isEqualIgnoringLeadingSlash('path/to/index', '/path/to')).toEqual(true);
18
+ expect(isEqualIgnoringLeadingSlash('/index', '')).toEqual(true);
19
+ expect(isEqualIgnoringLeadingSlash('/index', '/')).toEqual(true);
20
+ });
21
+ test('handles complex index paths', () => {
22
+ expect(isEqualIgnoringLeadingSlash('/api/v1/index', '/api/v1')).toEqual(true);
23
+ expect(isEqualIgnoringLeadingSlash('indexing/guide', 'ing/guide')).toEqual(false);
24
+ expect(isEqualIgnoringLeadingSlash('reindex/data', 'rein/data')).toEqual(false);
25
+ expect(isEqualIgnoringLeadingSlash('index/subpath/index', 'index/subpath')).toEqual(true);
26
+ expect(isEqualIgnoringLeadingSlash('index/subpath', 'index/subpath/index')).toEqual(true);
27
+ expect(isEqualIgnoringLeadingSlash('/', 'index')).toEqual(true);
28
+ expect(isEqualIgnoringLeadingSlash('/index/index', 'index')).toEqual(true);
29
+ });
30
+ test('normal paths work', () => {
31
+ expect(isEqualIgnoringLeadingSlash('path/to/resource', 'path/to/different')).toEqual(false);
32
+ expect(isEqualIgnoringLeadingSlash('/a/b/c', '/x/y/z')).toEqual(false);
33
+ expect(isEqualIgnoringLeadingSlash('Path/To', 'path/to')).toEqual(false);
34
+ expect(isEqualIgnoringLeadingSlash('INDEX', 'index')).toEqual(false);
35
+ expect(isEqualIgnoringLeadingSlash('path/to', 'path/to')).toEqual(true);
36
+ });
37
+ test('weird cases work', () => {
38
+ expect(isEqualIgnoringLeadingSlash('', 'index')).toEqual(true);
39
+ expect(isEqualIgnoringLeadingSlash('/index', '/index')).toEqual(true);
40
+ expect(isEqualIgnoringLeadingSlash('/index', 'index')).toEqual(true);
41
+ expect(isEqualIgnoringLeadingSlash('/index', '')).toEqual(true);
42
+ });
43
+ });