@mintlify/common 1.0.1229 → 1.0.1231
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/mdx/plugins/remark/remarkExtractTableOfContents.js +9 -2
- package/dist/mdx/plugins/remark/remarkVisibilityForMarkdown.d.ts +8 -3
- package/dist/mdx/plugins/remark/remarkVisibilityForMarkdown.js +58 -8
- package/dist/navigation/filterDivisions.test.js +23 -0
- package/dist/navigation/pathsDifferByOneSegment.test.js +17 -0
- package/dist/navigation/scopeNavToPath.d.ts +1 -1
- package/dist/navigation/scopeNavToPath.js +37 -5
- package/dist/navigation/scopeNavToPath.test.js +37 -0
- package/dist/navigation/updateFirstHrefInDivision.d.ts +1 -1
- package/dist/navigation/updateFirstHrefInDivision.js +26 -23
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { visit } from 'unist-util-visit';
|
|
2
|
+
import { normalizeGroups } from '../../../navigation/groupAccess.js';
|
|
2
3
|
import { safeCleanHeadingId, slugify } from '../../../slugify.js';
|
|
3
4
|
import { createMdxJsxAttribute, getTableOfContentsTitle, getTitleMarkdownText, } from '../../lib/remark-utils.js';
|
|
4
5
|
import { isMdxJsxFragment } from '../../utils.js';
|
|
5
6
|
import { AVOIDED_PAGE_MODES, HEADING_LEVELS } from './remarkComponentIds.js';
|
|
6
|
-
import { isHiddenFromHumans, isVisibilityElement, resolveVisibilityFor, toVisibilityAuthRule, } from './remarkVisibilityForMarkdown.js';
|
|
7
|
+
import { isHiddenFromHumans, isVisibilityElement, resolveVisibilityFor, toPageGroups, toVisibilityAuthRule, } from './remarkVisibilityForMarkdown.js';
|
|
7
8
|
import { isVariationElement, resolveVariationIds } from './variation.js';
|
|
8
9
|
export const HEADING_NAMES = ['h1', 'h2', 'h3', 'h4'];
|
|
9
10
|
const COMPONENTS_TO_EXCLUDE_HEADINGS = [
|
|
@@ -20,7 +21,13 @@ function getStringAttribute(attributes, name) {
|
|
|
20
21
|
}
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
|
-
const
|
|
24
|
+
const sortedGroups = (groups) => groups && [...groups].sort();
|
|
25
|
+
const authRuleKey = (rule) => {
|
|
26
|
+
if (typeof rule === 'string')
|
|
27
|
+
return rule;
|
|
28
|
+
const normalized = normalizeGroups(toPageGroups(rule.groups));
|
|
29
|
+
return JSON.stringify([sortedGroups(normalized === null || normalized === void 0 ? void 0 : normalized.includes), sortedGroups(normalized === null || normalized === void 0 ? void 0 : normalized.excludes)]);
|
|
30
|
+
};
|
|
24
31
|
const hasAuthRules = (childRules, parentRules) => {
|
|
25
32
|
if (parentRules === undefined)
|
|
26
33
|
return true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PageGroupRule, PageGroups } from '@mintlify/models';
|
|
1
2
|
import type { Root } from 'mdast';
|
|
2
3
|
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx';
|
|
3
4
|
export type MdxJsxElement = MdxJsxFlowElement | MdxJsxTextElement;
|
|
@@ -8,9 +9,11 @@ export declare function isVisibilityElement(node: {
|
|
|
8
9
|
export declare const VISIBILITY_AUDIENCES: readonly ["humans", "agents", "signed-in", "signed-out"];
|
|
9
10
|
export type VisibilityAudience = (typeof VISIBILITY_AUDIENCES)[number];
|
|
10
11
|
export declare const VISIBILITY_FOR_EXPECTED = "\"humans\", \"agents\", \"signed-in\", \"signed-out\", or { groups: [...] }";
|
|
11
|
-
export type
|
|
12
|
-
|
|
12
|
+
export type VisibilityGroups = string[] | PageGroupRule;
|
|
13
|
+
export type VisibilityGroupsRule = {
|
|
14
|
+
groups: VisibilityGroups;
|
|
13
15
|
};
|
|
16
|
+
export type VisibilityAuthRule = 'signed-in' | 'signed-out' | VisibilityGroupsRule;
|
|
14
17
|
export type VisibilityViewer = {
|
|
15
18
|
signedIn: boolean;
|
|
16
19
|
groups: readonly string[];
|
|
@@ -20,7 +23,7 @@ export type VisibilityResolution = {
|
|
|
20
23
|
audience: VisibilityAudience;
|
|
21
24
|
} | {
|
|
22
25
|
kind: 'groups';
|
|
23
|
-
groups:
|
|
26
|
+
groups: VisibilityGroups;
|
|
24
27
|
} | {
|
|
25
28
|
kind: 'invalid';
|
|
26
29
|
raw: string;
|
|
@@ -30,6 +33,8 @@ export type VisibilityResolution = {
|
|
|
30
33
|
kind: 'missing';
|
|
31
34
|
};
|
|
32
35
|
export declare function isVisibilityAudience(value: unknown): value is VisibilityAudience;
|
|
36
|
+
export declare const toPageGroups: (groups: VisibilityGroups) => PageGroups;
|
|
37
|
+
export declare function toVisibilityGroupsRule(value: unknown): VisibilityGroupsRule | undefined;
|
|
33
38
|
export declare function resolveVisibilityFor(node: MdxJsxElement): VisibilityResolution;
|
|
34
39
|
export declare function isHiddenFromHumans(resolution: VisibilityResolution): boolean;
|
|
35
40
|
export declare function isHiddenFromAgents(resolution: VisibilityResolution): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WILDCARD_GROUP, checkGroupAccess, isPageGroups } from '../../../navigation/groupAccess.js';
|
|
1
2
|
export function isVisibilityElement(node) {
|
|
2
3
|
return ((node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
|
|
3
4
|
node.name === 'Visibility');
|
|
@@ -42,19 +43,68 @@ function foldString(node) {
|
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
const trimGroupNames = (names) => [...new Set(names.map((name) => name.trim()))];
|
|
47
|
+
const isGroupNames = (groups) => groups.every((entry) => typeof entry === 'string');
|
|
48
|
+
export const toPageGroups = (groups) => Array.isArray(groups) ? groups : [groups];
|
|
49
|
+
function toVisibilityGroups(value) {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
if (value.length === 0 || !isPageGroups(value) || !isGroupNames(value))
|
|
52
|
+
return undefined;
|
|
53
|
+
const names = trimGroupNames(value);
|
|
54
|
+
return isPageGroups(names) ? names : undefined;
|
|
55
|
+
}
|
|
56
|
+
const rules = [value];
|
|
57
|
+
if (!isPageGroups(rules) || isGroupNames(rules))
|
|
58
|
+
return undefined;
|
|
59
|
+
const [rule] = rules.map(({ includes, excludes }) => (Object.assign(Object.assign({}, (includes && { includes: trimGroupNames(includes) })), (excludes && { excludes: trimGroupNames(excludes) }))));
|
|
60
|
+
return rule && isPageGroups([rule]) ? rule : undefined;
|
|
61
|
+
}
|
|
62
|
+
export function toVisibilityGroupsRule(value) {
|
|
63
|
+
if (!isRecord(value) || Object.keys(value).length !== 1)
|
|
64
|
+
return undefined;
|
|
65
|
+
const groups = toVisibilityGroups(value.groups);
|
|
66
|
+
return groups && { groups };
|
|
67
|
+
}
|
|
68
|
+
function staticKey(property) {
|
|
69
|
+
if (property.type !== 'Property' || property.computed)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (property.key.type === 'Identifier')
|
|
72
|
+
return property.key.name;
|
|
73
|
+
return property.key.type === 'Literal' && typeof property.key.value === 'string'
|
|
74
|
+
? property.key.value
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
function foldGroupNames(array) {
|
|
78
|
+
const names = array.elements.map((element) => foldString(element));
|
|
79
|
+
return names.every((name) => name !== undefined) ? names : undefined;
|
|
80
|
+
}
|
|
81
|
+
function foldGroupRule(object) {
|
|
82
|
+
const rule = {};
|
|
83
|
+
for (const property of object.properties) {
|
|
84
|
+
const key = staticKey(property);
|
|
85
|
+
if (property.type !== 'Property' || (key !== 'includes' && key !== 'excludes')) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const names = property.value.type === 'ArrayExpression' ? foldGroupNames(property.value) : undefined;
|
|
89
|
+
if (!names)
|
|
90
|
+
return undefined;
|
|
91
|
+
rule[key] = names;
|
|
92
|
+
}
|
|
93
|
+
return rule;
|
|
94
|
+
}
|
|
45
95
|
function resolveGroups(value) {
|
|
46
96
|
const expression = expressionOf(value);
|
|
47
97
|
if ((expression === null || expression === void 0 ? void 0 : expression.type) !== 'ObjectExpression' || expression.properties.length !== 1)
|
|
48
98
|
return undefined;
|
|
49
99
|
const [property] = expression.properties;
|
|
50
|
-
if ((property === null || property === void 0 ? void 0 : property.type) !== 'Property' || property
|
|
51
|
-
return undefined;
|
|
52
|
-
const key = property.key.type === 'Identifier' ? property.key.name : undefined;
|
|
53
|
-
const literalKey = property.key.type === 'Literal' ? property.key.value : undefined;
|
|
54
|
-
if ((key !== null && key !== void 0 ? key : literalKey) !== 'groups' || property.value.type !== 'ArrayExpression')
|
|
100
|
+
if ((property === null || property === void 0 ? void 0 : property.type) !== 'Property' || staticKey(property) !== 'groups')
|
|
55
101
|
return undefined;
|
|
56
|
-
|
|
57
|
-
|
|
102
|
+
if (property.value.type === 'ObjectExpression') {
|
|
103
|
+
return toVisibilityGroups(foldGroupRule(property.value));
|
|
104
|
+
}
|
|
105
|
+
return property.value.type === 'ArrayExpression'
|
|
106
|
+
? toVisibilityGroups(foldGroupNames(property.value))
|
|
107
|
+
: undefined;
|
|
58
108
|
}
|
|
59
109
|
function mayOverrideFor(attr) {
|
|
60
110
|
if (attr.type !== 'mdxJsxExpressionAttribute')
|
|
@@ -172,7 +222,7 @@ export function matchesVisibilityAuthRule(rule, viewer) {
|
|
|
172
222
|
return viewer.signedIn;
|
|
173
223
|
if (rule === 'signed-out')
|
|
174
224
|
return !viewer.signedIn;
|
|
175
|
-
return viewer.signedIn
|
|
225
|
+
return checkGroupAccess(toPageGroups(rule.groups), new Set(viewer.signedIn ? [...viewer.groups, WILDCARD_GROUP] : []));
|
|
176
226
|
}
|
|
177
227
|
// `export * from` is intentionally ignored: it exposes no statically-knowable
|
|
178
228
|
// names and creates no page-local JSX bindings in MDX, so there is nothing to
|
|
@@ -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
|
|
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 {
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
@@ -43,6 +43,43 @@ describe('scopeDecoratedNavToPath', () => {
|
|
|
43
43
|
expect(es.pages).toEqual([{ title: 'es', href: '/es/get-started/intro' }]);
|
|
44
44
|
expect(fr.pages).toEqual([{ title: 'fr', href: '/fr/other/page' }]);
|
|
45
45
|
});
|
|
46
|
+
it('keeps a regional language division active on its own pages', () => {
|
|
47
|
+
const regionalNav = {
|
|
48
|
+
languages: [
|
|
49
|
+
{ language: 'en', default: true, pages: [page('/intro'), page('/install')] },
|
|
50
|
+
{ language: 'en-GB', pages: [page('/en-gb/intro'), page('/en-gb/install')] },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
const [en, enGB] = loose(scopeDecoratedNavToPath(regionalNav, '/en-gb/intro')).languages;
|
|
54
|
+
if (!en || !enGB)
|
|
55
|
+
throw new Error('missing languages');
|
|
56
|
+
expect(enGB.pages).toEqual([page('/en-gb/intro'), page('/en-gb/install')]);
|
|
57
|
+
expect(en.pages).toEqual([{ title: 'en', href: '/intro' }]);
|
|
58
|
+
});
|
|
59
|
+
it('stubs a regional language division with the same page under its prefix', () => {
|
|
60
|
+
const regionalNav = {
|
|
61
|
+
languages: [
|
|
62
|
+
{ language: 'en', default: true, pages: [page('/intro'), page('/install')] },
|
|
63
|
+
{ language: 'en-GB', pages: [page('/en-gb/intro'), page('/en-gb/install')] },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const [, enGB] = loose(scopeDecoratedNavToPath(regionalNav, '/install')).languages;
|
|
67
|
+
expect(enGB === null || enGB === void 0 ? void 0 : enGB.pages).toEqual([{ title: 'en-GB', href: '/en-gb/install' }]);
|
|
68
|
+
});
|
|
69
|
+
it('ignores a page field named language when finding configured languages', () => {
|
|
70
|
+
const nav = {
|
|
71
|
+
languages: [
|
|
72
|
+
{
|
|
73
|
+
language: 'en',
|
|
74
|
+
default: true,
|
|
75
|
+
pages: [{ title: 'Setup', href: '/hi-fi/setup', language: 'hi-fi' }],
|
|
76
|
+
},
|
|
77
|
+
{ language: 'de', pages: [page('/de/setup'), page('/de/hi-fi/setup')] },
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
const [, de] = loose(scopeDecoratedNavToPath(nav, '/hi-fi/setup')).languages;
|
|
81
|
+
expect(de === null || de === void 0 ? void 0 : de.pages).toEqual([{ title: 'de', href: '/de/hi-fi/setup' }]);
|
|
82
|
+
});
|
|
46
83
|
it('falls back to the default division when the path is not found', () => {
|
|
47
84
|
const scoped = scopeDecoratedNavToPath(decoratedNav, '/does/not/exist');
|
|
48
85
|
const [en, es] = loose(scoped).languages;
|
|
@@ -28,4 +28,4 @@ export declare function updateFirstHrefInDivision({ type, currentPath, entry, pa
|
|
|
28
28
|
activeGroupNames?: Set<string>;
|
|
29
29
|
activeDivisionOwner?: object;
|
|
30
30
|
}): void;
|
|
31
|
-
export declare function pathsDifferByOneSegment(a: string, b: string, type: 'version' | 'language'): boolean;
|
|
31
|
+
export declare function pathsDifferByOneSegment(a: string, b: string, type: 'version' | 'language', languages?: readonly string[]): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isKnownLocale } from '@mintlify/models';
|
|
2
2
|
import { divisions, } from '@mintlify/validation';
|
|
3
3
|
import { readObjectArrayProperty } from '../divisions/iterateObjectArrayProperty.js';
|
|
4
4
|
import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
|
|
@@ -209,29 +209,30 @@ function findActiveGroupNames(entry, currentPath, activeGroupNames = new Set())
|
|
|
209
209
|
}
|
|
210
210
|
return activeGroupNames;
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
const isLanguageSegment = (segment, languages) => {
|
|
213
|
+
if (segment === undefined)
|
|
214
|
+
return false;
|
|
215
|
+
if (isKnownLocale(segment))
|
|
216
|
+
return true;
|
|
217
|
+
const lowerCaseSegment = segment.toLowerCase();
|
|
218
|
+
return languages.some((language) => language.toLowerCase() === lowerCaseSegment);
|
|
219
|
+
};
|
|
220
|
+
function parseLanguagePath(segments, languages) {
|
|
213
221
|
let index = 0;
|
|
214
222
|
let locale;
|
|
215
223
|
let hasDocsBase = false;
|
|
216
224
|
const firstSegment = segments[0];
|
|
217
225
|
const firstIsDocsBase = !!firstSegment && ACCEPTED_BASE_PATHS.includes(firstSegment);
|
|
218
|
-
const firstIsLocale = !!firstSegment &&
|
|
219
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
|
|
220
|
-
locales.includes(firstSegment);
|
|
221
226
|
if (firstIsDocsBase) {
|
|
222
227
|
hasDocsBase = true;
|
|
223
228
|
index = 1;
|
|
224
229
|
const maybeLocale = segments[index];
|
|
225
|
-
if (maybeLocale
|
|
226
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
|
|
227
|
-
locales.includes(maybeLocale)) {
|
|
228
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
|
|
230
|
+
if (isLanguageSegment(maybeLocale, languages)) {
|
|
229
231
|
locale = maybeLocale;
|
|
230
232
|
index++;
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
|
-
else if (
|
|
234
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
|
|
235
|
+
else if (isLanguageSegment(firstSegment, languages)) {
|
|
235
236
|
locale = firstSegment;
|
|
236
237
|
index = 1;
|
|
237
238
|
const maybeDocsBase = segments[index];
|
|
@@ -283,8 +284,9 @@ function maybeUpdateFirstHrefInDivision(type, href, finalVersion, inActiveGroup,
|
|
|
283
284
|
// we make the BOLD assumption that href is a different version of the same page
|
|
284
285
|
if (currentVersion !== undefined && finalVersion !== undefined) {
|
|
285
286
|
const existingRef = firstHrefInDivision.get(finalVersion);
|
|
287
|
+
const languages = type === 'language' ? versionStrings : undefined;
|
|
286
288
|
if ((existingRef === undefined || DivisionMatchLevel.PATH < existingRef.matchLevel) &&
|
|
287
|
-
pathsDifferByOneSegment(href, currentPath, type)) {
|
|
289
|
+
pathsDifferByOneSegment(href, currentPath, type, languages)) {
|
|
288
290
|
firstHrefInDivision.set(finalVersion, { href, matchLevel: DivisionMatchLevel.PATH });
|
|
289
291
|
}
|
|
290
292
|
}
|
|
@@ -316,18 +318,19 @@ function maybeUpdateFirstHrefInDivision(type, href, finalVersion, inActiveGroup,
|
|
|
316
318
|
}
|
|
317
319
|
}
|
|
318
320
|
}
|
|
319
|
-
export function pathsDifferByOneSegment(a, b, type) {
|
|
321
|
+
export function pathsDifferByOneSegment(a, b, type, languages = []) {
|
|
322
|
+
var _a, _b;
|
|
320
323
|
const aSegments = a.split('/').filter(Boolean);
|
|
321
324
|
const bSegments = b.split('/').filter(Boolean);
|
|
322
325
|
// For language switching, we need to be more strict - paths should be identical except for the language segment
|
|
323
326
|
// once normalized, content segments should be identical and only locale prefix may differ.
|
|
324
327
|
if (type === 'language') {
|
|
325
|
-
const aPath = parseLanguagePath(aSegments);
|
|
326
|
-
const bPath = parseLanguagePath(bSegments);
|
|
328
|
+
const aPath = parseLanguagePath(aSegments, languages);
|
|
329
|
+
const bPath = parseLanguagePath(bSegments, languages);
|
|
327
330
|
if (aPath.hasDocsBase !== bPath.hasDocsBase)
|
|
328
331
|
return false;
|
|
329
332
|
if (!aPath.locale && !bPath.locale) {
|
|
330
|
-
return differByOneLocaleSegment(aPath.contentSegments, bPath.contentSegments);
|
|
333
|
+
return differByOneLocaleSegment(aPath.contentSegments, bPath.contentSegments, languages);
|
|
331
334
|
}
|
|
332
335
|
// For language switching, the content parts (including version segments) must be identical.
|
|
333
336
|
if (aPath.contentSegments.length !== bPath.contentSegments.length)
|
|
@@ -340,7 +343,7 @@ export function pathsDifferByOneSegment(a, b, type) {
|
|
|
340
343
|
// The paths are language variants if locale presence/value differs.
|
|
341
344
|
if (!aPath.locale && !bPath.locale)
|
|
342
345
|
return false;
|
|
343
|
-
return aPath.locale !== bPath.locale;
|
|
346
|
+
return ((_a = aPath.locale) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== ((_b = bPath.locale) === null || _b === void 0 ? void 0 : _b.toLowerCase());
|
|
344
347
|
}
|
|
345
348
|
const aStartsWithDocs = aSegments[0] && ACCEPTED_BASE_PATHS.includes(aSegments[0]);
|
|
346
349
|
const bStartsWithDocs = bSegments[0] && ACCEPTED_BASE_PATHS.includes(bSegments[0]);
|
|
@@ -377,22 +380,22 @@ export function pathsDifferByOneSegment(a, b, type) {
|
|
|
377
380
|
}
|
|
378
381
|
return true;
|
|
379
382
|
}
|
|
380
|
-
function
|
|
381
|
-
return locales.some((locale) => locale === segment);
|
|
382
|
-
}
|
|
383
|
-
function differByOneLocaleSegment(a, b) {
|
|
383
|
+
function differByOneLocaleSegment(a, b, languages) {
|
|
384
384
|
if (a.length === b.length) {
|
|
385
385
|
const mismatches = a.flatMap((segment, i) => (segment === b[i] ? [] : [i]));
|
|
386
386
|
if (mismatches.length !== 1)
|
|
387
387
|
return false;
|
|
388
388
|
const [index] = mismatches;
|
|
389
|
-
return index !== undefined &&
|
|
389
|
+
return (index !== undefined &&
|
|
390
|
+
isLanguageSegment(a[index], languages) &&
|
|
391
|
+
isLanguageSegment(b[index], languages));
|
|
390
392
|
}
|
|
391
393
|
const [longer, shorter] = a.length > b.length ? [a, b] : [b, a];
|
|
392
394
|
if (longer.length !== shorter.length + 1)
|
|
393
395
|
return false;
|
|
394
396
|
const index = longer.findIndex((segment, i) => segment !== shorter[i]);
|
|
395
|
-
return (
|
|
397
|
+
return (isLanguageSegment(longer[index], languages) &&
|
|
398
|
+
longer.slice(index + 1).every((s, i) => s === shorter[index + i]));
|
|
396
399
|
}
|
|
397
400
|
function checkActiveGroup(group, currentPath) {
|
|
398
401
|
if (isPage(group)) {
|