UNPKG

@mintlify/common

Version:

Commonly shared code within Mintlify

194 lines (193 loc) 9.56 kB
import defaultSlugify, { slugifyWithCounter } from '@sindresorhus/slugify'; import { visit } from 'unist-util-visit'; import { slugify } from '../../../slugify.js'; import { createMdxJsxAttribute } from '../../lib/remark-utils.js'; import { getTableOfContentsTitle } from '../../lib/remark-utils.js'; export function generateComponentId(title, count) { const base = defaultSlugify(title.replace(':', '-'), { decamelize: false }); return count != null && count > 0 ? `${base}-${count}` : base; } export const HEADING_LEVELS = [1, 2, 3, 4]; export const AVOIDED_PAGE_MODES = ['custom', 'frame']; export const CHILD_TAB_IDS_ATTRIBUTE = 'data-child-tab-ids'; export const CHILD_HEADING_IDS_ATTRIBUTE = 'data-child-heading-ids'; export const remarkComponentIds = (pageMetadata) => (tree) => { var _a; const slugifyFn = slugifyWithCounter(); const tabSlugifyFn = slugifyWithCounter(); const isAvoidedPageMode = AVOIDED_PAGE_MODES.includes((_a = pageMetadata === null || pageMetadata === void 0 ? void 0 : pageMetadata.mode) !== null && _a !== void 0 ? _a : ''); visit(tree, 'heading', (node) => { if (isAvoidedPageMode) return; if (HEADING_LEVELS.includes(node.depth)) { const title = getTableOfContentsTitle(node); const slug = slugify(title, slugifyFn); const mdxJsxAttributes = [ createMdxJsxAttribute('level', node.depth), createMdxJsxAttribute('id', slug), ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any node.attributes = mdxJsxAttributes; // eslint-disable-next-line @typescript-eslint/no-explicit-any node.type = 'mdxJsxFlowElement'; // eslint-disable-next-line @typescript-eslint/no-explicit-any node.name = 'Heading'; } }); const tabSlugs = new Map(); const headingSlugs = new Map(); // Ids already claimed by an explicit `id` prop. Title-derived slugs must // avoid these: the slug counter keys on the base slug, so it can otherwise // regenerate a value (e.g. `tab-2`) that an explicit id already used. Seed // up front so the result doesn't depend on document order. const usedTabIds = new Set(); visit(tree, 'mdxJsxFlowElement', (node) => { var _a; if (node.name !== 'Tab') return; const explicitId = (_a = node.attributes.find((attr) => 'name' in attr && attr.name === 'id')) === null || _a === void 0 ? void 0 : _a.value; // Expression-valued ids (e.g. id={someVar}) can't be statically resolved, so // they aren't seeded here; collectDirectChildIds falls back to the // title-derived slug for such tabs, which may diverge from the runtime value. if (explicitId && typeof explicitId === 'string') usedTabIds.add(explicitId); }); const precomputeTabSlugs = (node) => { var _a, _b, _c, _d; if (node.type === 'mdxJsxFlowElement' && node.name === 'Tab') { // Respect an explicit `id` prop rather than overwriting it with the // slugified title (same as Accordion/View below). This keeps the URL // hash and any child-id references pointing at the author-provided id. const explicitId = (_a = node.attributes.find((attr) => 'name' in attr && attr.name === 'id')) === null || _a === void 0 ? void 0 : _a.value; if (explicitId && typeof explicitId === 'string') { tabSlugs.set(node, explicitId); } else { const title = (_b = node.attributes.find((attr) => 'name' in attr && attr.name === 'title')) === null || _b === void 0 ? void 0 : _b.value; if (title && typeof title === 'string') { // Re-slug (bumping the counter) past any id an explicit prop claimed. // usedTabIds only guards against explicit ids; uniqueness among // title-derived slugs is owned by tabSlugifyFn, which monotonically // advances and never repeats a value, so the slots burned by this // loop don't need to be tracked here. let slug = slugify(title, tabSlugifyFn); while (usedTabIds.has(slug)) slug = slugify(title, tabSlugifyFn); usedTabIds.add(slug); tabSlugs.set(node, slug); } } } else if (node.type === 'mdxJsxFlowElement' && (node.name === 'Heading' || ['h1', 'h2', 'h3', 'h4'].includes((_c = node.name) !== null && _c !== void 0 ? _c : ''))) { const id = (_d = node.attributes.find((attr) => 'name' in attr && attr.name === 'id')) === null || _d === void 0 ? void 0 : _d.value; if (id && typeof id === 'string') headingSlugs.set(node, id); } if ('children' in node) { for (const child of node.children) { precomputeTabSlugs(child); } } }; const processTabsRecursively = (node) => { if (node.type === 'mdxJsxFlowElement' && node.name === 'Tab') { const slug = tabSlugs.get(node); if (!slug) return; // `slug` already equals the explicit `id` when one was provided, so only // add the attribute when the Tab doesn't already have one. const hasId = node.attributes.some((attr) => 'name' in attr && attr.name === 'id'); if (!hasId) { node.attributes.push(createMdxJsxAttribute('id', slug)); } const childTabIds = []; for (const child of node.children) { const childIds = collectDirectChildIds(child, 'Tab'); childTabIds.push(...childIds); } if (childTabIds.length > 0) { try { node.attributes.push(createMdxJsxAttribute(CHILD_TAB_IDS_ATTRIBUTE, JSON.stringify(childTabIds))); } catch (_a) { } } const childHeadingIds = collectDirectChildIds(node, 'Heading'); if (childHeadingIds.length > 0) { try { node.attributes.push(createMdxJsxAttribute(CHILD_HEADING_IDS_ATTRIBUTE, JSON.stringify(childHeadingIds))); } catch (_b) { } } for (const child of node.children) { processTabsRecursively(child); } } else if ('children' in node) { for (const child of node.children) { processTabsRecursively(child); } } }; const isHeadingNode = (node) => { var _a; return node.type === 'mdxJsxFlowElement' && (node.name === 'Heading' || ['h1', 'h2', 'h3', 'h4'].includes((_a = node.name) !== null && _a !== void 0 ? _a : '')); }; const collectDirectChildIds = (node, type) => { const childIds = []; const isMatch = node.type === 'mdxJsxFlowElement' && (type === 'Tab' ? node.name === 'Tab' : isHeadingNode(node)); if (isMatch) { const id = type === 'Tab' ? tabSlugs.get(node) : headingSlugs.get(node); if (id) { childIds.push(id); } } else if ('children' in node) { for (const child of node.children) { childIds.push(...collectDirectChildIds(child, type)); } } return childIds; }; precomputeTabSlugs(tree); processTabsRecursively(tree); const accordionCounts = new Map(); visit(tree, 'mdxJsxFlowElement', (node) => { var _a; if (node.name !== 'Accordion') return; const titleAttr = node.attributes.find((attr) => 'name' in attr && attr.name === 'title'); if (!titleAttr || typeof titleAttr.value !== 'string' || !titleAttr.value) return; const hasId = node.attributes.some((attr) => 'name' in attr && attr.name === 'id'); if (hasId) return; const title = titleAttr.value; const count = (_a = accordionCounts.get(title)) !== null && _a !== void 0 ? _a : 0; accordionCounts.set(title, count + 1); node.attributes.push(createMdxJsxAttribute('id', generateComponentId(title, count))); }); // Generate IDs for View components from their title. // Counter is keyed by the base slug (not the raw title) so distinct titles that // slugify to the same id (e.g. "Setup-Advanced" and "Setup: Advanced") still get // unique ids — required for URL hash sharing. const viewCounts = new Map(); visit(tree, 'mdxJsxFlowElement', (node) => { var _a; if (node.name !== 'View') return; const titleAttr = node.attributes.find((attr) => 'name' in attr && attr.name === 'title'); if (!titleAttr || typeof titleAttr.value !== 'string' || !titleAttr.value) return; const hasId = node.attributes.some((attr) => 'name' in attr && attr.name === 'id'); if (hasId) return; const title = titleAttr.value; const baseSlug = generateComponentId(title); const count = (_a = viewCounts.get(baseSlug)) !== null && _a !== void 0 ? _a : 0; viewCounts.set(baseSlug, count + 1); node.attributes.push(createMdxJsxAttribute('id', generateComponentId(title, count))); }); };