UNPKG

@mintlify/common

Version:

Commonly shared code within Mintlify

230 lines (229 loc) 11.3 kB
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; } }; };