@mintlify/common
Version:
Commonly shared code within Mintlify
230 lines (229 loc) • 11.3 kB
JavaScript
import { visit } from 'unist-util-visit';
import { slugify } from '../../../slugify.js';
import { createMdxJsxAttribute, getTableOfContentsTitle } from '../../lib/remark-utils.js';
import { AVOIDED_PAGE_MODES, HEADING_LEVELS } from './remarkComponentIds.js';
export const HEADING_NAMES = ['h1', 'h2', 'h3', 'h4'];
const COMPONENTS_TO_EXCLUDE_HEADINGS = [
'Accordion',
'AccordionGroup',
'Expandable',
'Update',
'Prompt',
];
function getStringAttribute(attributes, name) {
const attr = attributes.find((a) => 'name' in a && a.name === name);
if (attr && 'value' in attr && typeof attr.value === 'string') {
return attr.value;
}
return undefined;
}
export const remarkExtractTableOfContents = (mdxExtracts, pageMetadata) => {
// Unified slug deduplication: both custom IDs and auto-generated slugs
// share the same seen-slug state so they cannot collide.
const seenSlugs = new Map();
const deduplicateSlug = (slug) => {
var _a;
const count = (_a = seenSlugs.get(slug)) !== null && _a !== void 0 ? _a : 0;
seenSlugs.set(slug, count + 1);
if (count === 0)
return slug;
const suffixed = `${slug}-${count + 1}`;
if (!seenSlugs.has(suffixed))
seenSlugs.set(suffixed, 0);
return suffixed;
};
return (tree) => {
const contents = [];
let hasTopLayer = false;
// the key is the node in unist
const tabContentMap = new Map();
const viewContentMap = new Map();
const excludedNodes = new Set();
visit(tree, (node) => {
if (node.type === 'mdxJsxFlowElement' &&
node.name &&
COMPONENTS_TO_EXCLUDE_HEADINGS.includes(node.name)) {
visit(node, (childNode) => {
if (childNode !== node)
excludedNodes.add(childNode);
});
}
if (node.type === 'mdxJsxFlowElement' && node.name === 'Tab') {
const idValue = getStringAttribute(node.attributes, 'id');
const titleValue = getStringAttribute(node.attributes, 'title');
const tabId = idValue !== null && idValue !== void 0 ? idValue : (titleValue ? slugify(titleValue) : undefined);
if (tabId) {
visit(node, (childNode) => {
tabContentMap.set(childNode, tabId);
});
}
}
if (node.type === 'mdxJsxFlowElement' && node.name === 'View') {
const viewId = getStringAttribute(node.attributes, 'title');
if (viewId) {
visit(node, (childNode) => {
viewContentMap.set(childNode, viewId);
});
}
}
});
visit(tree, (node) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
if (excludedNodes.has(node))
return;
const currentTabId = tabContentMap.get(node);
const currentViewId = viewContentMap.get(node);
const isValidHeading = node.type === 'heading' && HEADING_LEVELS.includes(node.depth);
const isValidMdxHeading = node.type === 'mdxJsxFlowElement' && HEADING_NAMES.includes((_a = node.name) !== null && _a !== void 0 ? _a : '');
const isTransformedHeading = node.type === 'mdxJsxFlowElement' && node.name === 'Heading';
const isValidUpdate = node.type === 'mdxJsxFlowElement' &&
node.name === 'Update' &&
node.attributes.some((attr) => 'name' in attr && attr.name === 'label');
const isValidStep = node.type === 'mdxJsxFlowElement' &&
node.name === 'Step' &&
['h2', 'h3'].includes((_b = getStringAttribute(node.attributes, 'titleSize')) !== null && _b !== void 0 ? _b : '') &&
((_d = (_c = getStringAttribute(node.attributes, 'title')) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '') !== '';
const hasIdAttribute = node.type === 'mdxJsxFlowElement' &&
node.attributes.some((attr) => 'name' in attr && attr.name === 'id');
const isAvoidedPageMode = AVOIDED_PAGE_MODES.includes((_e = pageMetadata === null || pageMetadata === void 0 ? void 0 : pageMetadata.mode) !== null && _e !== void 0 ? _e : '');
if (!isValidHeading &&
!isValidMdxHeading &&
!isTransformedHeading &&
!isValidUpdate &&
!isValidStep) {
return;
}
if (isAvoidedPageMode && (isValidHeading || isValidMdxHeading))
return;
let level;
if ('name' in node && node.name === 'Update') {
level = 1;
// @ts-expect-error we're assigning to depth despite the node not containing depth in the type
node.depth = 1;
}
else if ('name' in node && node.name === 'Step') {
const titleSize = getStringAttribute(node.attributes, 'titleSize');
const titleSizeToLevel = { h2: 2, h3: 3, h4: 4 };
level = (_f = titleSizeToLevel[titleSize !== null && titleSize !== void 0 ? titleSize : '']) !== null && _f !== void 0 ? _f : 3;
// @ts-expect-error we're assigning to depth despite the node not containing depth in the type
node.depth = level;
}
else if ('depth' in node) {
level = node.depth;
}
else if ('name' in node && node.name === 'Heading') {
const levelAttr = node.attributes.find((attr) => 'name' in attr && attr.name === 'level');
if (levelAttr && 'value' in levelAttr && typeof levelAttr.value === 'number') {
level = levelAttr.value;
}
}
else if ('name' in node && ((_g = node.name) === null || _g === void 0 ? void 0 : _g[1])) {
const num = Number(node.name[1]);
level = !isNaN(num) ? num : undefined;
if (level !== undefined) {
// @ts-expect-error we're assigning to depth despite the node not containing depth in the type
node.depth = level;
}
}
let title;
if ('name' in node && node.name === 'Step') {
title = (_h = getStringAttribute(node.attributes, 'title')) !== null && _h !== void 0 ? _h : getTableOfContentsTitle(node);
}
else {
title = getTableOfContentsTitle(node);
}
let slug;
const explicitId = ('name' in node &&
(node.name === 'Heading' || node.name === 'Step' || node.name === 'Update')) ||
hasIdAttribute
? getStringAttribute(node.attributes, 'id')
: undefined;
if (explicitId) {
slug = deduplicateSlug(explicitId);
}
else {
slug = deduplicateSlug(slugify(title));
}
let mdxJsxAttributes;
if ('name' in node && node.name === 'Update') {
const existingId = getStringAttribute(node.attributes, 'id');
mdxJsxAttributes = !existingId
? [...node.attributes, createMdxJsxAttribute('id', slug)]
: existingId !== slug
? node.attributes.map((attr) => 'name' in attr && attr.name === 'id' ? createMdxJsxAttribute('id', slug) : attr)
: node.attributes;
}
else if ('name' in node && node.name === 'Step') {
const existingId = getStringAttribute(node.attributes, 'id');
mdxJsxAttributes = !existingId
? [...node.attributes, createMdxJsxAttribute('id', slug)]
: existingId !== slug
? node.attributes.map((attr) => 'name' in attr && attr.name === 'id' ? createMdxJsxAttribute('id', slug) : attr)
: node.attributes;
}
else if ('name' in node && node.name === 'Heading') {
mdxJsxAttributes =
explicitId && slug !== explicitId
? node.attributes.map((attr) => 'name' in attr && attr.name === 'id' ? createMdxJsxAttribute('id', slug) : attr)
: node.attributes;
}
else if (level !== undefined && hasIdAttribute) {
// JSX heading with an explicit id attribute (e.g. <h2 id="custom-id">)
const updatedAttrs = explicitId && slug !== explicitId
? node.attributes.map((attr) => 'name' in attr && attr.name === 'id' ? createMdxJsxAttribute('id', slug) : attr)
: node.attributes;
mdxJsxAttributes = [createMdxJsxAttribute('level', level), ...updatedAttrs];
}
else if (level !== undefined && !hasIdAttribute) {
mdxJsxAttributes = [
createMdxJsxAttribute('level', level),
createMdxJsxAttribute('id', slug),
];
if (isValidMdxHeading && node.attributes.length > 0) {
mdxJsxAttributes.push(...node.attributes);
}
}
// @ts-expect-error we're assigning over 'attributes' if it doesn't exist
node.attributes = mdxJsxAttributes;
node.type = 'mdxJsxFlowElement';
// @ts-expect-error we're assigning over 'name' if it doesn't exist
node.name = node.name === 'Update' ? 'Update' : node.name === 'Step' ? 'Step' : 'Heading';
// @ts-expect-error we've already written to 'depth' and so this should be safe
const depth = node.depth;
if (level !== undefined && Number(level) <= 2) {
hasTopLayer = true;
contents.push({
title,
slug,
depth,
children: [],
tabId: currentTabId,
viewId: currentViewId,
});
}
else {
// Account if there is no first layer
let arrToPushInto = contents;
if (hasTopLayer) {
const lastTopLevel = contents.at(-1);
if (lastTopLevel &&
(lastTopLevel.tabId === undefined || lastTopLevel.tabId === currentTabId) &&
(lastTopLevel.viewId === undefined || lastTopLevel.viewId === currentViewId)) {
arrToPushInto = lastTopLevel.children;
}
}
arrToPushInto.push({
title,
slug,
depth,
children: [],
tabId: currentTabId,
viewId: currentViewId,
});
}
});
if (mdxExtracts) {
mdxExtracts.tableOfContents = contents;
}
};
};