@mintlify/common 1.0.1252 → 1.0.1253

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,3 @@
1
+ import type { DecoratedPageOrGroupConfig } from '@mintlify/validation';
2
+ export declare function clientNavView(entries: DecoratedPageOrGroupConfig[]): unknown[];
3
+ export declare function paginationDescriptions(entries: DecoratedPageOrGroupConfig[], currentPath: string): [unknown, unknown];
@@ -0,0 +1,44 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
13
+ import { replaceSlashIndex } from '../slug/replaceSlashIndex.js';
14
+ import { isPage } from './isPage.js';
15
+ import { CLIENT_NAV_PAGE_FIELDS } from './trimScopedNavPages.js';
16
+ function clientPage(page) {
17
+ return Object.fromEntries(CLIENT_NAV_PAGE_FIELDS.filter((field) => Object.hasOwn(page, field)).map((field) => [
18
+ field,
19
+ page[field],
20
+ ]));
21
+ }
22
+ export function clientNavView(entries) {
23
+ return entries.map((entry) => {
24
+ if (isPage(entry))
25
+ return clientPage(entry);
26
+ const { root, pages } = entry, rest = __rest(entry, ["root", "pages"]);
27
+ return Object.assign(Object.assign(Object.assign({}, rest), (root ? { root: clientPage(root) } : {})), { pages: clientNavView(pages) });
28
+ });
29
+ }
30
+ function flattenPages(entries) {
31
+ return entries.flatMap((entry) => {
32
+ if (isPage(entry))
33
+ return [entry];
34
+ return [...(entry.root ? [entry.root] : []), ...flattenPages(entry.pages)];
35
+ });
36
+ }
37
+ export function paginationDescriptions(entries, currentPath) {
38
+ var _a, _b;
39
+ const pages = flattenPages(entries);
40
+ const index = pages.findIndex((page) => isEqualIgnoringLeadingSlash(replaceSlashIndex(page.href), currentPath));
41
+ if (index === -1)
42
+ return [undefined, undefined];
43
+ return [(_a = pages[index - 1]) === null || _a === void 0 ? void 0 : _a.description, (_b = pages[index + 1]) === null || _b === void 0 ? void 0 : _b.description];
44
+ }
@@ -1,3 +1,4 @@
1
+ import { clientNavView, paginationDescriptions } from './clientNavView.js';
1
2
  import { decoratedNavigationConfig } from './decoratedNavigationConfig.js';
2
3
  import { filterDivisions } from './filterDivisions.js';
3
4
  import { getAllPathsInDecoratedNav } from './getAllPathsInDecoratedNav.js';
@@ -99,7 +100,8 @@ function assertInvariantForEveryPath(nav, includeProductVersionMetadata = false)
99
100
  });
100
101
  const full = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage);
101
102
  const rescoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage);
102
- expect(rescoped.groupsOrPages, currentPath).toEqual(full.groupsOrPages);
103
+ expect(clientNavView(rescoped.groupsOrPages), currentPath).toEqual(clientNavView(full.groupsOrPages));
104
+ expect(paginationDescriptions(rescoped.groupsOrPages, currentPath), currentPath).toEqual(paginationDescriptions(full.groupsOrPages, currentPath));
103
105
  expect(divisionSummary(rescoped), currentPath).toEqual(divisionSummary(full));
104
106
  expect(mapHrefs(rescoped.firstHrefInVersion), currentPath).toEqual(mapHrefs(full.firstHrefInVersion));
105
107
  expect(mapHrefs(rescoped.firstHrefInLanguage), currentPath).toEqual(mapHrefs(full.firstHrefInLanguage));
@@ -1,4 +1,5 @@
1
1
  import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
2
+ import { clientNavView, paginationDescriptions } from './clientNavView.js';
2
3
  import { decoratedNavigationConfig } from './decoratedNavigationConfig.js';
3
4
  import { filterDivisions } from './filterDivisions.js';
4
5
  import { getAllPathsInDecoratedNav } from './getAllPathsInDecoratedNav.js';
