@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
170 lines (157 loc) • 7.23 kB
JavaScript
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
});
}