UNPKG

@mintlify/validation

Version:

Validates mint.json files

364 lines (363 loc) 20.6 kB
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)); };