@mintlify/common
Version:
Commonly shared code within Mintlify
194 lines (193 loc) • 9.56 kB
JavaScript
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)));
});
};