@@ -56,7 +57,8 @@ function assertStateFlipSafety(nav) {
56
57
  const label = `${currentPath} -> version ${version.name}`;
57
58
  const flippedFull = filterWith(nav, currentPath, version.name, scoped.currentLanguage);
58
59
  const flippedScoped = filterWith(scoped.scopedNav, currentPath, version.name, scoped.currentLanguage);
59
- expect(flippedScoped.groupsOrPages, label).toEqual(flippedFull.groupsOrPages);
60
+ expect(clientNavView(flippedScoped.groupsOrPages), label).toEqual(clientNavView(flippedFull.groupsOrPages));
61
+ expect(paginationDescriptions(flippedScoped.groupsOrPages, currentPath), label).toEqual(paginationDescriptions(flippedFull.groupsOrPages, currentPath));
60
62
  expect(divisionSummary(flippedScoped), label).toEqual(divisionSummary(flippedFull));
61
63
  }
62
64
  // deliberately unguarded (no stays check): even a flip to a stubbed,
@@ -66,7 +68,8 @@ function assertStateFlipSafety(nav) {
66
68
  const label = `${currentPath} -> product ${product.name}`;
67
69
  const flippedFull = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage, product.name);
68
70
  const flippedScoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage, product.name);
69
- expect(flippedScoped.groupsOrPages, label).toEqual(flippedFull.groupsOrPages);
71
+ expect(clientNavView(flippedScoped.groupsOrPages), label).toEqual(clientNavView(flippedFull.groupsOrPages));
72
+ expect(paginationDescriptions(flippedScoped.groupsOrPages, currentPath), label).toEqual(paginationDescriptions(flippedFull.groupsOrPages, currentPath));
70
73
  expect(divisionSummary(flippedScoped), label).toEqual(divisionSummary(flippedFull));
71
74
  }
72
75
  for (const language of full.languages) {
@@ -77,7 +80,8 @@ function assertStateFlipSafety(nav) {
77
80
  const label = `${currentPath} -> language ${language.language}`;
78
81
  const flippedFull = filterWith(nav, currentPath, scoped.currentVersion, language.language);
79
82
  const flippedScoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, language.language);
80
- expect(flippedScoped.groupsOrPages, label).toEqual(flippedFull.groupsOrPages);
83
+ expect(clientNavView(flippedScoped.groupsOrPages), label).toEqual(clientNavView(flippedFull.groupsOrPages));
84
+ expect(paginationDescriptions(flippedScoped.groupsOrPages, currentPath), label).toEqual(paginationDescriptions(flippedFull.groupsOrPages, currentPath));
81
85
  expect(divisionSummary(flippedScoped), label).toEqual(divisionSummary(flippedFull));
82
86
  }
83
87
  }
@@ -18,6 +18,7 @@ import { filterDivisions } from './filterDivisions.js';
18
18
  import { getFirstPageFromNavigation } from './getFirstPageFromNavigation.js';
19
19
  import { getVersionOrLanguageFromPath } from './getVersionOrLanguageFromPath.js';
20
20
  import { isPageGroups } from './groupAccess.js';
21
+ import { trimScopedNavPages } from './trimScopedNavPages.js';
21
22
  import { pathsDifferByOneSegment } from './updateFirstHrefInDivision.js';
22
23
  const SCOPED_DIVISION_KEYS = ['versions', 'languages'];
23
24
  const DIVISION_TYPE_BY_KEY = {
@@ -621,7 +622,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
621
622
  cache: { dropdownCache: new Map() },
622
623
  isPreview,
623
624
  });
624
- const scopedNav = scopeNode(
625
+ const scopedNav = trimScopedNavPages(scopeNode(
625
626
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
626
627
  decoratedNav, {
627
628
  currentPath,
@@ -634,7 +635,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
634
635
  isPreview,
635
636
  skipVersionLanguageStubs: productAmbiguous,
636
637
  includeProductVersionMetadata,
637
- }
638
+ }), currentPath
638
639
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
639
640
  );
