UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

170 lines (157 loc) 7.23 kB
import { markdownToMetadata, metadataToMarkdown } from "./metadataToMarkdown.mjs"; /** * Options for mergeMetadataMarkdown */ /** * Merges new page metadata with existing markdown content, preserving the order * of pages from the existing markdown when available, unless the file contains * only the autogeneration marker (no editable section), in which case pages are * sorted alphabetically by title. * * Pages are matched by their `path` property (e.g., './button/page.mdx'), not by slug. * This allows multiple pages to have the same slug (anchor) while still being treated * as distinct pages. * * @param existingMarkdown - The existing markdown content (or undefined if none exists) * @param newMetadata - The new metadata to merge in * @param options - Optional configuration * @param options.preserveUnlisted - If true, pages in existing markdown that aren't in newMetadata will be preserved. If false (default), they are removed. * @param options.indexWrapperComponent - Optional component name to wrap the autogenerated content (e.g., 'PagesIndex') * @returns The updated markdown content with merged metadata * * @example * ```ts * const existingMarkdown = `# Components * - Button - ([Outline](#button), [Contents](./button/page.mdx)) - A button * - Checkbox - ([Outline](#checkbox), [Contents](./checkbox/page.mdx)) - A checkbox * `; * * const newMetadata = { * title: 'Components', * pages: [ * { slug: 'checkbox', path: './checkbox/page.mdx', title: 'Checkbox', description: 'Updated checkbox' }, * { slug: 'button', path: './button/page.mdx', title: 'Button', description: 'Updated button' }, * { slug: 'input', path: './input/page.mdx', title: 'Input', description: 'New input' }, * ], * }; * * const result = await mergeMetadataMarkdown(existingMarkdown, newMetadata); * // Result preserves Button, Checkbox order from existing markdown, adds Input at the end * ``` */ export async function mergeMetadataMarkdown(existingMarkdown, newMetadata, options = {}) { const { indexWrapperComponent, path, preserveExistingTitleAndSlug } = options; // If no existing markdown, just convert the new metadata // Use the provided wrapper unless it's null (which means remove) if (!existingMarkdown) { return metadataToMarkdown(newMetadata, { indexWrapperComponent: indexWrapperComponent === null ? undefined : indexWrapperComponent, path }); } // Parse the existing markdown to get the current order const existingMetadata = await markdownToMetadata(existingMarkdown); // If parsing failed, just use the new metadata if (!existingMetadata) { return metadataToMarkdown(newMetadata, { indexWrapperComponent: indexWrapperComponent === null ? undefined : indexWrapperComponent, path }); } // Determine effective wrapper component: // - undefined: preserve existing // - null: explicitly remove // - string: use provided value let effectiveWrapper; if (indexWrapperComponent === undefined) { effectiveWrapper = existingMetadata.indexWrapperComponent; } else if (indexWrapperComponent === null) { effectiveWrapper = undefined; } else { effectiveWrapper = indexWrapperComponent; } // Create a map of new pages by path for quick lookup const newPagesMap = new Map(); for (const page of newMetadata.pages) { newPagesMap.set(page.path, page); } // Build the merged pages array, preserving order from existing markdown let pages = []; const addedPaths = new Set(); // First, add all pages that exist in the existing markdown, in their original order for (const existingPage of existingMetadata.pages) { const newPage = newPagesMap.get(existingPage.path); if (newPage) { // Page exists in both - merge the metadata // Only exclude descriptionMarkdown if newPage provides a new description const { descriptionMarkdown, ...existingPageWithoutDescriptionMarkdown } = existingPage; const merged = { ...(newPage.description ? existingPageWithoutDescriptionMarkdown : existingPage), ...newPage, // Optionally preserve title/slug from existing (for auto-generated metadata that shouldn't override) ...(preserveExistingTitleAndSlug ? { title: existingPage.title || newPage.title, slug: existingPage.slug || newPage.slug } : {}), // Preserve tags from existing (user-managed, program should never delete tags) tags: existingPage.tags, // Preserve skipDetailSection from existing (user-managed for external links) skipDetailSection: existingPage.skipDetailSection, // Preserve sections from existing if new doesn't have them sections: newPage.sections || existingPage.sections, // Preserve displayTitle (user-managed title override) only if it still // differs from the new title. If the override now matches the actual title, // clear it so the titles stay in sync going forward. displayTitle: existingPage.displayTitle && existingPage.displayTitle !== newPage.title ? existingPage.displayTitle : undefined }; pages.push(merged); addedPaths.add(newPage.path); } // If page doesn't exist in new metadata, it's been removed - don't include it } // Then, add any new pages that weren't in the existing markdown for (const newPage of newMetadata.pages) { if (!addedPaths.has(newPage.path)) { // This is a new page - automatically add the [New] tag const pageWithTag = { ...newPage, tags: newPage.tags ? [...newPage.tags, 'New'] : ['New'] }; pages.push(pageWithTag); addedPaths.add(newPage.path); } } // If alphabetical sorting is requested, sort pages alphabetically by title const alphabeticalSortMarker = "[//]: # 'This section is autogenerated, but the following list order, title, and [Tag]s can be modified, but nothing within the parentheses. Automatically sorted alphabetically.'"; // TODO: Remove the old marker check once all index files have been migrated to the new format. const oldAlphabeticalSortMarker = "[//]: # 'This file is autogenerated, but the following list can be modified. Automatically sorted alphabetically.'"; const requestsAlphabeticalSort = existingMarkdown.includes(alphabeticalSortMarker) || existingMarkdown.includes(oldAlphabeticalSortMarker); if (requestsAlphabeticalSort) { pages = pages.sort((a, b) => { const titleA = a.displayTitle ?? a.title ?? a.slug; const titleB = b.displayTitle ?? b.title ?? b.slug; return titleA.localeCompare(titleB); }); } // Create the final metadata with merged pages const mergedMetadata = { title: newMetadata.title, // Always use the new title pages, // Preserve the existing pageMetadata (e.g., robots config) from the current file pageMetadata: existingMetadata.pageMetadata }; // Preserve the alphabetical sorting marker if it was present return metadataToMarkdown(mergedMetadata, { editableMarker: requestsAlphabeticalSort ? alphabeticalSortMarker : undefined, indexWrapperComponent: effectiveWrapper, path }); }