@mintlify/common 1.0.1228 → 1.0.1230

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.
@@ -33,3 +33,4 @@ export * from './remarkUnwrapJsxHeadings.js';
33
33
  export { TITLE_COMPONENTS } from './remarkTitleMarkdown.js';
34
34
  export * from './remarkUserMentions.js';
35
35
  export * from './remarkVisibilityForMarkdown.js';
36
+ export * from './pureTemplateComponents.js';
@@ -33,3 +33,4 @@ export * from './remarkUnwrapJsxHeadings.js';
33
33
  export { TITLE_COMPONENTS } from './remarkTitleMarkdown.js';
34
34
  export * from './remarkUserMentions.js';
35
35
  export * from './remarkVisibilityForMarkdown.js';
36
+ export * from './pureTemplateComponents.js';
@@ -0,0 +1,7 @@
1
+ import type { Program } from 'estree-jsx';
2
+ import type { Root } from 'mdast';
3
+ export declare const TEMPLATE_BUILT_IN_COMPONENTS: ReadonlySet<string>;
4
+ type Statement = Program['body'][number];
5
+ export declare function getKeptEsmStatements(tree: Root): Set<Statement>;
6
+ export declare function extractIdentifierRef(estree: Program | undefined): string | null;
7
+ export {};
@@ -0,0 +1,406 @@
1
+ import { visit } from 'unist-util-visit';
2
+ import { isMdxJsEsm } from '../../utils.js';
3
+ export const TEMPLATE_BUILT_IN_COMPONENTS = new Set([
4
+ 'Accordion',
5
+ 'AccordionGroup',
6
+ 'Badge',
7
+ 'Callout',
8
+ 'Card',
9
+ 'CardGroup',
10
+ 'Check',
11
+ 'CodeBlock',
12
+ 'CodeGroup',
13
+ 'Color',
14
+ 'Column',
15
+ 'Columns',
16
+ 'Danger',
17
+ 'Expandable',
18
+ 'FileTree',
19
+ 'Frame',
20
+ 'GitHub',
21
+ 'Github',
22
+ 'Heading',
23
+ 'Icon',
24
+ 'Info',
25
+ 'Latex',
26
+ 'Link',
27
+ 'MDX',
28
+ 'Mermaid',
29
+ 'Note',
30
+ 'OptimizedVideo',
31
+ 'Panel',
32
+ 'ParamField',
33
+ 'Prompt',
34
+ 'RequestExample',
35
+ 'ResponseExample',
36
+ 'ResponseField',
37
+ 'SnippetGroup',
38
+ 'Step',
39
+ 'Steps',
40
+ 'Tab',
41
+ 'Tabs',
42
+ 'Tile',
43
+ 'Tip',
44
+ 'Tooltip',
45
+ 'Tree',
46
+ 'Update',
47
+ 'Variation',
48
+ 'View',
49
+ 'Visibility',
50
+ 'Warning',
51
+ 'ZoomImage',
52
+ ]);
53
+ const UNSAFE_TAGS = new Set([
54
+ 'base',
55
+ 'embed',
56
+ 'form',
57
+ 'frame',
58
+ 'frameset',
59
+ 'iframe',
60
+ 'link',
61
+ 'math',
62
+ 'meta',
63
+ 'noscript',
64
+ 'object',
65
+ 'portal',
66
+ 'script',
67
+ 'style',
68
+ 'svg',
69
+ 'template',
70
+ ]);
71
+ const UNSAFE_ATTRIBUTES = new Set([
72
+ 'action',
73
+ 'dangerouslysetinnerhtml',
74
+ 'formaction',
75
+ 'ref',
76
+ 'srcdoc',
77
+ 'style',
78
+ ]);
79
+ const MDX_CONTENT_LOCALS = new Set(['props']);
80
+ const VALUE_NAME = /^[a-z][\w$]*$/;
81
+ const COMPONENT_NAME = /^[A-Z]\w*$/;
82
+ const TAG_NAME = /^[a-z][a-z0-9]*$/;
83
+ const ATTRIBUTE_NAME = /^[a-zA-Z][a-zA-Z0-9-]*$/;
84
+ export function getKeptEsmStatements(tree) {
85
+ const components = new Map();
86
+ const kept = new Set();
87
+ const constantNames = new Set();
88
+ const esmBindings = new Set();
89
+ visit(tree, (node) => {
90
+ var _a;
91
+ if (!isMdxJsEsm(node) || !((_a = node.data) === null || _a === void 0 ? void 0 : _a.estree))
92
+ return;
93
+ const program = node.data.estree;
94
+ collectTopLevelBindings(program, esmBindings);
95
+ for (const statement of program.body) {
96
+ const constantName = getLiteralConstantName(statement);
97
+ if (constantName) {
98
+ kept.add(statement);
99
+ constantNames.add(constantName);
100
+ continue;
101
+ }
102
+ const componentName = getPureTemplateComponentName(statement);
103
+ if (componentName)
104
+ components.set(statement, componentName);
105
+ }
106
+ });
107
+ if (components.size === 0)
108
+ return kept;
109
+ const { rejected, attributeStubNames } = scanContent(tree, new Set(components.values()), constantNames);
110
+ const shadowing = new Set([...esmBindings, ...attributeStubNames]);
111
+ for (const [statement, name] of components) {
112
+ if (rejected.has(name) || !usesOnlyUnshadowedBuiltIns(statement, shadowing))
113
+ continue;
114
+ kept.add(statement);
115
+ }
116
+ return kept;
117
+ }
118
+ function getLiteralConstantName(statement) {
119
+ var _a;
120
+ if (statement.type !== 'ExportNamedDeclaration')
121
+ return null;
122
+ const declaration = statement.declaration;
123
+ if ((declaration === null || declaration === void 0 ? void 0 : declaration.type) !== 'VariableDeclaration' || declaration.kind !== 'const')
124
+ return null;
125
+ const declarator = declaration.declarations[0];
126
+ if (declaration.declarations.length !== 1 || (declarator === null || declarator === void 0 ? void 0 : declarator.id.type) !== 'Identifier')
127
+ return null;
128
+ const name = declarator.id.name;
129
+ if (!VALUE_NAME.test(name) || MDX_CONTENT_LOCALS.has(name))
130
+ return null;
131
+ if (((_a = declarator.init) === null || _a === void 0 ? void 0 : _a.type) !== 'Literal')
132
+ return null;
133
+ const value = declarator.init.value;
134
+ const isPrimitive = typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
135
+ return isPrimitive ? name : null;
136
+ }
137
+ function getPureTemplateComponentName(statement) {
138
+ var _a, _b;
139
+ if (statement.type !== 'ExportNamedDeclaration' || !statement.declaration)
140
+ return null;
141
+ const declaration = statement.declaration;
142
+ let name;
143
+ let fn;
144
+ if (declaration.type === 'FunctionDeclaration') {
145
+ name = (_a = declaration.id) === null || _a === void 0 ? void 0 : _a.name;
146
+ fn = declaration;
147
+ }
148
+ else if (declaration.type === 'VariableDeclaration' &&
149
+ declaration.kind === 'const' &&
150
+ declaration.declarations.length === 1) {
151
+ const declarator = declaration.declarations[0];
152
+ if ((declarator === null || declarator === void 0 ? void 0 : declarator.id.type) !== 'Identifier')
153
+ return null;
154
+ if (((_b = declarator.init) === null || _b === void 0 ? void 0 : _b.type) !== 'ArrowFunctionExpression')
155
+ return null;
156
+ name = declarator.id.name;
157
+ fn = declarator.init;
158
+ }
159
+ else {
160
+ return null;
161
+ }
162
+ if (!name || !COMPONENT_NAME.test(name) || TEMPLATE_BUILT_IN_COMPONENTS.has(name))
163
+ return null;
164
+ if (fn.async || fn.generator)
165
+ return null;
166
+ const params = getParamNames(fn.params);
167
+ if (!params)
168
+ return null;
169
+ const root = getReturnedExpression(fn);
170
+ if ((root === null || root === void 0 ? void 0 : root.type) !== 'JSXElement' && (root === null || root === void 0 ? void 0 : root.type) !== 'JSXFragment')
171
+ return null;
172
+ return isPureElement(root, params) ? name : null;
173
+ }
174
+ function getParamNames(params) {
175
+ const names = new Set();
176
+ if (params.length === 0)
177
+ return names;
178
+ const param = params[0];
179
+ if (params.length > 1 || (param === null || param === void 0 ? void 0 : param.type) !== 'ObjectPattern')
180
+ return null;
181
+ for (const property of param.properties) {
182
+ if (property.type !== 'Property' || property.computed || !property.shorthand)
183
+ return null;
184
+ if (property.key.type !== 'Identifier' || property.value.type !== 'Identifier')
185
+ return null;
186
+ const name = property.key.name;
187
+ if (property.value.name !== name || !VALUE_NAME.test(name) || name in Object.prototype) {
188
+ return null;
189
+ }
190
+ names.add(name);
191
+ }
192
+ return names;
193
+ }
194
+ function getReturnedExpression(fn) {
195
+ var _a;
196
+ if (fn.body.type !== 'BlockStatement')
197
+ return fn.body;
198
+ const statement = fn.body.body[0];
199
+ if (fn.body.body.length !== 1 || (statement === null || statement === void 0 ? void 0 : statement.type) !== 'ReturnStatement')
200
+ return null;
201
+ return (_a = statement.argument) !== null && _a !== void 0 ? _a : null;
202
+ }
203
+ function isPureElement(node, params) {
204
+ if (node.type === 'JSXElement') {
205
+ const { name, attributes } = node.openingElement;
206
+ if (name.type !== 'JSXIdentifier' || !isSafeElementName(name.name))
207
+ return false;
208
+ if (!attributes.every((attribute) => isPureAttribute(attribute, params)))
209
+ return false;
210
+ }
211
+ return node.children.every((child) => isPureChild(child, params));
212
+ }
213
+ function isSafeElementName(name) {
214
+ if (TEMPLATE_BUILT_IN_COMPONENTS.has(name))
215
+ return true;
216
+ return TAG_NAME.test(name) && !UNSAFE_TAGS.has(name);
217
+ }
218
+ function isPureAttribute(attribute, params) {
219
+ if (attribute.type !== 'JSXAttribute' || attribute.name.type !== 'JSXIdentifier')
220
+ return false;
221
+ const name = attribute.name.name;
222
+ if (!ATTRIBUTE_NAME.test(name) ||
223
+ /^on/i.test(name) ||
224
+ UNSAFE_ATTRIBUTES.has(name.toLowerCase())) {
225
+ return false;
226
+ }
227
+ const value = attribute.value;
228
+ if (value === null)
229
+ return true;
230
+ if (value.type === 'Literal')
231
+ return typeof value.value === 'string';
232
+ if (value.type !== 'JSXExpressionContainer')
233
+ return false;
234
+ if (value.expression.type === 'JSXEmptyExpression')
235
+ return false;
236
+ return isPureValue(value.expression, params);
237
+ }
238
+ function isPureChild(child, params) {
239
+ switch (child.type) {
240
+ case 'JSXText':
241
+ return true;
242
+ case 'JSXElement':
243
+ case 'JSXFragment':
244
+ return isPureElement(child, params);
245
+ case 'JSXExpressionContainer':
246
+ return (child.expression.type === 'JSXEmptyExpression' || isPureValue(child.expression, params));
247
+ default:
248
+ return false;
249
+ }
250
+ }
251
+ function isPureValue(node, params) {
252
+ switch (node.type) {
253
+ case 'Literal':
254
+ return typeof node.value === 'string' || typeof node.value === 'number';
255
+ case 'Identifier':
256
+ return params.has(node.name);
257
+ case 'TemplateLiteral':
258
+ return node.expressions.every((expression) => expression.type === 'Identifier' && params.has(expression.name));
259
+ default:
260
+ return false;
261
+ }
262
+ }
263
+ function scanContent(tree, candidates, constantNames) {
264
+ const rejected = new Set();
265
+ const attributeStubNames = new Set();
266
+ visit(tree, (node) => {
267
+ var _a, _b, _c;
268
+ if (isMdxJsEsm(node))
269
+ return;
270
+ if ((node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
271
+ 'name' in node &&
272
+ typeof node.name === 'string') {
273
+ const memberRoot = node.name.split('.')[0];
274
+ if (memberRoot && memberRoot !== node.name && candidates.has(memberRoot)) {
275
+ rejected.add(memberRoot);
276
+ }
277
+ if (candidates.has(node.name) && 'attributes' in node) {
278
+ const hasOnlyLiteralAttributes = node.attributes.every((attribute) => {
279
+ var _a;
280
+ return attribute.type === 'mdxJsxAttribute' &&
281
+ (attribute.value == null ||
282
+ typeof attribute.value === 'string' ||
283
+ isConstantReference((_a = attribute.value.data) === null || _a === void 0 ? void 0 : _a.estree, constantNames));
284
+ });
285
+ if (!hasOnlyLiteralAttributes)
286
+ rejected.add(node.name);
287
+ }
288
+ }
289
+ const referenced = new Set();
290
+ if ('data' in node && isRecord(node.data) && 'estree' in node.data) {
291
+ collectIdentifierNames(node.data.estree, referenced);
292
+ }
293
+ if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
294
+ for (const attribute of node.attributes) {
295
+ collectIdentifierNames(attribute, referenced);
296
+ if (attribute.type !== 'mdxJsxAttribute' || typeof attribute.value !== 'object')
297
+ continue;
298
+ const stubName = extractIdentifierRef((_c = (_b = (_a = attribute.value) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.estree) !== null && _c !== void 0 ? _c : undefined);
299
+ if (stubName)
300
+ attributeStubNames.add(stubName);
301
+ }
302
+ }
303
+ for (const name of referenced) {
304
+ if (candidates.has(name))
305
+ rejected.add(name);
306
+ }
307
+ });
308
+ return { rejected, attributeStubNames };
309
+ }
310
+ function isConstantReference(program, constantNames) {
311
+ const statement = program === null || program === void 0 ? void 0 : program.body[0];
312
+ if ((program === null || program === void 0 ? void 0 : program.body.length) !== 1 || (statement === null || statement === void 0 ? void 0 : statement.type) !== 'ExpressionStatement')
313
+ return false;
314
+ return statement.expression.type === 'Identifier' && constantNames.has(statement.expression.name);
315
+ }
316
+ // Returns the identifier name if the estree expression is a bare reference that will be kept
317
+ // by isStringSafe but needs a stub to avoid a ReferenceError at render time:
318
+ // {foo} → Identifier
319
+ // {!!foo} → UnaryExpression(! UnaryExpression(! Identifier))
320
+ // Parenthesised forms are transparent in ESTree so (foo) produces the same AST as foo.
321
+ export function extractIdentifierRef(estree) {
322
+ const stmt = estree === null || estree === void 0 ? void 0 : estree.body[0];
323
+ if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement')
324
+ return null;
325
+ const expr = stmt.expression;
326
+ if (expr.type === 'Identifier')
327
+ return expr.name;
328
+ if (expr.type === 'UnaryExpression' &&
329
+ expr.operator === '!' &&
330
+ expr.argument.type === 'UnaryExpression' &&
331
+ expr.argument.operator === '!' &&
332
+ expr.argument.argument.type === 'Identifier')
333
+ return expr.argument.argument.name;
334
+ return null;
335
+ }
336
+ function usesOnlyUnshadowedBuiltIns(statement, shadowing) {
337
+ const elementNames = new Set();
338
+ collectJsxElementNames(statement, elementNames);
339
+ for (const name of elementNames) {
340
+ if (/^[A-Z]/.test(name) && shadowing.has(name))
341
+ return false;
342
+ }
343
+ return true;
344
+ }
345
+ function collectTopLevelBindings(program, out) {
346
+ for (const statement of program.body) {
347
+ if (statement.type === 'ImportDeclaration') {
348
+ for (const specifier of statement.specifiers)
349
+ out.add(specifier.local.name);
350
+ continue;
351
+ }
352
+ if ((statement.type !== 'ExportNamedDeclaration' &&
353
+ statement.type !== 'ExportDefaultDeclaration') ||
354
+ !statement.declaration) {
355
+ collectIdentifierNames(statement, out, ['Identifier']);
356
+ continue;
357
+ }
358
+ const declaration = statement.declaration;
359
+ if ((declaration === null || declaration === void 0 ? void 0 : declaration.type) === 'VariableDeclaration') {
360
+ for (const declarator of declaration.declarations) {
361
+ collectIdentifierNames(declarator.id, out, ['Identifier']);
362
+ }
363
+ }
364
+ else if (((declaration === null || declaration === void 0 ? void 0 : declaration.type) === 'FunctionDeclaration' || (declaration === null || declaration === void 0 ? void 0 : declaration.type) === 'ClassDeclaration') &&
365
+ declaration.id) {
366
+ out.add(declaration.id.name);
367
+ }
368
+ }
369
+ }
370
+ function collectIdentifierNames(value, out, types = ['Identifier', 'JSXIdentifier']) {
371
+ walkEstree(value, (node) => {
372
+ if (typeof node.type === 'string' &&
373
+ types.includes(node.type) &&
374
+ typeof node.name === 'string') {
375
+ out.add(node.name);
376
+ }
377
+ });
378
+ }
379
+ function collectJsxElementNames(value, out) {
380
+ walkEstree(value, (node) => {
381
+ if (node.type !== 'JSXOpeningElement' || !('name' in node) || !isRecord(node.name))
382
+ return;
383
+ if (node.name.type === 'JSXIdentifier' && typeof node.name.name === 'string') {
384
+ out.add(node.name.name);
385
+ }
386
+ });
387
+ }
388
+ function walkEstree(value, onNode) {
389
+ if (Array.isArray(value)) {
390
+ for (const item of value)
391
+ walkEstree(item, onNode);
392
+ return;
393
+ }
394
+ if (!isRecord(value))
395
+ return;
396
+ if (typeof value.type === 'string')
397
+ onNode(value);
398
+ for (const [key, child] of Object.entries(value)) {
399
+ if (key === 'loc' || key === 'range' || key === 'position')
400
+ continue;
401
+ walkEstree(child, onNode);
402
+ }
403
+ }
404
+ function isRecord(value) {
405
+ return typeof value === 'object' && value !== null;
406
+ }
@@ -1,7 +1,9 @@
1
+ import { jsx as jsxHandlers, toJs } from 'estree-util-to-js';
1
2
  import { remove } from 'unist-util-remove';
2
3
  import { CONTINUE, visit } from 'unist-util-visit';
3
4
  import { UNSAFE_OBJECT_KEYS } from '../../static-object-keys.js';
4
5
  import { isMdxJsEsm } from '../../utils.js';
6
+ import { extractIdentifierRef, getKeptEsmStatements } from './pureTemplateComponents.js';
5
7
  const objectEstree = {
6
8
  type: 'Program',
7
9
  body: [
@@ -402,26 +404,6 @@ function collectIdentifiersFromPattern(pattern, out) {
402
404
  collectIdentifiersFromPattern(pattern.argument, out);
403
405
  }
404
406
  }
405
- // Returns the identifier name if the estree expression is a bare reference that will be kept
406
- // by isStringSafe but needs a stub to avoid a ReferenceError at render time:
407
- // {foo} → Identifier
408
- // {!!foo} → UnaryExpression(! UnaryExpression(! Identifier))
409
- // Parenthesised forms are transparent in ESTree so (foo) produces the same AST as foo.
410
- function extractIdentifierRef(estree) {
411
- const stmt = estree === null || estree === void 0 ? void 0 : estree.body[0];
412
- if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement')
413
- return null;
414
- const expr = stmt.expression;
415
- if (expr.type === 'Identifier')
416
- return expr.name;
417
- if (expr.type === 'UnaryExpression' &&
418
- expr.operator === '!' &&
419
- expr.argument.type === 'UnaryExpression' &&
420
- expr.argument.operator === '!' &&
421
- expr.argument.argument.type === 'Identifier')
422
- return expr.argument.argument.name;
423
- return null;
424
- }
425
407
  function collectExportedNames(estree) {
426
408
  var _a, _b;
427
409
  const names = [];
@@ -690,19 +672,43 @@ function sanitizer(edit) {
690
672
  });
691
673
  }
692
674
  edit.remove(tree, (node) => isMdxExpression(node) && !isInertExpression(node));
675
+ const keptStatements = getKeptEsmStatements(tree);
676
+ const keptNames = new Set(collectExportedNames({
677
+ type: 'Program',
678
+ body: [...keptStatements],
679
+ sourceType: 'module',
680
+ }).map(({ name }) => name));
693
681
  // Collect exported names before stripping ESM so we can inject stubs.
694
682
  // Map keyed by name deduplicates naturally.
695
683
  const stubbedNamesMap = new Map();
696
- visit(tree, (node) => {
684
+ const partiallyKept = [];
685
+ visit(tree, (node, index, parent) => {
697
686
  var _a;
698
- if (isMdxJsEsm(node) && ((_a = node.data) === null || _a === void 0 ? void 0 : _a.estree)) {
699
- for (const entry of collectExportedNames(node.data.estree)) {
700
- if (!stubbedNamesMap.has(entry.name))
701
- stubbedNamesMap.set(entry.name, entry);
702
- }
687
+ if (!isMdxJsEsm(node) || !((_a = node.data) === null || _a === void 0 ? void 0 : _a.estree))
688
+ return;
689
+ const program = node.data.estree;
690
+ const stripped = program.body.filter((statement) => !keptStatements.has(statement));
691
+ for (const entry of collectExportedNames(Object.assign(Object.assign({}, program), { body: stripped }))) {
692
+ if (!stubbedNamesMap.has(entry.name))
693
+ stubbedNamesMap.set(entry.name, entry);
694
+ }
695
+ if (stripped.length > 0 && stripped.length < program.body.length && parent && index != null) {
696
+ const body = program.body.filter((statement) => keptStatements.has(statement));
697
+ partiallyKept.push({ parent, index, program: Object.assign(Object.assign({}, program), { body, comments: [] }) });
703
698
  }
704
699
  });
705
- edit.remove(tree, (node) => isMdxJsEsm(node));
700
+ for (const { parent, index, program } of partiallyKept) {
701
+ edit.replaceChild(parent, index, {
702
+ type: 'mdxjsEsm',
703
+ value: toJs(program, { handlers: jsxHandlers }).value,
704
+ data: { estree: program },
705
+ });
706
+ }
707
+ edit.remove(tree, (node) => {
708
+ var _a, _b;
709
+ return isMdxJsEsm(node) &&
710
+ !((_b = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree) === null || _b === void 0 ? void 0 : _b.body.some((statement) => keptStatements.has(statement)));
711
+ });
706
712
  // Sanitize JSX attributes. Any bare identifier that survives is added to stubbedNamesMap
707
713
  // so it gets a stub even if its import was already stripped by a prior plugin.
708
714
  visit(tree, (node) => {
@@ -717,7 +723,7 @@ function sanitizer(edit) {
717
723
  if (attr.type === 'mdxJsxAttribute' && attr.value instanceof Object) {
718
724
  if (isStringSafe(attr.value.value)) {
719
725
  const ident = extractIdentifierRef((_a = attr.value.data) === null || _a === void 0 ? void 0 : _a.estree);
720
- if (ident && !stubbedNamesMap.has(ident))
726
+ if (ident && !stubbedNamesMap.has(ident) && !keptNames.has(ident))
721
727
  stubbedNamesMap.set(ident, { name: ident, isFunction: false });
722
728
  return attr;
723
729
  }
@@ -270,6 +270,29 @@ describe(filterDivisions, () => {
270
270
  });
271
271
  });
272
272
  });
273
+ describe('filterDivisions with regional languages', () => {
274
+ it('matches the same page in a regional language whose prefix is lower case', () => {
275
+ const page = (href) => ({ title: href, href });
276
+ const result = filterDivisions({
277
+ currentPath: '/quickstart',
278
+ userGroups: new Set(),
279
+ currentVersion: undefined,
280
+ currentLanguage: 'en',
281
+ shouldUseDivisionMatch: false,
282
+ cache: { dropdownCache: new Map() },
283
+ decoratedNav: {
284
+ languages: [
285
+ { language: 'en', default: true, pages: [page('/index'), page('/quickstart')] },
286
+ { language: 'en-GB', pages: [page('/en-gb/index'), page('/en-gb/quickstart')] },
287
+ ],
288
+ },
289
+ });
290
+ expect(result.firstHrefInLanguage.get('en-GB')).toEqual({
291
+ href: '/en-gb/quickstart',
292
+ matchLevel: DivisionMatchLevel.PATH,
293
+ });
294
+ });
295
+ });
273
296
  describe('filterDivisions with partial auth', () => {
274
297
  const defaultArgs = {
275
298
  decoratedNav: { groups: [] },
@@ -9,6 +9,23 @@ describe('pathsDifferByOneSegment', () => {
9
9
  expect(pathsDifferByOneSegment('/ja/getting-started', '/es/getting-started', 'language')).toBe(true);
10
10
  expect(pathsDifferByOneSegment('/fr/docs/intro', '/de/docs/intro', 'language')).toBe(true);
11
11
  });
12
+ it('treats configured regional language prefixes as locales, ignoring case', () => {
13
+ const languages = ['en', 'en-GB', 'es', 'es-MX'];
14
+ expect(pathsDifferByOneSegment('/en-gb/quickstart', '/quickstart', 'language', languages)).toBe(true);
15
+ expect(pathsDifferByOneSegment('/es-MX/quickstart', '/es/quickstart', 'language', languages)).toBe(true);
16
+ });
17
+ it('treats a configured regional language after a version segment as a locale', () => {
18
+ expect(pathsDifferByOneSegment('/v2/en-gb/quickstart', '/v2/quickstart', 'language', [
19
+ 'en',
20
+ 'en-GB',
21
+ ])).toBe(true);
22
+ expect(pathsDifferByOneSegment('/v2/hi-fi/quickstart', '/v2/quickstart', 'language')).toBe(false);
23
+ });
24
+ it('does not treat unconfigured code-like segments as locales', () => {
25
+ expect(pathsDifferByOneSegment('/en-gb/quickstart', '/quickstart', 'language')).toBe(false);
26
+ expect(pathsDifferByOneSegment('/hi-fi/setup', '/de/hi-fi/setup', 'language', ['en', 'de'])).toBe(true);
27
+ expect(pathsDifferByOneSegment('/hi-fi/setup', '/de/setup', 'language', ['en', 'de'])).toBe(false);
28
+ });
12
29
  it('returns false for paths with different content', () => {
13
30
  expect(pathsDifferByOneSegment('/ja/getting-started', '/ja/quickstart', 'language')).toBe(false);
14
31
  expect(pathsDifferByOneSegment('/getting-started', '/quickstart', 'language')).toBe(false);
@@ -1,5 +1,5 @@
1
1
  import { LocaleType } from '@mintlify/models';
2
- import type { DecoratedNavigationConfig, DocsConfig } from '@mintlify/validation';
2
+ import { type DecoratedNavigationConfig, type DocsConfig } from '@mintlify/validation';
3
3
  import { DivisionMatch } from './updateFirstHrefInDivision.js';
4
4
  export declare function scopeDecoratedNavToPath(nav: DecoratedNavigationConfig, currentPath: string, userGroups?: Set<string>): DecoratedNavigationConfig;
5
5
  export declare function scopeDocsConfigNavToPath(docsConfig: DocsConfig, currentPath: string): DocsConfig;
@@ -9,7 +9,8 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { locales } from '@mintlify/models';
12
+ import { isLocale } from '@mintlify/models';
13
+ import { divisions, } from '@mintlify/validation';
13
14
  import { generatePathToLanguageDict, generatePathToVersionDict } from '../divisions/index.js';
14
15
  import { optionallyRemoveLeadingSlash } from '../fs/optionallySlash.js';
15
16
  import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
@@ -49,6 +50,31 @@ const STUB_STRIPPED_KEYS = new Set([
49
50
  function isNavNode(value) {
50
51
  return typeof value === 'object' && value !== null && !Array.isArray(value);
51
52
  }
53
+ const LANGUAGE_CONTAINER_KEYS = [...divisions, 'global'];
54
+ function addConfiguredLanguages(node, languages) {
55
+ if (Array.isArray(node)) {
56
+ for (const entry of node)
57
+ addConfiguredLanguages(entry, languages);
58
+ return;
59
+ }
60
+ if (!isNavNode(node))
61
+ return;
62
+ if (typeof node.language === 'string' && isLocale(node.language))
63
+ languages.add(node.language);
64
+ for (const key of LANGUAGE_CONTAINER_KEYS)
65
+ addConfiguredLanguages(node[key], languages);
66
+ }
67
+ const configuredLanguagesCache = new WeakMap();
68
+ function getConfiguredLanguages(nav) {
69
+ const cached = configuredLanguagesCache.get(nav);
70
+ if (cached)
71
+ return cached;
72
+ const languages = new Set();
73
+ addConfiguredLanguages(nav, languages);
74
+ const result = [...languages];
75
+ configuredLanguagesCache.set(nav, result);
76
+ return result;
77
+ }
52
78
  function isScopedDivisionKey(key) {
53
79
  return key === 'versions' || key === 'languages';
54
80
  }
@@ -143,7 +169,7 @@ function pickStubTarget(division, ctx, type) {
143
169
  const exact = pool.find((candidate) => isEqualIgnoringLeadingSlash(candidate.href, ctx.currentPath));
144
170
  if (exact)
145
171
  return exact;
146
- const pathMatch = pool.find((candidate) => pathsDifferByOneSegment(candidate.href, ctx.currentPath, type));
172
+ const pathMatch = pool.find((candidate) => pathsDifferByOneSegment(candidate.href, ctx.currentPath, type, ctx.languages));
147
173
  return pathMatch !== null && pathMatch !== void 0 ? pathMatch : pool[0];
148
174
  }
149
175
  // carrying the target's hidden/groups keeps a division with no anonymously
@@ -517,13 +543,18 @@ function scopeNode(node, ctx) {
517
543
  return scoped;
518
544
  }
519
545
  export function scopeDecoratedNavToPath(nav, currentPath, userGroups) {
546
+ const ctx = { currentPath, userGroups, languages: getConfiguredLanguages(nav) };
520
547
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
521
- return scopeNode(nav, { currentPath, userGroups });
548
+ return scopeNode(nav, ctx);
522
549
  }
523
550
  export function scopeDocsConfigNavToPath(docsConfig, currentPath) {
551
+ const ctx = {
552
+ currentPath,
553
+ languages: getConfiguredLanguages(docsConfig.navigation),
554
+ };
524
555
  return Object.assign(Object.assign({}, docsConfig), {
525
556
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
526
- navigation: scopeNode(docsConfig.navigation, { currentPath }) });
557
+ navigation: scopeNode(docsConfig.navigation, ctx) });
527
558
  }
528
559
  const pathToVersionDictCache = new WeakMap();
529
560
  const pathToLanguageDictCache = new WeakMap();
@@ -571,7 +602,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
571
602
  const strippedPath = optionallyRemoveLeadingSlash(currentPath);
572
603
  const dictVersion = cachedPathToVersionDict(decoratedNav).get(strippedPath);
573
604
  const dictLanguage = cachedPathToLanguageDict(decoratedNav).get(strippedPath);
574
- const localeFromDict = locales.find((locale) => locale === dictLanguage);
605
+ const localeFromDict = dictLanguage && isLocale(dictLanguage) ? dictLanguage : undefined;
575
606
  currentVersion =
576
607
  dictVersion ||
577
608
  getVersionOrLanguageFromPath('version', decoratedNav, currentPath, defaultVersion, (_d = (_c = initial.versions[0]) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '') ||
@@ -596,6 +627,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
596
627
  currentPath,
597
628
  currentVersion,
598
629
  currentLanguage,
630
+ languages: getConfiguredLanguages(decoratedNav),
599
631
  versionTargets: filtered.firstHrefInVersion,
600
632
  languageTargets: filtered.firstHrefInLanguage,
601
633
  userGroups,