@mintlify/common 1.0.1023 → 1.0.1024
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/navigation/decoratedNavigationConfig.d.ts +2 -0
- package/dist/navigation/decoratedNavigationConfig.js +204 -0
- package/dist/navigation/filterDivisions.d.ts +65 -0
- package/dist/navigation/filterDivisions.js +697 -0
- package/dist/navigation/filterDivisions.test.d.ts +1 -0
- package/dist/navigation/filterDivisions.test.js +670 -0
- package/dist/navigation/getScopedNavForPath.test.d.ts +1 -0
- package/dist/navigation/getScopedNavForPath.test.js +135 -0
- package/dist/navigation/getVersionOrLanguageFromPath.d.ts +12 -0
- package/dist/navigation/getVersionOrLanguageFromPath.js +71 -0
- package/dist/navigation/getVersionOrLanguageFromPath.test.d.ts +1 -0
- package/dist/navigation/getVersionOrLanguageFromPath.test.js +133 -0
- package/dist/navigation/index.d.ts +5 -0
- package/dist/navigation/index.js +5 -0
- package/dist/navigation/isCommonPage.d.ts +8 -0
- package/dist/navigation/isCommonPage.js +36 -0
- package/dist/navigation/isCommonPage.test.d.ts +1 -0
- package/dist/navigation/isCommonPage.test.js +56 -0
- package/dist/navigation/pathsDifferByOneSegment.test.d.ts +1 -0
- package/dist/navigation/pathsDifferByOneSegment.test.js +91 -0
- package/dist/navigation/scopeNavToPath.d.ts +18 -0
- package/dist/navigation/scopeNavToPath.js +455 -0
- package/dist/navigation/scopeNavToPath.test.d.ts +1 -0
- package/dist/navigation/scopeNavToPath.test.js +117 -0
- package/dist/navigation/scopeOffPathDivisions.test.d.ts +1 -0
- package/dist/navigation/scopeOffPathDivisions.test.js +852 -0
- package/dist/navigation/scopeProductNav.test.d.ts +1 -0
- package/dist/navigation/scopeProductNav.test.js +288 -0
- package/dist/navigation/updateFirstHrefInDivision.d.ts +29 -0
- package/dist/navigation/updateFirstHrefInDivision.js +383 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.d.ts +1 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.js +21 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.test.d.ts +1 -0
- package/dist/paths/isEqualIgnoringLeadingSlash.test.js +43 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { filterDivisions } from './filterDivisions.js';
|
|
2
|
+
import { getAllPathsInDecoratedNav } from './getAllPathsInDecoratedNav.js';
|
|
3
|
+
import { getScopedNavForPath } from './scopeNavToPath.js';
|
|
4
|
+
const multiTabNav = {
|
|
5
|
+
tabs: [
|
|
6
|
+
{
|
|
7
|
+
tab: 'Documentation',
|
|
8
|
+
groups: [
|
|
9
|
+
{
|
|
10
|
+
group: 'Getting started',
|
|
11
|
+
pages: [
|
|
12
|
+
{ title: 'Quickstart', href: '/quickstart' },
|
|
13
|
+
{ title: 'Installation', href: '/installation' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
tab: 'Guides',
|
|
20
|
+
groups: [
|
|
21
|
+
{
|
|
22
|
+
group: 'Guides',
|
|
23
|
+
pages: [
|
|
24
|
+
{ title: 'Auth', href: '/guides/auth' },
|
|
25
|
+
{ title: 'Deploy', href: '/guides/deploy' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
tab: 'Changelog',
|
|
32
|
+
pages: [{ title: 'Changelog', href: '/changelog' }],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
tab: 'Community',
|
|
36
|
+
href: 'https://community.example.com',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
const menuTabNav = {
|
|
41
|
+
tabs: [
|
|
42
|
+
{
|
|
43
|
+
tab: 'Database',
|
|
44
|
+
menu: [
|
|
45
|
+
{
|
|
46
|
+
item: 'Get started',
|
|
47
|
+
description: 'Start here',
|
|
48
|
+
pages: [
|
|
49
|
+
{ title: 'Intro', href: '/db/intro' },
|
|
50
|
+
{ title: 'Install', href: '/db/install' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
item: 'Reference',
|
|
55
|
+
groups: [
|
|
56
|
+
{
|
|
57
|
+
group: 'SQL',
|
|
58
|
+
pages: [
|
|
59
|
+
{ title: 'SELECT', href: '/db/sql/select' },
|
|
60
|
+
{ title: 'INSERT', href: '/db/sql/insert' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
tab: 'Cloud',
|
|
69
|
+
menu: [
|
|
70
|
+
{
|
|
71
|
+
item: 'Overview',
|
|
72
|
+
pages: [
|
|
73
|
+
{ title: 'Cloud intro', href: '/cloud/intro' },
|
|
74
|
+
{ title: 'Pricing', href: '/cloud/pricing' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
item: 'Status page',
|
|
79
|
+
href: 'https://status.example.com',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
const hiddenAndGatedTabNav = {
|
|
86
|
+
tabs: [
|
|
87
|
+
{
|
|
88
|
+
tab: 'Public',
|
|
89
|
+
pages: [
|
|
90
|
+
{ title: 'Home', href: '/home' },
|
|
91
|
+
{ title: 'About', href: '/about' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
tab: 'Drafts',
|
|
96
|
+
pages: [
|
|
97
|
+
{ title: 'Draft one', href: '/drafts/one', hidden: true },
|
|
98
|
+
{ title: 'Draft two', href: '/drafts/two', hidden: true },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
tab: 'Internal',
|
|
103
|
+
pages: [
|
|
104
|
+
{ title: 'Internal one', href: '/internal/one', groups: ['staff'] },
|
|
105
|
+
{ title: 'Internal two', href: '/internal/two', groups: ['staff'] },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
const hiddenGroupTabNav = {
|
|
111
|
+
tabs: [
|
|
112
|
+
{
|
|
113
|
+
tab: 'Public',
|
|
114
|
+
pages: [{ title: 'Home', href: '/home' }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
tab: 'Archive',
|
|
118
|
+
groups: [
|
|
119
|
+
{
|
|
120
|
+
group: 'Old docs',
|
|
121
|
+
hidden: true,
|
|
122
|
+
pages: [
|
|
123
|
+
{ title: 'Legacy one', href: '/archive/one' },
|
|
124
|
+
{ title: 'Legacy two', href: '/archive/two' },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
// dropdowns are stateful client-side (dropdownCache), so the dropdown entries
|
|
132
|
+
// themselves stay whole while versions/languages nested inside them still stub
|
|
133
|
+
const dropdownNav = {
|
|
134
|
+
dropdowns: [
|
|
135
|
+
{
|
|
136
|
+
dropdown: 'Platform',
|
|
137
|
+
versions: [
|
|
138
|
+
{
|
|
139
|
+
version: 'v2',
|
|
140
|
+
default: true,
|
|
141
|
+
pages: [
|
|
142
|
+
{ title: 'Platform home', href: '/platform/v2/home' },
|
|
143
|
+
{ title: 'Platform setup', href: '/platform/v2/setup' },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
version: 'v1',
|
|
148
|
+
pages: [{ title: 'Platform home', href: '/platform/v1/home' }],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
dropdown: 'SDKs',
|
|
154
|
+
groups: [
|
|
155
|
+
{
|
|
156
|
+
group: 'Libraries',
|
|
157
|
+
pages: [
|
|
158
|
+
{ title: 'Node SDK', href: '/sdks/node' },
|
|
159
|
+
{ title: 'Python SDK', href: '/sdks/python' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
dropdown: 'Support',
|
|
166
|
+
href: 'https://support.example.com',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
// groups: [] denies every viewer (checkNavAccess), unlike an absent groups key
|
|
171
|
+
const denyAllGroupsTabNav = {
|
|
172
|
+
tabs: [
|
|
173
|
+
{
|
|
174
|
+
tab: 'Public',
|
|
175
|
+
pages: [{ title: 'Home', href: '/home' }],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
tab: 'Locked',
|
|
179
|
+
pages: [
|
|
180
|
+
{ title: 'Locked one', href: '/locked/one', groups: [] },
|
|
181
|
+
{ title: 'Locked two', href: '/locked/two', groups: [] },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
tab: 'Mixed',
|
|
186
|
+
pages: [
|
|
187
|
+
{ title: 'Deny all', href: '/mixed/deny', groups: [] },
|
|
188
|
+
{ title: 'Staff only', href: '/mixed/staff', groups: ['staff'] },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
const anchorNav = {
|
|
194
|
+
anchors: [
|
|
195
|
+
{
|
|
196
|
+
anchor: 'Docs',
|
|
197
|
+
groups: [
|
|
198
|
+
{
|
|
199
|
+
group: 'Docs',
|
|
200
|
+
pages: [
|
|
201
|
+
{ title: 'Overview', href: '/docs/overview' },
|
|
202
|
+
{ title: 'Concepts', href: '/docs/concepts' },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
anchor: 'API',
|
|
209
|
+
pages: [
|
|
210
|
+
{ title: 'Auth API', href: '/api/auth' },
|
|
211
|
+
{ title: 'Users API', href: '/api/users' },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
anchor: 'Blog',
|
|
216
|
+
href: 'https://blog.example.com',
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
const versionedTabNav = {
|
|
221
|
+
tabs: [
|
|
222
|
+
{
|
|
223
|
+
tab: 'Docs',
|
|
224
|
+
pages: [{ title: 'Docs home', href: '/docs' }],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
tab: 'API',
|
|
228
|
+
versions: [
|
|
229
|
+
{
|
|
230
|
+
version: 'v2',
|
|
231
|
+
default: true,
|
|
232
|
+
pages: [{ title: 'API v2', href: '/api/v2' }],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
version: 'v1',
|
|
236
|
+
pages: [{ title: 'API v1', href: '/api/v1' }],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
// a sibling version whose pages are all hidden or gated: invisible in the
|
|
243
|
+
// version switcher for anon viewers, so its stub must stay invisible too even
|
|
244
|
+
// when a precomputed jump-map target exists for it
|
|
245
|
+
const hiddenVersionNav = {
|
|
246
|
+
versions: [
|
|
247
|
+
{
|
|
248
|
+
version: 'v2',
|
|
249
|
+
default: true,
|
|
250
|
+
pages: [
|
|
251
|
+
{ title: 'Home', href: '/v2/home' },
|
|
252
|
+
{ title: 'Guide', href: '/v2/guide' },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
version: 'v1',
|
|
257
|
+
pages: [
|
|
258
|
+
{ title: 'One', href: '/v1/one', hidden: true },
|
|
259
|
+
{ title: 'Two', href: '/v1/two', hidden: true },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
version: 'beta',
|
|
264
|
+
pages: [
|
|
265
|
+
{ title: 'Alpha', href: '/beta/alpha', groups: ['staff'] },
|
|
266
|
+
{ title: 'Omega', href: '/beta/omega', groups: ['staff'] },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
const languagesWithTabsNav = {
|
|
272
|
+
languages: [
|
|
273
|
+
{
|
|
274
|
+
language: 'en',
|
|
275
|
+
tabs: [
|
|
276
|
+
{
|
|
277
|
+
tab: 'Documentation',
|
|
278
|
+
pages: [
|
|
279
|
+
{ title: 'Quickstart', href: '/en/quickstart' },
|
|
280
|
+
{ title: 'Installation', href: '/en/installation' },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
tab: 'Guides',
|
|
285
|
+
pages: [{ title: 'Auth guide', href: '/en/guides/auth' }],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
language: 'es',
|
|
291
|
+
tabs: [
|
|
292
|
+
{
|
|
293
|
+
tab: 'Documentation',
|
|
294
|
+
pages: [
|
|
295
|
+
{ title: 'Quickstart', href: '/es/quickstart' },
|
|
296
|
+
{ title: 'Installation', href: '/es/installation' },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
tab: 'Guides',
|
|
301
|
+
pages: [{ title: 'Auth guide', href: '/es/guides/auth' }],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
const deepMenuTabNav = {
|
|
308
|
+
tabs: [
|
|
309
|
+
{
|
|
310
|
+
tab: 'Database',
|
|
311
|
+
menu: [
|
|
312
|
+
{
|
|
313
|
+
item: 'Get started',
|
|
314
|
+
description: 'Start here',
|
|
315
|
+
pages: [
|
|
316
|
+
{ title: 'Intro', href: '/db/intro' },
|
|
317
|
+
{ title: 'Install', href: '/db/install' },
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
item: 'Reference',
|
|
322
|
+
description: 'Look things up',
|
|
323
|
+
groups: [
|
|
324
|
+
{
|
|
325
|
+
group: 'SQL',
|
|
326
|
+
pages: [
|
|
327
|
+
{ title: 'SELECT', href: '/ref/sql/select' },
|
|
328
|
+
{
|
|
329
|
+
group: 'Functions',
|
|
330
|
+
pages: [
|
|
331
|
+
{ title: 'String', href: '/ref/functions/string' },
|
|
332
|
+
{ title: 'Array', href: '/ref/functions/array' },
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
group: 'System tables',
|
|
339
|
+
pages: [
|
|
340
|
+
{ title: 'Parts', href: '/ref/system/parts' },
|
|
341
|
+
{ title: 'Query log', href: '/ref/system/query-log' },
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
item: 'Concepts',
|
|
348
|
+
pages: [
|
|
349
|
+
{ title: 'OLAP', href: '/concepts/olap' },
|
|
350
|
+
{ title: 'Columnar', href: '/concepts/columnar' },
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
tab: 'Cloud',
|
|
357
|
+
pages: [{ title: 'Cloud intro', href: '/cloud/intro' }],
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
const menuWithVersionsTabNav = {
|
|
362
|
+
tabs: [
|
|
363
|
+
{
|
|
364
|
+
tab: 'API',
|
|
365
|
+
menu: [
|
|
366
|
+
{
|
|
367
|
+
item: 'Endpoints',
|
|
368
|
+
versions: [
|
|
369
|
+
{
|
|
370
|
+
version: 'v2',
|
|
371
|
+
default: true,
|
|
372
|
+
pages: [{ title: 'Users v2', href: '/api/v2/users' }],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
version: 'v1',
|
|
376
|
+
pages: [{ title: 'Users v1', href: '/api/v1/users' }],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
item: 'Webhooks',
|
|
382
|
+
pages: [
|
|
383
|
+
{ title: 'Events', href: '/api/webhooks/events' },
|
|
384
|
+
{ title: 'Retries', href: '/api/webhooks/retries' },
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
tab: 'Docs',
|
|
391
|
+
pages: [{ title: 'Docs home', href: '/docs' }],
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
};
|
|
395
|
+
const hiddenMenuTabNav = {
|
|
396
|
+
tabs: [
|
|
397
|
+
{
|
|
398
|
+
tab: 'Product',
|
|
399
|
+
menu: [
|
|
400
|
+
{
|
|
401
|
+
item: 'Public',
|
|
402
|
+
pages: [
|
|
403
|
+
{ title: 'Home', href: '/p/home' },
|
|
404
|
+
{ title: 'Features', href: '/p/features' },
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
item: 'Drafts',
|
|
409
|
+
hidden: true,
|
|
410
|
+
pages: [{ title: 'Draft', href: '/p/drafts/one' }],
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
item: 'Internal',
|
|
414
|
+
pages: [
|
|
415
|
+
{
|
|
416
|
+
title: 'Runbook',
|
|
417
|
+
href: '/p/internal/runbook',
|
|
418
|
+
groups: ['staff'],
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
tab: 'Blog',
|
|
426
|
+
pages: [{ title: 'Posts', href: '/blog' }],
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
function filterWith(decoratedNav, currentPath, currentVersion, currentLanguage, isPreview = false, userGroups = new Set()) {
|
|
431
|
+
return filterDivisions({
|
|
432
|
+
currentPath,
|
|
433
|
+
decoratedNav,
|
|
434
|
+
userGroups,
|
|
435
|
+
currentVersion,
|
|
436
|
+
currentLanguage,
|
|
437
|
+
shouldUseDivisionMatch: true,
|
|
438
|
+
cache: { dropdownCache: new Map() },
|
|
439
|
+
isPreview,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function chromeSummary(result) {
|
|
443
|
+
return {
|
|
444
|
+
tabs: result.tabs.map(({ name, href, isActive, menu }) => ({
|
|
445
|
+
name,
|
|
446
|
+
href,
|
|
447
|
+
isActive,
|
|
448
|
+
menu: menu === null || menu === void 0 ? void 0 : menu.map((item) => ({ name: item.name, href: item.href })),
|
|
449
|
+
})),
|
|
450
|
+
anchors: result.anchors.map(({ name, href, isActive }) => ({ name, href, isActive })),
|
|
451
|
+
versions: result.versions.map(({ name, href, isActive }) => ({ name, href, isActive })),
|
|
452
|
+
languages: result.languages.map(({ name, isActive }) => ({ name, isActive })),
|
|
453
|
+
dropdowns: result.dropdowns.map(({ name, href, isActive }) => ({ name, href, isActive })),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function mapHrefs(map) {
|
|
457
|
+
return new Map([...map.entries()].map(([key, value]) => [key, value.href]));
|
|
458
|
+
}
|
|
459
|
+
function assertChromeInvariantForEveryPath(nav, isPreview = false, userGroups = new Set()) {
|
|
460
|
+
const paths = getAllPathsInDecoratedNav(nav);
|
|
461
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
462
|
+
for (const path of paths) {
|
|
463
|
+
const currentPath = `/${path.replace(/^\//, '')}`;
|
|
464
|
+
const scoped = getScopedNavForPath({ decoratedNav: nav, currentPath, userGroups, isPreview });
|
|
465
|
+
const full = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage, isPreview, userGroups);
|
|
466
|
+
const rescoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage, isPreview, userGroups);
|
|
467
|
+
expect(rescoped.groupsOrPages, currentPath).toEqual(full.groupsOrPages);
|
|
468
|
+
expect(chromeSummary(rescoped), currentPath).toEqual(chromeSummary(full));
|
|
469
|
+
expect(mapHrefs(rescoped.firstHrefInVersion), currentPath).toEqual(mapHrefs(full.firstHrefInVersion));
|
|
470
|
+
expect(mapHrefs(rescoped.firstHrefInLanguage), currentPath).toEqual(mapHrefs(full.firstHrefInLanguage));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function tabByName(nav, name) {
|
|
474
|
+
if (!('tabs' in nav))
|
|
475
|
+
throw new Error('nav has no tabs');
|
|
476
|
+
const tab = nav.tabs.find((entry) => entry.tab === name);
|
|
477
|
+
if (!tab)
|
|
478
|
+
throw new Error(`tab ${name} not found`);
|
|
479
|
+
return tab;
|
|
480
|
+
}
|
|
481
|
+
describe('off-path tab and anchor stubbing', () => {
|
|
482
|
+
it('filters identically to the full nav for every path in a multi-tab nav', () => {
|
|
483
|
+
assertChromeInvariantForEveryPath(multiTabNav);
|
|
484
|
+
});
|
|
485
|
+
// home URLs reach the scoper as '/' or 'index' without matching any nav
|
|
486
|
+
// href; filterDivisions resolves them to the first page internally, so the
|
|
487
|
+
// scoper must still produce an equivalently-filtering tree
|
|
488
|
+
it('filters identically for home paths that match no nav href', () => {
|
|
489
|
+
for (const nav of [multiTabNav, menuTabNav, anchorNav, versionedTabNav]) {
|
|
490
|
+
for (const currentPath of ['/', '/index', '']) {
|
|
491
|
+
const scoped = getScopedNavForPath({ decoratedNav: nav, currentPath });
|
|
492
|
+
const full = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage);
|
|
493
|
+
const rescoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage);
|
|
494
|
+
expect(rescoped.groupsOrPages, `${currentPath}`).toEqual(full.groupsOrPages);
|
|
495
|
+
expect(chromeSummary(rescoped), `${currentPath}`).toEqual(chromeSummary(full));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
it('filters identically to the full nav for every path in a menu-tab nav', () => {
|
|
500
|
+
assertChromeInvariantForEveryPath(menuTabNav);
|
|
501
|
+
});
|
|
502
|
+
it('filters identically to the full nav for every path in an anchor nav', () => {
|
|
503
|
+
assertChromeInvariantForEveryPath(anchorNav);
|
|
504
|
+
});
|
|
505
|
+
it('filters identically to the full nav for every path in a languages-with-tabs nav', () => {
|
|
506
|
+
assertChromeInvariantForEveryPath(languagesWithTabsNav);
|
|
507
|
+
});
|
|
508
|
+
it('collapses off-path tabs to a single resolved page', () => {
|
|
509
|
+
const { scopedNav } = getScopedNavForPath({
|
|
510
|
+
decoratedNav: multiTabNav,
|
|
511
|
+
currentPath: '/quickstart',
|
|
512
|
+
});
|
|
513
|
+
const guides = tabByName(scopedNav, 'Guides');
|
|
514
|
+
expect(guides).toEqual({
|
|
515
|
+
tab: 'Guides',
|
|
516
|
+
pages: [{ title: 'Guides', href: '/guides/auth' }],
|
|
517
|
+
});
|
|
518
|
+
const documentation = tabByName(scopedNav, 'Documentation');
|
|
519
|
+
expect(documentation).toEqual(tabByName(multiTabNav, 'Documentation'));
|
|
520
|
+
const community = tabByName(scopedNav, 'Community');
|
|
521
|
+
expect(community).toEqual({ tab: 'Community', href: 'https://community.example.com' });
|
|
522
|
+
});
|
|
523
|
+
it('keeps menus on stubbed tabs with each item collapsed to its first page', () => {
|
|
524
|
+
const { scopedNav } = getScopedNavForPath({
|
|
525
|
+
decoratedNav: menuTabNav,
|
|
526
|
+
currentPath: '/db/intro',
|
|
527
|
+
});
|
|
528
|
+
const cloud = tabByName(scopedNav, 'Cloud');
|
|
529
|
+
expect(cloud).toEqual({
|
|
530
|
+
tab: 'Cloud',
|
|
531
|
+
menu: [
|
|
532
|
+
{
|
|
533
|
+
item: 'Overview',
|
|
534
|
+
pages: [{ title: 'Overview', href: '/cloud/intro' }],
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
item: 'Status page',
|
|
538
|
+
href: 'https://status.example.com',
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
pages: [{ title: 'Cloud', href: '/cloud/intro' }],
|
|
542
|
+
});
|
|
543
|
+
expect(tabByName(scopedNav, 'Database')).toEqual({
|
|
544
|
+
tab: 'Database',
|
|
545
|
+
menu: [
|
|
546
|
+
{
|
|
547
|
+
item: 'Get started',
|
|
548
|
+
description: 'Start here',
|
|
549
|
+
pages: [
|
|
550
|
+
{ title: 'Intro', href: '/db/intro' },
|
|
551
|
+
{ title: 'Install', href: '/db/install' },
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
item: 'Reference',
|
|
556
|
+
pages: [{ title: 'Reference', href: '/db/sql/select' }],
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
it('keeps hidden-only and gated-only tabs invisible to anonymous filtering', () => {
|
|
562
|
+
const { scopedNav } = getScopedNavForPath({
|
|
563
|
+
decoratedNav: hiddenAndGatedTabNav,
|
|
564
|
+
currentPath: '/home',
|
|
565
|
+
});
|
|
566
|
+
expect(tabByName(scopedNav, 'Drafts')).toEqual({
|
|
567
|
+
tab: 'Drafts',
|
|
568
|
+
pages: [{ title: 'Drafts', href: '/drafts/one', hidden: true }],
|
|
569
|
+
});
|
|
570
|
+
expect(tabByName(scopedNav, 'Internal')).toEqual({
|
|
571
|
+
tab: 'Internal',
|
|
572
|
+
pages: [{ title: 'Internal', href: '/internal/one', groups: ['staff'] }],
|
|
573
|
+
});
|
|
574
|
+
const full = filterWith(hiddenAndGatedTabNav, '/home', undefined, undefined);
|
|
575
|
+
const rescoped = filterWith(scopedNav, '/home', undefined, undefined);
|
|
576
|
+
expect(rescoped.tabs.map(({ name }) => name)).toEqual(full.tabs.map(({ name }) => name));
|
|
577
|
+
expect(full.tabs.map(({ name }) => name)).toEqual(['Public']);
|
|
578
|
+
});
|
|
579
|
+
it('filters identically for every path in navs with hidden or gated divisions', () => {
|
|
580
|
+
assertChromeInvariantForEveryPath(hiddenAndGatedTabNav);
|
|
581
|
+
assertChromeInvariantForEveryPath(hiddenGroupTabNav);
|
|
582
|
+
});
|
|
583
|
+
it('filters identically for a group user, with gated stub targets carrying their groups', () => {
|
|
584
|
+
const staff = new Set(['staff']);
|
|
585
|
+
assertChromeInvariantForEveryPath(hiddenAndGatedTabNav, false, staff);
|
|
586
|
+
const { scopedNav } = getScopedNavForPath({
|
|
587
|
+
decoratedNav: hiddenAndGatedTabNav,
|
|
588
|
+
currentPath: '/home',
|
|
589
|
+
userGroups: staff,
|
|
590
|
+
});
|
|
591
|
+
expect(tabByName(scopedNav, 'Internal')).toEqual({
|
|
592
|
+
tab: 'Internal',
|
|
593
|
+
pages: [{ title: 'Internal', href: '/internal/one', groups: ['staff'] }],
|
|
594
|
+
});
|
|
595
|
+
const full = filterWith(hiddenAndGatedTabNav, '/home', undefined, undefined, false, staff);
|
|
596
|
+
const rescoped = filterWith(scopedNav, '/home', undefined, undefined, false, staff);
|
|
597
|
+
expect(rescoped.tabs.map(({ name }) => name)).toEqual(full.tabs.map(({ name }) => name));
|
|
598
|
+
expect(full.tabs.map(({ name }) => name)).toEqual(['Public', 'Internal']);
|
|
599
|
+
});
|
|
600
|
+
it('keeps a tab whose pages sit under a hidden group invisible to anonymous filtering', () => {
|
|
601
|
+
const { scopedNav } = getScopedNavForPath({
|
|
602
|
+
decoratedNav: hiddenGroupTabNav,
|
|
603
|
+
currentPath: '/home',
|
|
604
|
+
});
|
|
605
|
+
expect(tabByName(scopedNav, 'Archive')).toEqual({
|
|
606
|
+
tab: 'Archive',
|
|
607
|
+
pages: [{ title: 'Archive', href: '/archive/one', hidden: true }],
|
|
608
|
+
});
|
|
609
|
+
const full = filterWith(hiddenGroupTabNav, '/home', undefined, undefined);
|
|
610
|
+
const rescoped = filterWith(scopedNav, '/home', undefined, undefined);
|
|
611
|
+
expect(full.tabs.map(({ name }) => name)).toEqual(['Public']);
|
|
612
|
+
expect(rescoped.tabs.map(({ name }) => name)).toEqual(full.tabs.map(({ name }) => name));
|
|
613
|
+
});
|
|
614
|
+
it('filters identically under preview mode, where hidden and gated content is visible', () => {
|
|
615
|
+
assertChromeInvariantForEveryPath(hiddenAndGatedTabNav, true);
|
|
616
|
+
assertChromeInvariantForEveryPath(hiddenGroupTabNav, true);
|
|
617
|
+
assertChromeInvariantForEveryPath(menuTabNav, true);
|
|
618
|
+
});
|
|
619
|
+
it('filters identically for every path in a dropdown nav, stubbing versions inside dropdowns', () => {
|
|
620
|
+
assertChromeInvariantForEveryPath(dropdownNav);
|
|
621
|
+
assertChromeInvariantForEveryPath(dropdownNav, true);
|
|
622
|
+
const { scopedNav } = getScopedNavForPath({
|
|
623
|
+
decoratedNav: dropdownNav,
|
|
624
|
+
currentPath: '/platform/v2/setup',
|
|
625
|
+
});
|
|
626
|
+
if (!('dropdowns' in scopedNav) || !('dropdowns' in dropdownNav))
|
|
627
|
+
throw new Error('nav has no dropdowns');
|
|
628
|
+
const platform = scopedNav.dropdowns[0];
|
|
629
|
+
const fullPlatform = dropdownNav.dropdowns[0];
|
|
630
|
+
if (!platform || !('versions' in platform))
|
|
631
|
+
throw new Error('platform dropdown has no versions');
|
|
632
|
+
if (!fullPlatform || !('versions' in fullPlatform))
|
|
633
|
+
throw new Error('full platform dropdown has no versions');
|
|
634
|
+
expect(platform.versions[0]).toEqual(fullPlatform.versions[0]);
|
|
635
|
+
expect(platform.versions[1]).toEqual({
|
|
636
|
+
version: 'v1',
|
|
637
|
+
pages: [{ title: 'v1', href: '/platform/v1/home' }],
|
|
638
|
+
});
|
|
639
|
+
expect(scopedNav.dropdowns[1]).toEqual(dropdownNav.dropdowns[1]);
|
|
640
|
+
expect(scopedNav.dropdowns[2]).toEqual(dropdownNav.dropdowns[2]);
|
|
641
|
+
});
|
|
642
|
+
it('keeps hidden/gated sibling versions invisible even with precomputed jump targets', () => {
|
|
643
|
+
assertChromeInvariantForEveryPath(hiddenVersionNav);
|
|
644
|
+
assertChromeInvariantForEveryPath(hiddenVersionNav, false, new Set(['staff']));
|
|
645
|
+
assertChromeInvariantForEveryPath(hiddenVersionNav, true);
|
|
646
|
+
});
|
|
647
|
+
it('keeps deny-all groups: [] divisions invisible, carrying groups onto stubs', () => {
|
|
648
|
+
assertChromeInvariantForEveryPath(denyAllGroupsTabNav);
|
|
649
|
+
assertChromeInvariantForEveryPath(denyAllGroupsTabNav, false, new Set(['staff']));
|
|
650
|
+
assertChromeInvariantForEveryPath(denyAllGroupsTabNav, true);
|
|
651
|
+
const { scopedNav } = getScopedNavForPath({
|
|
652
|
+
decoratedNav: denyAllGroupsTabNav,
|
|
653
|
+
currentPath: '/home',
|
|
654
|
+
});
|
|
655
|
+
expect(tabByName(scopedNav, 'Locked')).toEqual({
|
|
656
|
+
tab: 'Locked',
|
|
657
|
+
pages: [{ title: 'Locked', href: '/locked/one', groups: [] }],
|
|
658
|
+
});
|
|
659
|
+
// deny-all candidates must not win the "anonymously visible" pool over
|
|
660
|
+
// gated ones the viewer could actually hold
|
|
661
|
+
expect(tabByName(scopedNav, 'Mixed')).toEqual({
|
|
662
|
+
tab: 'Mixed',
|
|
663
|
+
pages: [{ title: 'Mixed', href: '/mixed/deny', groups: [] }],
|
|
664
|
+
});
|
|
665
|
+
const full = filterWith(denyAllGroupsTabNav, '/home', undefined, undefined);
|
|
666
|
+
const rescoped = filterWith(scopedNav, '/home', undefined, undefined);
|
|
667
|
+
expect(full.tabs.map(({ name }) => name)).toEqual(['Public']);
|
|
668
|
+
expect(rescoped.tabs.map(({ name }) => name)).toEqual(full.tabs.map(({ name }) => name));
|
|
669
|
+
});
|
|
670
|
+
it('collapses off-path anchors and keeps external ones untouched', () => {
|
|
671
|
+
const { scopedNav } = getScopedNavForPath({
|
|
672
|
+
decoratedNav: anchorNav,
|
|
673
|
+
currentPath: '/docs/overview',
|
|
674
|
+
});
|
|
675
|
+
if (!('anchors' in scopedNav))
|
|
676
|
+
throw new Error('nav has no anchors');
|
|
677
|
+
expect(scopedNav.anchors).toEqual([
|
|
678
|
+
...('anchors' in anchorNav ? [anchorNav.anchors[0]] : []),
|
|
679
|
+
{ anchor: 'API', pages: [{ title: 'API', href: '/api/auth' }] },
|
|
680
|
+
{ anchor: 'Blog', href: 'https://blog.example.com' },
|
|
681
|
+
]);
|
|
682
|
+
});
|
|
683
|
+
it('never stubs a tab with nested versions at the tab level', () => {
|
|
684
|
+
const { scopedNav } = getScopedNavForPath({
|
|
685
|
+
decoratedNav: versionedTabNav,
|
|
686
|
+
currentPath: '/docs',
|
|
687
|
+
});
|
|
688
|
+
expect(tabByName(scopedNav, 'API')).toEqual({
|
|
689
|
+
tab: 'API',
|
|
690
|
+
versions: [
|
|
691
|
+
{
|
|
692
|
+
version: 'v2',
|
|
693
|
+
default: true,
|
|
694
|
+
pages: [{ title: 'API v2', href: '/api/v2' }],
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
version: 'v1',
|
|
698
|
+
pages: [{ title: 'v1', href: '/api/v1' }],
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
it('stubs nothing when no tab contains the current path', () => {
|
|
704
|
+
const { scopedNav } = getScopedNavForPath({
|
|
705
|
+
decoratedNav: multiTabNav,
|
|
706
|
+
currentPath: '/not-in-any-tab',
|
|
707
|
+
});
|
|
708
|
+
expect(scopedNav).toEqual(multiTabNav);
|
|
709
|
+
});
|
|
710
|
+
it('filters identically to the full nav for every path in a versioned-tab nav', () => {
|
|
711
|
+
assertChromeInvariantForEveryPath(versionedTabNav);
|
|
712
|
+
});
|
|
713
|
+
it('stubs off-path tabs inside the active language and re-derives stably', () => {
|
|
714
|
+
const scoped = getScopedNavForPath({
|
|
715
|
+
decoratedNav: languagesWithTabsNav,
|
|
716
|
+
currentPath: '/en/quickstart',
|
|
717
|
+
});
|
|
718
|
+
if (!('languages' in scoped.scopedNav))
|
|
719
|
+
throw new Error('nav has no languages');
|
|
720
|
+
const en = scoped.scopedNav.languages.find((language) => language.language === 'en');
|
|
721
|
+
if (!en || !('tabs' in en))
|
|
722
|
+
throw new Error('en language has no tabs');
|
|
723
|
+
expect(en.tabs.find((tab) => tab.tab === 'Guides')).toEqual({
|
|
724
|
+
tab: 'Guides',
|
|
725
|
+
pages: [{ title: 'Guides', href: '/en/guides/auth' }],
|
|
726
|
+
});
|
|
727
|
+
const rescoped = getScopedNavForPath({
|
|
728
|
+
decoratedNav: scoped.scopedNav,
|
|
729
|
+
currentPath: '/en/quickstart',
|
|
730
|
+
});
|
|
731
|
+
expect(rescoped.scopedNav).toEqual(scoped.scopedNav);
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
describe('active-tab menu scoping', () => {
|
|
735
|
+
it('filters identically to the full nav for every path in a deep menu nav', () => {
|
|
736
|
+
assertChromeInvariantForEveryPath(deepMenuTabNav);
|
|
737
|
+
});
|
|
738
|
+
it('filters identically to the full nav for every path with versions inside a menu', () => {
|
|
739
|
+
assertChromeInvariantForEveryPath(menuWithVersionsTabNav);
|
|
740
|
+
});
|
|
741
|
+
it('filters identically with hidden and gated menu items for every viewer', () => {
|
|
742
|
+
assertChromeInvariantForEveryPath(hiddenMenuTabNav);
|
|
743
|
+
assertChromeInvariantForEveryPath(hiddenMenuTabNav, false, new Set(['staff']));
|
|
744
|
+
assertChromeInvariantForEveryPath(hiddenMenuTabNav, true);
|
|
745
|
+
});
|
|
746
|
+
it('stubs off-path menu items within the active tab, keeping their metadata', () => {
|
|
747
|
+
const { scopedNav } = getScopedNavForPath({
|
|
748
|
+
decoratedNav: deepMenuTabNav,
|
|
749
|
+
currentPath: '/db/intro',
|
|
750
|
+
});
|
|
751
|
+
const database = tabByName(scopedNav, 'Database');
|
|
752
|
+
if (!('menu' in database))
|
|
753
|
+
throw new Error('Database tab has no menu');
|
|
754
|
+
expect(database.menu).toEqual([
|
|
755
|
+
{
|
|
756
|
+
item: 'Get started',
|
|
757
|
+
description: 'Start here',
|
|
758
|
+
pages: [
|
|
759
|
+
{ title: 'Intro', href: '/db/intro' },
|
|
760
|
+
{ title: 'Install', href: '/db/install' },
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
item: 'Reference',
|
|
765
|
+
description: 'Look things up',
|
|
766
|
+
pages: [{ title: 'Reference', href: '/ref/sql/select' }],
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
item: 'Concepts',
|
|
770
|
+
pages: [{ title: 'Concepts', href: '/concepts/olap' }],
|
|
771
|
+
},
|
|
772
|
+
]);
|
|
773
|
+
});
|
|
774
|
+
it('expands the menu item containing a deep path and collapses its siblings', () => {
|
|
775
|
+
const { scopedNav } = getScopedNavForPath({
|
|
776
|
+
decoratedNav: deepMenuTabNav,
|
|
777
|
+
currentPath: '/ref/functions/string',
|
|
778
|
+
});
|
|
779
|
+
const database = tabByName(scopedNav, 'Database');
|
|
780
|
+
if (!('menu' in database))
|
|
781
|
+
throw new Error('Database tab has no menu');
|
|
782
|
+
const reference = database.menu.find((item) => item.item === 'Reference');
|
|
783
|
+
const original = tabByName(deepMenuTabNav, 'Database');
|
|
784
|
+
if (!('menu' in original))
|
|
785
|
+
throw new Error('Database tab has no menu');
|
|
786
|
+
expect(reference).toEqual(original.menu.find((item) => item.item === 'Reference'));
|
|
787
|
+
expect(database.menu.find((item) => item.item === 'Get started')).toEqual({
|
|
788
|
+
item: 'Get started',
|
|
789
|
+
description: 'Start here',
|
|
790
|
+
pages: [{ title: 'Get started', href: '/db/intro' }],
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
it('keeps menu items containing versions whole and version-stubs inside them', () => {
|
|
794
|
+
const { scopedNav } = getScopedNavForPath({
|
|
795
|
+
decoratedNav: menuWithVersionsTabNav,
|
|
796
|
+
currentPath: '/api/webhooks/events',
|
|
797
|
+
});
|
|
798
|
+
const api = tabByName(scopedNav, 'API');
|
|
799
|
+
if (!('menu' in api))
|
|
800
|
+
throw new Error('API tab has no menu');
|
|
801
|
+
expect(api.menu.find((item) => item.item === 'Webhooks')).toEqual({
|
|
802
|
+
item: 'Webhooks',
|
|
803
|
+
pages: [
|
|
804
|
+
{ title: 'Events', href: '/api/webhooks/events' },
|
|
805
|
+
{ title: 'Retries', href: '/api/webhooks/retries' },
|
|
806
|
+
],
|
|
807
|
+
});
|
|
808
|
+
expect(api.menu.find((item) => item.item === 'Endpoints')).toEqual({
|
|
809
|
+
item: 'Endpoints',
|
|
810
|
+
versions: [
|
|
811
|
+
{
|
|
812
|
+
version: 'v2',
|
|
813
|
+
default: true,
|
|
814
|
+
pages: [{ title: 'Users v2', href: '/api/v2/users' }],
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
version: 'v1',
|
|
818
|
+
pages: [{ title: 'v1', href: '/api/v1/users' }],
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
it('carries hidden and groups onto off-path menu item stubs', () => {
|
|
824
|
+
const { scopedNav } = getScopedNavForPath({
|
|
825
|
+
decoratedNav: hiddenMenuTabNav,
|
|
826
|
+
currentPath: '/p/home',
|
|
827
|
+
});
|
|
828
|
+
const product = tabByName(scopedNav, 'Product');
|
|
829
|
+
if (!('menu' in product))
|
|
830
|
+
throw new Error('Product tab has no menu');
|
|
831
|
+
expect(product.menu.find((item) => item.item === 'Drafts')).toEqual({
|
|
832
|
+
item: 'Drafts',
|
|
833
|
+
hidden: true,
|
|
834
|
+
pages: [{ title: 'Drafts', href: '/p/drafts/one' }],
|
|
835
|
+
});
|
|
836
|
+
expect(product.menu.find((item) => item.item === 'Internal')).toEqual({
|
|
837
|
+
item: 'Internal',
|
|
838
|
+
pages: [{ title: 'Internal', href: '/p/internal/runbook', groups: ['staff'] }],
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
it('re-derives stably from an already scoped menu nav', () => {
|
|
842
|
+
const scoped = getScopedNavForPath({
|
|
843
|
+
decoratedNav: deepMenuTabNav,
|
|
844
|
+
currentPath: '/db/intro',
|
|
845
|
+
});
|
|
846
|
+
const rescoped = getScopedNavForPath({
|
|
847
|
+
decoratedNav: scoped.scopedNav,
|
|
848
|
+
currentPath: '/db/intro',
|
|
849
|
+
});
|
|
850
|
+
expect(rescoped.scopedNav).toEqual(scoped.scopedNav);
|
|
851
|
+
});
|
|
852
|
+
});
|