640
641
  return {
@@ -0,0 +1,4 @@
1
+ type NavNode = Record<string, unknown>;
2
+ export declare const CLIENT_NAV_PAGE_FIELDS: readonly ["href", "title", "hidden", "groups", "sidebarTitle", "url", "icon", "iconType", "tag", "api", "openapi", "asyncapi", "graphql", "deprecated", "hideApiMarker", "mode", "playground", "version"];
3
+ export declare function trimScopedNavPages(nav: NavNode, currentPath: string): NavNode;
4
+ export {};
@@ -0,0 +1,134 @@
1
+ import { normalizeComparablePath } from '../paths/isEqualIgnoringLeadingSlash.js';
2
+ import { replaceSlashIndex } from '../slug/replaceSlashIndex.js';
3
+ function pageFieldAllowlist(fields) {
4
+ return fields;
5
+ }
6
+ export const CLIENT_NAV_PAGE_FIELDS = pageFieldAllowlist([
7
+ 'href',
8
+ 'title',
9
+ 'hidden',
10
+ 'groups',
11
+ 'sidebarTitle',
12
+ 'url',
13
+ 'icon',
14
+ 'iconType',
15
+ 'tag',
16
+ 'api',
17
+ 'openapi',
18
+ 'asyncapi',
19
+ 'graphql',
20
+ 'deprecated',
21
+ 'hideApiMarker',
22
+ 'mode',
23
+ 'playground',
24
+ 'version',
25
+ ]);
26
+ const isNavNode = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ const isPageNode = (node) => Object.hasOwn(node, 'href') && Object.hasOwn(node, 'title');
28
+ const hrefMatches = (href, target) => typeof href === 'string' && normalizeComparablePath(replaceSlashIndex(href)) === target;
29
+ function collectPagesInOrder(node, out, inHidden = false) {
30
+ if (Array.isArray(node)) {
31
+ for (const entry of node)
32
+ collectPagesInOrder(entry, out, inHidden);
33
+ return;
34
+ }
35
+ if (!isNavNode(node))
36
+ return;
37
+ if (isPageNode(node)) {
38
+ out.push({
39
+ page: node,
40
+ visibleToEveryone: !inHidden && node.hidden !== true && node.groups === undefined,
41
+ });
42
+ return;
43
+ }
44
+ const hidden = inHidden || node.hidden === true;
45
+ if (isNavNode(node.root) && isPageNode(node.root))
46
+ collectPagesInOrder(node.root, out, hidden);
47
+ for (const [key, value] of Object.entries(node)) {
48
+ if (key === 'root' || key === 'global')
49
+ continue;
50
+ if (Array.isArray(value))
51
+ collectPagesInOrder(value, out, hidden);
52
+ }
53
+ }
54
+ function addPaginationNeighbors(nav, target, keep) {
55
+ const pages = [];
56
+ collectPagesInOrder(nav, pages);
57
+ pages.forEach(({ page }, index) => {
58
+ if (!hrefMatches(page.href, target))
59
+ return;
60
+ keepUntilVisible(pages, index, -1, keep);
61
+ keepUntilVisible(pages, index, 1, keep);
62
+ });
63
+ }
64
+ function keepUntilVisible(pages, from, step, keep) {
65
+ for (let i = from + step; i >= 0 && i < pages.length; i += step) {
66
+ const entry = pages[i];
67
+ if (!entry)
68
+ return;
69
+ keep.add(entry.page);
70
+ if (entry.visibleToEveryone)
71
+ return;
72
+ }
73
+ }
74
+ function addDirectPages(group, keep) {
75
+ if (!Array.isArray(group.pages))
76
+ return;
77
+ for (const entry of group.pages) {
78
+ if (isNavNode(entry) && isPageNode(entry))
79
+ keep.add(entry);
80
+ }
81
+ }
82
+ function addDirectoryChildren(node, target, keep) {
83
+ if (Array.isArray(node)) {
84
+ for (const entry of node)
85
+ addDirectoryChildren(entry, target, keep);
86
+ return;
87
+ }
88
+ if (!isNavNode(node) || isPageNode(node))
89
+ return;
90
+ if (isNavNode(node.root) && hrefMatches(node.root.href, target)) {
91
+ addDirectPages(node, keep);
92
+ if (Array.isArray(node.pages)) {
93
+ for (const entry of node.pages) {
94
+ if (isNavNode(entry) && !isPageNode(entry))
95
+ addDirectPages(entry, keep);
96
+ }
97
+ }
98
+ }
99
+ for (const [key, value] of Object.entries(node)) {
100
+ if (key !== 'global' && Array.isArray(value))
101
+ addDirectoryChildren(value, target, keep);
102
+ }
103
+ }
104
+ const CLIENT_FIELDS = new Set(CLIENT_NAV_PAGE_FIELDS);
105
+ function trimPage(page, keepDescription) {
106
+ const trimmed = {};
107
+ for (const key of Object.keys(page)) {
108
+ if (CLIENT_FIELDS.has(key) || (keepDescription && key === 'description')) {
109
+ trimmed[key] = page[key];
110
+ }
111
+ }
112
+ return trimmed;
113
+ }
114
+ function trimNode(node, keepDescription) {
115
+ if (Array.isArray(node))
116
+ return node.map((entry) => trimNode(entry, keepDescription));
117
+ if (!isNavNode(node))
118
+ return node;
119
+ if (isPageNode(node))
120
+ return trimPage(node, keepDescription.has(node));
121
+ const trimmed = {};
122
+ for (const [key, value] of Object.entries(node)) {
123
+ trimmed[key] = key === 'global' ? value : trimNode(value, keepDescription);
124
+ }
125
+ return trimmed;
126
+ }
127
+ export function trimScopedNavPages(nav, currentPath) {
128
+ const keepDescription = new Set();
129
+ const target = normalizeComparablePath(replaceSlashIndex(currentPath));
130
+ addPaginationNeighbors(nav, target, keepDescription);
131
+ addDirectoryChildren(nav, target, keepDescription);
132
+ const trimmed = trimNode(nav, keepDescription);
133
+ return isNavNode(trimmed) ? trimmed : nav;
134
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,194 @@
1
+ import { trimScopedNavPages } from './trimScopedNavPages.js';
2
+ const page = (slug, extra = {}) => (Object.assign({ title: slug, href: `/${slug}`, description: `${slug} description` }, extra));
3
+ function descriptions(nav) {
4
+ const out = {};
5
+ const walk = (node) => {
6
+ if (Array.isArray(node))
7
+ return node.forEach(walk);
8
+ if (typeof node !== 'object' || node === null)
9
+ return;
10
+ if ('href' in node && 'title' in node && typeof node.href === 'string') {
11
+ out[node.href] = 'description' in node ? node.description : undefined;
12
+ return;
13
+ }
14
+ Object.values(node).forEach(walk);
15
+ };
16
+ walk(nav);
17
+ return out;
18
+ }
19
+ describe('trimScopedNavPages', () => {
20
+ it('keeps only client fields on pages', () => {
21
+ const nav = {
22
+ pages: [
23
+ page('a', {
24
+ sidebarTitle: 'A',
25
+ openapi: 'get /a',
26
+ deprecated: true,
27
+ playground: 'simple',
28
+ version: 'v2',
29
+ mode: 'wide',
30
+ autogeneratedByOpenApi: true,
31
+ authors: [{ name: 'x' }],
32
+ keywords: ['k'],
33
+ customFrontmatter: 'x',
34
+ }),
35
+ ],
36
+ };
37
+ expect(trimScopedNavPages(nav, '/a')).toEqual({
38
+ pages: [
39
+ {
40
+ title: 'a',
41
+ href: '/a',
42
+ sidebarTitle: 'A',
43
+ openapi: 'get /a',
44
+ deprecated: true,
45
+ playground: 'simple',
46
+ version: 'v2',
47
+ mode: 'wide',
48
+ },
49
+ ],
50
+ });
51
+ });
52
+ it('keeps descriptions only on pagination neighbors', () => {
53
+ const nav = {
54
+ groups: [
55
+ { group: 'One', pages: [page('a'), page('b')] },
56
+ { group: 'Two', pages: [page('c'), page('d')] },
57
+ ],
58
+ };
59
+ expect(descriptions(trimScopedNavPages(nav, '/b'))).toEqual({
60
+ '/a': 'a description',
61
+ '/b': undefined,
62
+ '/c': 'c description',
63
+ '/d': undefined,
64
+ });
65
+ });
66
+ it('orders a group root before its pages', () => {
67
+ const nav = {
68
+ groups: [{ group: 'G', root: page('root'), pages: [page('a'), page('b')] }],
69
+ };
70
+ expect(descriptions(trimScopedNavPages(nav, '/a'))).toEqual({
71
+ '/root': 'root description',
72
+ '/a': undefined,
73
+ '/b': 'b description',
74
+ });
75
+ });
76
+ it('steps past neighbors some viewers cannot see', () => {
77
+ const nav = {
78
+ pages: [
79
+ page('a'),
80
+ page('hidden', { hidden: true }),
81
+ page('current'),
82
+ page('gated', { groups: ['admin'] }),
83
+ page('b'),
84
+ page('c'),
85
+ ],
86
+ };
87
+ expect(descriptions(trimScopedNavPages(nav, '/current'))).toEqual({
88
+ '/a': 'a description',
89
+ '/hidden': 'hidden description',
90
+ '/current': undefined,
91
+ '/gated': 'gated description',
92
+ '/b': 'b description',
93
+ '/c': undefined,
94
+ });
95
+ });
96
+ it('steps past pages under a hidden group or division', () => {
97
+ const nav = {
98
+ tabs: [
99
+ {
100
+ tab: 'Docs',
101
+ groups: [
102
+ { group: 'One', pages: [page('current')] },
103
+ { group: 'Drafts', hidden: true, root: page('draft-root'), pages: [page('draft')] },
104
+ { group: 'Two', pages: [page('next'), page('after')] },
105
+ ],
106
+ },
107
+ { tab: 'Hidden', hidden: true, pages: [page('hidden-tab')] },
108
+ { tab: 'Other', pages: [page('other')] },
109
+ ],
110
+ };
111
+ expect(descriptions(trimScopedNavPages(nav, '/current'))).toEqual({
112
+ '/current': undefined,
113
+ '/draft-root': 'draft-root description',
114
+ '/draft': 'draft description',
115
+ '/next': 'next description',
116
+ '/after': undefined,
117
+ '/hidden-tab': undefined,
118
+ '/other': undefined,
119
+ });
120
+ expect(descriptions(trimScopedNavPages(nav, '/after'))).toMatchObject({
121
+ '/hidden-tab': 'hidden-tab description',
122
+ '/other': 'other description',
123
+ });
124
+ });
125
+ it('keeps descriptions a directory root lists', () => {
126
+ const nav = {
127
+ groups: [
128
+ {
129
+ group: 'Dir',
130
+ directory: 'card',
131
+ root: page('dir'),
132
+ pages: [
133
+ page('child'),
134
+ {
135
+ group: 'Sub',
136
+ pages: [page('grandchild'), { group: 'Deep', pages: [page('deep')] }],
137
+ },
138
+ ],
139
+ },
140
+ { group: 'Other', pages: [page('x'), page('y')] },
141
+ ],
142
+ };
143
+ expect(descriptions(trimScopedNavPages(nav, '/dir'))).toEqual({
144
+ '/dir': undefined,
145
+ '/child': 'child description',
146
+ '/grandchild': 'grandchild description',
147
+ '/deep': undefined,
148
+ '/x': undefined,
149
+ '/y': undefined,
150
+ });
151
+ });
152
+ it('matches index hrefs like the client', () => {
153
+ const nav = { pages: [page('a'), page('guide/index'), page('b')] };
154
+ expect(descriptions(trimScopedNavPages(nav, '/guide'))).toEqual({
155
+ '/a': 'a description',
156
+ '/guide/index': undefined,
157
+ '/b': 'b description',
158
+ });
159
+ });
160
+ it('leaves division descriptions and global links alone', () => {
161
+ const nav = {
162
+ global: {
163
+ anchors: [{ anchor: 'Blog', href: 'https://example.com', description: 'kept' }],
164
+ },
165
+ dropdowns: [
166
+ {
167
+ dropdown: 'D',
168
+ description: 'dropdown description',
169
+ pages: [page('a')],
170
+ },
171
+ ],
172
+ };
173
+ expect(trimScopedNavPages(nav, '/a')).toEqual({
174
+ global: {
175
+ anchors: [{ anchor: 'Blog', href: 'https://example.com', description: 'kept' }],
176
+ },
177
+ dropdowns: [
178
+ {
179
+ dropdown: 'D',
180
+ description: 'dropdown description',
181
+ pages: [{ title: 'a', href: '/a' }],
182
+ },
183
+ ],
184
+ });
185
+ });
186
+ it('does not mutate the input', () => {
187
+ const nav = {
188
+ pages: [page('a', { keywords: ['k'] }), page('b'), page('c')],
189
+ };
190
+ const before = structuredClone(nav);
191
+ trimScopedNavPages(nav, '/a');
192
+ expect(nav).toEqual(before);
193
+ });
194
+ });
@@ -1 +1,2 @@
1
1
  export declare function isEqualIgnoringLeadingSlash(path1: string | undefined, path2: string | undefined): boolean;
2
+ export declare function normalizeComparablePath(path: string): string;
@@ -15,7 +15,8 @@ export function isEqualIgnoringLeadingSlash(path1, path2) {
15
15
  if (path1 == null || path2 == null || typeof path1 !== 'string' || typeof path2 !== 'string') {
16
16
  return false;
17
17
  }
18
- const first = optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(path1));
19
- const second = optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(path2));
20
- return normalizeIndexPath(first) === normalizeIndexPath(second);
18
+ return normalizeComparablePath(path1) === normalizeComparablePath(path2);
19
+ }
20
+ export function normalizeComparablePath(path) {
21
+ return normalizeIndexPath(optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(path)));
21
22
  }