@mintlify/validation
Version:
Validates mint.json files
364 lines (363 loc) • 20.6 kB
JavaScript
import isAbsoluteUrl from 'is-absolute-url';
import _ from 'lodash';
const DEFAULT_TAB = {
tab: 'Documentation',
};
const DEFAULT_ANCHOR = {
anchor: 'Documentation',
icon: 'book-open',
};
const filterGroupsByVersion = (groups, versionName) => groups.filter((group) => group.version === versionName || group.version === undefined || !versionName);
const formatIcon = (icon, iconType) => iconType ? { name: icon, style: iconType } : icon;
const isRemoteUrl = (url) => {
if (!url)
return false;
return url.startsWith('https://') || url.startsWith('http://');
};
/**
* Get global divisions from config
* 1. External links
* 2. always there
*/
const getGlobalDivisions = (config) => {
const { tabs, anchors, versions } = config;
const globalConfig = {};
if ((versions === null || versions === void 0 ? void 0 : versions.length) &&
versions.every((version) => typeof version === 'object' && isRemoteUrl(version.url)))
globalConfig.versions = versions.map((version) => ({
version: version.name,
href: version.url,
}));
if ((tabs === null || tabs === void 0 ? void 0 : tabs.length) && tabs.every((tab) => !tab.version && isRemoteUrl(tab.url))) {
globalConfig.tabs = tabs.map((tab) => (Object.assign({ tab: tab.name, href: tab.url }, (tab.isDefaultHidden ? { hidden: tab.isDefaultHidden } : {}))));
}
if ((anchors === null || anchors === void 0 ? void 0 : anchors.length) && anchors.every((anchor) => !anchor.version && isRemoteUrl(anchor.url))) {
globalConfig.anchors = anchors.map((anchor) => (Object.assign(Object.assign(Object.assign({ anchor: anchor.name, href: anchor.url }, (anchor.icon ? { icon: formatIcon(anchor.icon, anchor.iconType) } : {})), (typeof anchor.color === 'string'
? { color: { light: anchor.color, dark: anchor.color } }
: {})), (anchor.isDefaultHidden ? { hidden: anchor.isDefaultHidden } : {}))));
}
return globalConfig;
};
const findPagesForPrefix = (groups, division, versionName, ignoredDivisions = []) => {
const matchedGroups = [];
const unmatchedGroups = [];
const prefix = division === null || division === void 0 ? void 0 : division.url;
if (isRemoteUrl(prefix)) {
return { matchedGroups, unmatchedGroups: groups };
}
groups.forEach((group) => {
const groupPages = [];
const unmatchedGroupPages = [];
const isGroupVersionMatch = group.version === versionName || group.version === undefined || !versionName;
if (isGroupVersionMatch) {
group.pages.forEach((page) => {
if (typeof page === 'string') {
if ((!prefix ||
ensureLeadingSlash(ensureTrailingSlash(page)).startsWith(ensureLeadingSlash(ensureTrailingSlash(prefix)))) &&
!ignoredDivisions.some((d) => ensureLeadingSlash(ensureTrailingSlash(page)).startsWith(ensureLeadingSlash(ensureTrailingSlash(d.url))))) {
groupPages.push(page);
}
else {
unmatchedGroupPages.push(page);
}
}
else {
const { matchedGroups: nestedGroups, unmatchedGroups: nestedUnmatchedGroups } = findPagesForPrefix([page], division, versionName, ignoredDivisions);
groupPages.push(...nestedGroups);
if (nestedUnmatchedGroups.length) {
unmatchedGroupPages.push(page);
}
}
});
}
if (groupPages.length) {
matchedGroups.push(Object.assign(Object.assign({ group: group.group }, (group.icon ? { icon: formatIcon(group.icon, group.iconType) } : {})), { pages: groupPages }));
}
if (unmatchedGroupPages.length) {
unmatchedGroups.push(Object.assign(Object.assign({}, group), { pages: unmatchedGroupPages }));
}
});
return { matchedGroups, unmatchedGroups };
};
const processDivisions = (type, divisions = [], navigationGroups = [], shouldInsertRemainingGroups = false, versionName, config, otherDivisions) => {
let remainingGroups = filterGroupsByVersion(navigationGroups, versionName);
const result = divisions
.map((division) => {
if (division.version !== versionName && versionName && division.version) {
return undefined;
}
let openapi;
if (division.openapi) {
openapi = isRemoteUrl(division.url)
? division.openapi
: {
source: division.openapi,
directory: division.url,
};
}
const baseDivision = Object.assign(Object.assign(Object.assign(Object.assign({}, (type === 'tabs' ? { tab: division.name } : { anchor: division.name })), { href: division.url }), (division.isDefaultHidden ? { hidden: division.isDefaultHidden } : {})), (openapi ? { openapi: openapi } : {}));
if (type === 'anchors') {
if ('icon' in division && division.icon) {
baseDivision.icon = formatIcon(division.icon, division.iconType);
}
if ('color' in division && division.color) {
// TODO: we need a better way to handle this
const color = typeof division.color === 'string' ? division.color : division.color.from;
baseDivision.color = {
light: color,
dark: color,
};
}
}
const ignoredDivisions = divisions.filter((d) => d.url &&
ensureLeadingSlash(d.url).startsWith(ensureLeadingSlash(division.url)) &&
d.url !== division.url);
let moreTabs = [];
let moreAnchors = [];
// process the other divisions
if (otherDivisions === null || otherDivisions === void 0 ? void 0 : otherDivisions.length) {
const matchedDivisions = otherDivisions.filter((d) => {
return ensureLeadingSlash(d.url).startsWith(ensureLeadingSlash(division.url));
});
if (matchedDivisions.length) {
const { tabs, anchors } = processDivisions(type === 'tabs' ? 'anchors' : 'tabs', matchedDivisions, navigationGroups, false, versionName, config);
moreTabs = tabs;
moreAnchors = anchors;
}
}
const { matchedGroups, unmatchedGroups } = findPagesForPrefix(remainingGroups, division, versionName, ignoredDivisions);
remainingGroups = unmatchedGroups;
const divisionWithoutHref = _.omit(baseDivision, 'href');
if (moreTabs.length)
return Object.assign(Object.assign({}, divisionWithoutHref), { tabs: moreTabs });
if (moreAnchors.length)
return Object.assign(Object.assign({}, divisionWithoutHref), { anchors: moreAnchors });
if (matchedGroups.length) {
return Object.assign(Object.assign({}, divisionWithoutHref), { groups: matchedGroups });
}
if (!matchedGroups.length && !isAbsoluteUrl(division.url) && !division.openapi)
return undefined;
if (division.openapi && !isRemoteUrl(division.url)) {
return _.omit(baseDivision, 'href');
}
return baseDivision;
})
.filter(Boolean);
if (remainingGroups.length && shouldInsertRemainingGroups) {
const { matchedGroups } = findPagesForPrefix(remainingGroups, undefined, versionName, []);
result.unshift(Object.assign(Object.assign({}, getPrimaryConfig(type === 'tabs' ? 'tab' : 'anchor', config)), { groups: matchedGroups }));
}
if (type === 'tabs') {
return { tabs: result, anchors: [], remainingGroups };
}
return { anchors: result, tabs: [], remainingGroups };
};
const findPagesForVersionOrLanguage = (groups, version, prefixes) => {
const matchedGroups = [];
groups.forEach((group) => {
const groupPages = [];
if (group.version === version || group.version === undefined) {
group.pages.forEach((page) => {
if (typeof page === 'string') {
const isGatedByDivision = Object.entries(prefixes).some(([href, versions]) => versions.includes(version) && page.startsWith(href));
const isNotIncludedInAnyVersion = Object.keys(prefixes).every((href) => !page.startsWith(href));
// we want to include the page if it's gated by a version or it's not included in any version
if (isGatedByDivision || isNotIncludedInAnyVersion) {
groupPages.push(page);
}
}
else {
const { matchedGroups: nestedGroups } = findPagesForVersionOrLanguage([page], version, prefixes);
groupPages.push(...nestedGroups);
}
});
}
if (groupPages.length) {
matchedGroups.push(Object.assign(Object.assign({ group: group.group }, (group.icon ? { icon: formatIcon(group.icon, group.iconType) } : {})), { pages: groupPages }));
}
});
return { matchedGroups };
};
const processVersionsOrLanguages = (versions = [], navigationGroups = [], prefixes) => {
const isLocale = versions.every((version) => typeof version === 'object' && version.locale);
const result = versions.map((version) => {
if (isLocale && typeof version === 'object' && version.locale) {
const baseLanguage = Object.assign({ language: version.locale }, (version.default ? { default: version.default } : {}));
if (version.url)
return Object.assign(Object.assign({}, baseLanguage), { href: version.url });
const { matchedGroups } = findPagesForVersionOrLanguage(navigationGroups, version.name, prefixes);
return Object.assign(Object.assign({}, baseLanguage), { groups: matchedGroups });
}
else {
let baseVersion;
if (typeof version === 'object') {
baseVersion = {
version: version.name,
default: version.default,
};
if (version.url)
return Object.assign(Object.assign({}, baseVersion), { href: version.url });
}
else {
baseVersion = {
version,
};
}
const { matchedGroups } = findPagesForVersionOrLanguage(navigationGroups, baseVersion.version, prefixes);
return Object.assign(Object.assign({}, baseVersion), { groups: matchedGroups });
}
});
return {
isLocale,
versions: result,
};
};
export const updateNavigationToDocsConfig = (config) => {
let { tabs, anchors, versions } = config;
const groups = config.navigation;
const { tabs: globalTabs, anchors: globalAnchors, versions: globalVersions, } = getGlobalDivisions(config);
const hasTabs = tabs === null || tabs === void 0 ? void 0 : tabs.length;
const hasAnchors = anchors === null || anchors === void 0 ? void 0 : anchors.length;
tabs = (globalTabs === null || globalTabs === void 0 ? void 0 : globalTabs.length) ? [] : tabs;
anchors = (globalAnchors === null || globalAnchors === void 0 ? void 0 : globalAnchors.length) ? [] : anchors;
versions = (globalVersions === null || globalVersions === void 0 ? void 0 : globalVersions.length) ? [] : versions;
// process divisions
const getUpdatedNavigation = (groups, config, versionName) => {
const topLevelDivision = findMostInclusiveDivision(tabs, anchors);
if (topLevelDivision === 'anchors') {
if (hasAnchors) {
const { anchors: anchorsResult, remainingGroups } = processDivisions('anchors', anchors, groups, false, versionName, config, tabs);
if (remainingGroups.length || (tabs === null || tabs === void 0 ? void 0 : tabs.length)) {
if (tabs === null || tabs === void 0 ? void 0 : tabs.length) {
const { tabs: tabsResult } = processDivisions('tabs', tabs, remainingGroups.length ? remainingGroups : groups, true, versionName, config);
anchorsResult.unshift(Object.assign(Object.assign({}, getPrimaryConfig('anchor', config)), { tabs: tabsResult }));
}
else {
const { matchedGroups } = findPagesForPrefix(remainingGroups, undefined, versionName);
anchorsResult.unshift(Object.assign(Object.assign({}, getPrimaryConfig('anchor', config)), { groups: matchedGroups }));
}
}
return { anchors: anchorsResult };
}
if (hasTabs) {
const { tabs: tabsResult } = processDivisions('tabs', tabs, groups, true, versionName, config);
return { tabs: tabsResult };
}
}
else {
if (hasTabs) {
const { tabs: tabsResult, remainingGroups } = processDivisions('tabs', tabs, groups, false, versionName, config, anchors);
if (remainingGroups.length || (anchors === null || anchors === void 0 ? void 0 : anchors.length)) {
if (anchors === null || anchors === void 0 ? void 0 : anchors.length) {
const { anchors: anchorsResult } = processDivisions('anchors', anchors, remainingGroups.length ? remainingGroups : groups, true, versionName, config);
tabsResult.unshift(Object.assign(Object.assign({}, getPrimaryConfig('tab', config)), { anchors: anchorsResult }));
}
else {
const { matchedGroups } = findPagesForPrefix(remainingGroups, undefined, versionName);
tabsResult.unshift(Object.assign(Object.assign({}, getPrimaryConfig('tab', config)), { groups: matchedGroups }));
}
}
return { tabs: tabsResult };
}
if (hasAnchors) {
const { anchors: anchorsResult } = processDivisions('anchors', anchors, groups, true, versionName, config);
return { anchors: anchorsResult };
}
}
if (groups.length) {
const parsedGroups = filterGroupsByVersion(groups, versionName).map((group) => (Object.assign({ group: group.group, pages: group.pages }, (group.icon ? { icon: formatIcon(group.icon, group.iconType) } : {}))));
return { groups: parsedGroups };
}
return undefined;
};
if ((versions === null || versions === void 0 ? void 0 : versions.length) && !(globalVersions === null || globalVersions === void 0 ? void 0 : globalVersions.length)) {
// divisions that match the versions
const prefixes = versions.reduce((acc, version) => {
const versionName = typeof version === 'string' ? version : version.name;
const anchorPrefixes = anchors === null || anchors === void 0 ? void 0 : anchors.filter((anchor) => anchor.version === versionName && !isRemoteUrl(anchor.url));
const tabPrefixes = tabs === null || tabs === void 0 ? void 0 : tabs.filter((tab) => tab.version === versionName && !isRemoteUrl(tab.url));
anchorPrefixes === null || anchorPrefixes === void 0 ? void 0 : anchorPrefixes.forEach((anchor) => (acc[anchor.url] = [...(acc[anchor.url] || []), versionName]));
tabPrefixes === null || tabPrefixes === void 0 ? void 0 : tabPrefixes.forEach((tab) => (acc[tab.url] = [...(acc[tab.url] || []), versionName]));
return acc;
}, {});
const { versions: versionsResult, isLocale } = processVersionsOrLanguages(versions, groups, prefixes);
versionsResult.forEach((version, index) => {
var _a;
// if version is an external link, skip it
if (typeof version === 'object' && 'href' in version) {
return;
}
const versionName = typeof versions[index] === 'string'
? version.version
: ((_a = versions[index]) === null || _a === void 0 ? void 0 : _a.name) || version.language;
if ('groups' in version && version.groups.length) {
const updatedNavigationPerVersion = getUpdatedNavigation(version.groups, config, versionName);
if (updatedNavigationPerVersion) {
versionsResult[index] = Object.assign(Object.assign({}, _.omit(version, 'groups')), updatedNavigationPerVersion);
}
}
else {
const updatedNavigationPerVersion = getUpdatedNavigation(groups, config, versionName);
if (updatedNavigationPerVersion) {
versionsResult[index] = Object.assign(Object.assign({}, _.omit(version, 'groups')), updatedNavigationPerVersion);
}
}
});
const navigationConfig = (isLocale ? { languages: versionsResult } : { versions: versionsResult });
if ((globalTabs === null || globalTabs === void 0 ? void 0 : globalTabs.length) || (globalAnchors === null || globalAnchors === void 0 ? void 0 : globalAnchors.length)) {
navigationConfig.global = Object.assign(Object.assign({}, ((globalTabs === null || globalTabs === void 0 ? void 0 : globalTabs.length) ? { tabs: globalTabs } : {})), ((globalAnchors === null || globalAnchors === void 0 ? void 0 : globalAnchors.length) ? { anchors: globalAnchors } : {}));
}
return navigationConfig;
}
const navigationConfig = (getUpdatedNavigation(groups, config) || {
groups: [],
});
if ((globalTabs === null || globalTabs === void 0 ? void 0 : globalTabs.length) || (globalAnchors === null || globalAnchors === void 0 ? void 0 : globalAnchors.length)) {
navigationConfig.global = Object.assign(Object.assign({}, ((globalTabs === null || globalTabs === void 0 ? void 0 : globalTabs.length) ? { tabs: globalTabs } : {})), ((globalAnchors === null || globalAnchors === void 0 ? void 0 : globalAnchors.length) ? { anchors: globalAnchors } : {}));
}
return navigationConfig;
};
/**
* Decide which division is most relevant to the top level navigation
* 1. if tabs doesn't exist, return anchors, vice versa
* 2. if both exist, return the most inclusive division
* 2.1 if tabs are more inclusive, return tabs
* 2.2 if anchors are more inclusive, return anchors
*/
function findMostInclusiveDivision(tabs, anchors) {
if (!(tabs === null || tabs === void 0 ? void 0 : tabs.length))
return 'anchors';
if (!(anchors === null || anchors === void 0 ? void 0 : anchors.length))
return 'tabs';
const inclusiveTabs = tabs.filter((tab) => {
return anchors.some((anchor) => ensureLeadingSlash(tab.url).startsWith(ensureLeadingSlash(anchor.url)));
});
const inclusiveAnchors = anchors.filter((anchor) => {
return tabs.some((tab) => ensureLeadingSlash(anchor.url).startsWith(ensureLeadingSlash(tab.url)));
});
// this means that there is a division that covers all paths
if (inclusiveTabs.length === tabs.length)
return 'tabs';
if (inclusiveAnchors.length === anchors.length)
return 'anchors';
// compare the number of inclusive divisions
return inclusiveTabs.length > inclusiveAnchors.length ? 'tabs' : 'anchors';
}
function ensureLeadingSlash(path) {
return path.startsWith('/') ? path : `/${path}`;
}
function ensureTrailingSlash(path) {
return path.endsWith('/') ? path : `${path}/`;
}
const getPrimaryConfig = (type, config) => {
var _a, _b;
if (type === 'anchor') {
return Object.assign(Object.assign({}, (((_a = config === null || config === void 0 ? void 0 : config.topAnchor) === null || _a === void 0 ? void 0 : _a.name) ? { anchor: config.topAnchor.name } : DEFAULT_ANCHOR)), (((_b = config === null || config === void 0 ? void 0 : config.topAnchor) === null || _b === void 0 ? void 0 : _b.icon)
? { icon: formatIcon(config.topAnchor.icon, config.topAnchor.iconType) }
: {}));
}
return Object.assign({}, ((config === null || config === void 0 ? void 0 : config.primaryTab)
? Object.assign({ tab: config.primaryTab.name }, (config.primaryTab.isDefaultHidden !== undefined && {
hidden: config.primaryTab.isDefaultHidden,
})) : DEFAULT_TAB));
};